<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gowtham Potureddi</title>
    <description>The latest articles on DEV Community by Gowtham Potureddi (@gowthampotureddi).</description>
    <link>https://dev.to/gowthampotureddi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3874592%2Fb901f929-0a60-4dd2-9dac-22ce22291bdc.png</url>
      <title>DEV Community: Gowtham Potureddi</title>
      <link>https://dev.to/gowthampotureddi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gowthampotureddi"/>
    <language>en</language>
    <item>
      <title>From Data Engineer to Data Architect: Skill Ladder, Certifications &amp; Interview Signals</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Sat, 01 Aug 2026 13:35:22 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/from-data-engineer-to-data-architect-skill-ladder-certifications-interview-signals-1a2d</link>
      <guid>https://dev.to/gowthampotureddi/from-data-engineer-to-data-architect-skill-ladder-certifications-interview-signals-1a2d</guid>
      <description>&lt;p&gt;The move &lt;strong&gt;&lt;code&gt;data engineer to data architect&lt;/code&gt;&lt;/strong&gt; is the single most misunderstood step on the data career ladder, because it looks like a promotion inside the same job when it is actually a change of job entirely — the day you become an architect you stop being measured on the pipelines you ship and start being measured on the decisions other people ship because of you. A strong data engineer is graded on throughput: DAGs that run green, latency that stays under SLA, tables that land on time, bugs that get fixed fast. A data architect is graded on judgement: whether the blueprint they drew survives three years of scaling, five business units, two acquisitions, and a governance audit — and whether the twelve engineers building against that blueprint could do their jobs without asking the architect what to do next. The gap between those two grading rubrics is the whole subject of this guide, and it is why so many excellent engineers stall at the boundary: they keep sharpening the skill that got them here instead of building the &lt;strong&gt;skill ladder&lt;/strong&gt; the new role demands.&lt;/p&gt;

&lt;p&gt;That new ladder has five load-bearing rungs — &lt;strong&gt;data modeling&lt;/strong&gt;, governance and security, integration patterns, cost and performance, and cloud platforms — sitting under a sixth skill that ties them together: the ability to explain a trade-off out loud, in numbers, to a room that includes a CFO and a compliance officer as well as engineers. This walkthrough is the senior-DE map for climbing all six: what actually changes when the mandate shifts from implementation to blueprint, which &lt;strong&gt;data architect skills&lt;/strong&gt; are worth building first and which are noise, which &lt;strong&gt;data architect certification&lt;/strong&gt; paths genuinely move the needle versus which are résumé decoration, how the architecture design interview is scored (and why "governance-first" beats "clever" almost every time), and a concrete 18-month transition roadmap that turns a good data engineer into a credible &lt;strong&gt;enterprise architecture&lt;/strong&gt; hire. Each section pairs a teaching block with a worked interview answer — code or a text template, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd746pwvheeuzg5udiu5w.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd746pwvheeuzg5udiu5w.jpeg" alt="PipeCode blog header for the data engineer to data architect skill ladder — bold white headline over a hero composition of a rising ladder of glyph rungs (modeling, governance, integration, cost, cloud) climbing toward a blueprint seal, on a dark gradient." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt;, build modeling muscle on the &lt;a href="https://pipecode.ai/explore/practice/topic/dimensional-modeling" rel="noopener noreferrer"&gt;dimensional-modeling practice library →&lt;/a&gt;, and keep your query foundations sharp with the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DE vs Data Architect: what actually changes&lt;/li&gt;
&lt;li&gt;The architect skill ladder: modeling, governance, cost&lt;/li&gt;
&lt;li&gt;Certifications that actually move the needle&lt;/li&gt;
&lt;li&gt;Architecture interviews: blueprints &amp;amp; trade-offs&lt;/li&gt;
&lt;li&gt;Signals architects are graded on &amp;amp; the transition plan&lt;/li&gt;
&lt;li&gt;Cheat sheet — DE → architect recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. DE vs Data Architect: what actually changes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The mandate shift from implementation to blueprint — you stop owning the DAG and start owning the decision
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;the data engineer owns the implementation and the data architect owns the blueprint — the architect is accountable for the shape of the system, the standards the engineers build against, and the trade-offs the business signs off on, while being deliberately one level removed from the day-to-day code that ships&lt;/strong&gt;. This is not a seniority bump inside the same lane; it is a lane change. The failure mode that kills most transitions is the engineer who gets the architect title and keeps doing the engineer's job — reviewing every PR, writing the tricky DAGs personally, being the on-call hero — while the actual architect responsibilities (the reference designs, the governance model, the data-contract standards, the cost envelope) go unowned because nobody was told they were now theirs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changes across the four dimensions interviewers probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope of accountability.&lt;/strong&gt; A DE is accountable for a component — a pipeline, a table, a service. An architect is accountable for a system — how components fit, what standard they share, how the whole thing evolves. Interviewers test this by asking you to zoom out: "don't tell me how you'd build the pipeline, tell me how you'd decide whether this data even belongs in the warehouse."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breadth vs depth.&lt;/strong&gt; The DE goes deep on a stack — Spark internals, Airflow scheduling, Postgres query plans. The architect goes broad across domains — modeling, governance, integration, cost, cloud, security — trading some depth in each for the ability to reason about all of them at once. You cannot be an architect who only knows the ingestion layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time horizon.&lt;/strong&gt; The DE optimises for the sprint and the incident. The architect optimises for the three-year horizon — the migration cost, the vendor lock-in, the schema decisions that are cheap now and expensive later. "What will this decision cost us in year three?" is an architect question, never a DE question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communication surface.&lt;/strong&gt; The DE talks to other engineers and a manager. The architect talks to engineers, product, finance, security, and often the C-suite. The blueprint only matters if the people funding it and building it both understand it, so the architect's output is as much a document and a conversation as it is a diagram.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The skill-ladder overview — the six things an architect is expected to hold.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data modeling.&lt;/strong&gt; Dimensional (star / snowflake), normalized (3NF), and Data Vault — knowing which fits which workload and why. This is the pillar DEs are closest to and still underrate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance and security.&lt;/strong&gt; Catalog, lineage, access control, PII classification, data contracts, retention. The pillar DEs are furthest from and interviewers weight highest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration patterns.&lt;/strong&gt; Batch, streaming, CDC, API integration, event-driven — the menu of how systems exchange data and the trade-offs of each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost and performance.&lt;/strong&gt; Storage tiering, partitioning, compute right-sizing, the FinOps of a warehouse. The pillar that turns a "cool design" into a "fundable design".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud platforms.&lt;/strong&gt; The reference services on at least one major cloud, and enough of the other two to reason about portability and lock-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communication and stewardship.&lt;/strong&gt; ADRs, reference designs, trade-off narratives — the connective tissue that makes the other five visible to the org.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for in the DE-to-architect screen.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you frame answers as &lt;strong&gt;decisions with trade-offs&lt;/strong&gt; rather than tools with features? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you lead with &lt;strong&gt;governance and cost&lt;/strong&gt; unprompted, not just latency and correctness? — required answer.&lt;/li&gt;
&lt;li&gt;Do you say &lt;strong&gt;"it depends, and here's the axis it depends on"&lt;/strong&gt; instead of naming a single right answer? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you describe your role as &lt;strong&gt;"setting the standard other teams build against"&lt;/strong&gt; rather than "building the hardest pipelines"? — required answer.&lt;/li&gt;
&lt;li&gt;Do you show you can &lt;strong&gt;zoom out to the business&lt;/strong&gt; — cost, risk, compliance — not just the stack? — senior signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the DE-vs-architect responsibility table
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most useful artifact for the transition conversation — and the first thing a hiring manager wants to see you understand — is a clean responsibility split between the two roles. Getting this table right proves you know what you are signing up for, which is half the battle in an architect screen.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The axis.&lt;/strong&gt; For each responsibility, who is accountable (owns the outcome) versus who is responsible (does the work).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trap.&lt;/strong&gt; Candidates who say the architect "does everything the DE does, plus design" have not understood the lane change — the architect is &lt;em&gt;less&lt;/em&gt; hands-on by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The signal.&lt;/strong&gt; The strongest answers show the architect delegating implementation and owning standards, review of designs (not code), and cross-team consistency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the responsibility split between data engineer and data architect for a typical warehouse-plus-lakehouse platform team, and identify the two responsibilities most likely to be dropped in a botched transition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Data engineer&lt;/th&gt;
&lt;th&gt;Data architect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline / DAG implementation&lt;/td&gt;
&lt;td&gt;owns&lt;/td&gt;
&lt;td&gt;reviews design, not code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table / schema design&lt;/td&gt;
&lt;td&gt;owns per-table&lt;/td&gt;
&lt;td&gt;owns modeling standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference architecture&lt;/td&gt;
&lt;td&gt;consumes&lt;/td&gt;
&lt;td&gt;owns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance &amp;amp; data contracts&lt;/td&gt;
&lt;td&gt;applies&lt;/td&gt;
&lt;td&gt;defines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost envelope / FinOps&lt;/td&gt;
&lt;td&gt;reports usage&lt;/td&gt;
&lt;td&gt;owns budget &amp;amp; tiering policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-team consistency&lt;/td&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;owns platform-wide&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Responsibility split — DE vs Data Architect (RACI-style)
========================================================

R = Responsible (does the work)   A = Accountable (owns the outcome)

Area                         | DE   | Architect
-----------------------------+------+----------
Build a pipeline             |  R   |   -
Design one table             |  R   |  (A on the standard)
Set the modeling standard    |  -   |   A
Choose batch vs streaming    |  R*  |   A     (* recommends; architect decides)
Define data contracts        |  R   |   A
Classify PII / access tiers   |  R   |   A
Own the cost envelope        |  -   |   A
Approve a reference design    |  -   |   A
Cross-team schema consistency |  -   |   A
On-call for a pipeline       |  R   |   -

Most-dropped in a botched transition:
  1. "Own the cost envelope"  — nobody picks it up; spend drifts.
  2. "Cross-team consistency" — each team re-invents; the platform fragments.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the table top-down and notice the pattern: the DE column is full of &lt;em&gt;R&lt;/em&gt; (does the work), the architect column is full of &lt;em&gt;A&lt;/em&gt; (owns the outcome). The architect is accountable for far more than they are responsible for — that is the definition of the role.&lt;/li&gt;
&lt;li&gt;The single row that flips both — "choose batch vs streaming" — is where DE and architect overlap. The DE recommends based on hands-on knowledge; the architect decides based on the trade-off across cost, latency, and governance. Naming this handoff explicitly is a senior signal.&lt;/li&gt;
&lt;li&gt;"Set the modeling standard" has no DE responsibility at all — the DE designs individual tables &lt;em&gt;within&lt;/em&gt; a standard the architect owns. If you cannot articulate a modeling standard, you are not ready for the architect column.&lt;/li&gt;
&lt;li&gt;The two most-dropped responsibilities — cost envelope and cross-team consistency — are dropped precisely because they are pure-architect (no DE fallback owns them). A transition that leaves these unowned produces spiralling cloud bills and a fragmented platform within two quarters.&lt;/li&gt;
&lt;li&gt;The whole table is your answer to "what changes when you become an architect": you move from the R column to the A column, and you inherit responsibilities that nobody else in the org is positioned to hold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Botched-transition symptom&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;The architect responsibility that was dropped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud bill grows 40% YoY unexplained&lt;/td&gt;
&lt;td&gt;no budget owner&lt;/td&gt;
&lt;td&gt;cost envelope / FinOps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every team models "customer" differently&lt;/td&gt;
&lt;td&gt;no shared standard&lt;/td&gt;
&lt;td&gt;modeling standard + consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance audit fails on lineage&lt;/td&gt;
&lt;td&gt;no contract owner&lt;/td&gt;
&lt;td&gt;data contracts / classification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architect is the bottleneck on every PR&lt;/td&gt;
&lt;td&gt;stayed in the R column&lt;/td&gt;
&lt;td&gt;delegation of implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Draw the RACI split before you take the title. If you cannot name at least three responsibilities that move from &lt;em&gt;you do the work&lt;/em&gt; to &lt;em&gt;you own the outcome others do the work on&lt;/em&gt;, you are interviewing for a senior-DE role with an architect label, not for an architect role.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a skill-gap self-assessment rubric
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Before you can close the gap you have to measure it honestly. The self-assessment rubric scores you on each of the six pillars from 0 (no exposure) to 4 (can set the org standard), which turns "I want to be an architect" into a ranked list of what to build first. Most DEs discover they are a 3–4 on modeling and integration and a 0–1 on governance and cost — which is exactly the profile interviewers screen out.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The scale.&lt;/strong&gt; 0 none, 1 aware, 2 can apply under guidance, 3 can lead independently, 4 can set the org standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bar.&lt;/strong&gt; An architect hire is expected at 3+ across all six, with at least one 4. A 4 in modeling and a 1 in governance is a rejection, not a strength.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The output.&lt;/strong&gt; Your two lowest scores are your transition plan for the next two quarters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Score a typical mid-senior DE against the six-pillar rubric and derive the first two areas to invest in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;Typical mid-senior DE&lt;/th&gt;
&lt;th&gt;Architect bar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data modeling&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance &amp;amp; security&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration patterns&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost &amp;amp; performance&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud platforms&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Communication &amp;amp; stewardship&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Skill-gap self-assessment — score each pillar 0..4, rank the gaps
&lt;/span&gt;&lt;span class="n"&gt;PILLARS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_modeling&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;governance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;integration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost_performance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloud_platforms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;communication&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;ARCHITECT_BAR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transition_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ARCHITECT_BAR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return pillars below the architect bar, largest gap first.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;gaps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;   &lt;span class="c1"&gt;# biggest gap first, then alphabetical
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gaps&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pillar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;transition_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PILLARS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pillar&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; gap = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  -&amp;gt; invest now&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# governance          gap = 2  -&amp;gt; invest now
# cloud_platforms     gap = 1  -&amp;gt; invest now
# communication       gap = 1  -&amp;gt; invest now
# cost_performance    gap = 1  -&amp;gt; invest now
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The rubric forces a number on each pillar, which defeats the natural bias to over-rate the pillars you enjoy (modeling, integration) and ignore the ones you avoid (governance, cost). The number is the point.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;transition_plan&lt;/code&gt; filters to pillars below the bar and ranks by gap size. The largest gap — governance at 2 — is the first investment, because it is both the furthest from the bar and the pillar interviewers weight highest.&lt;/li&gt;
&lt;li&gt;Ties (cloud, communication, cost all at gap 1) are broken alphabetically here, but in practice you break ties by &lt;em&gt;interview frequency&lt;/em&gt;: cost and governance come up in almost every architect screen, so they jump the queue over the others.&lt;/li&gt;
&lt;li&gt;The plan is deliberately short — two to four items — because a transition that tries to level all six pillars at once levels none. Fix the biggest gap to a 3, re-score, then move to the next.&lt;/li&gt;
&lt;li&gt;Re-run the assessment every quarter. The transition is done when every pillar reads 3+ and you have earned at least one 4 through a real project you can talk about in an interview.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;th&gt;First concrete action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Governance &amp;amp; security&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Design a data-contract + PII-classification standard for one domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Cost &amp;amp; performance&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Build a cost model for one warehouse workload; propose tiering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Cloud platforms&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Earn one associate-level cloud data cert on your primary cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Communication&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Write three ADRs and one reference design for real decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Score all six pillars honestly, sort by gap, and fix the largest gap first — and remember that a single 4 with a 1 elsewhere reads as &lt;em&gt;specialist&lt;/em&gt;, not &lt;em&gt;architect&lt;/em&gt;. Breadth at 3 beats a spike at 4 with a hole beside it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the "why architect" positioning template
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Every architect screen opens with some version of "you're a strong data engineer — why architect?" The weak answer is "it's the next level up" (which frames it as a title grab). The strong answer names the specific decisions you already influence informally, the gap you have deliberately closed, and the scope you want to be accountable for. This is a 90-second scripted answer you rehearse once and deploy every time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1 — the pull.&lt;/strong&gt; The kind of problem you already gravitate to: the cross-team decision, the standard, the trade-off — not the hard pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2 — the proof.&lt;/strong&gt; A concrete decision you already drove that was architect-shaped (a modeling standard, a build-vs-buy call, a governance model).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3 — the gap closed.&lt;/strong&gt; The pillar you knew you were weak on and what you did about it — this pre-empts the "but you've never owned governance" objection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draft a 90-second "why architect" answer that a hiring manager reads as &lt;em&gt;ready&lt;/em&gt;, not &lt;em&gt;ambitious&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;Strong answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framing&lt;/td&gt;
&lt;td&gt;"it's the next promotion"&lt;/td&gt;
&lt;td&gt;"I already make architect-shaped decisions"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proof&lt;/td&gt;
&lt;td&gt;"I built hard pipelines"&lt;/td&gt;
&lt;td&gt;"I set our modeling standard across 3 teams"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gap awareness&lt;/td&gt;
&lt;td&gt;"I know everything already"&lt;/td&gt;
&lt;td&gt;"I was weak on governance; here's what I did"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope wanted&lt;/td&gt;
&lt;td&gt;"more responsibility"&lt;/td&gt;
&lt;td&gt;"accountability for the platform blueprint"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Why architect?" — 90-second answer template
============================================

Beat 1 — the pull (20s)
  "The parts of my current role I gravitate to aren't the pipelines —
   they're the decisions. Which modeling pattern the team standardises
   on, whether we buy or build ingestion, how we keep 'customer'
   consistent across three squads. I'm already doing that informally."

Beat 2 — the proof (30s)
  "Last year I wrote the star-schema standard our analytics teams now
   build against, and led the build-vs-buy call on CDC — we picked
   log-based over a vendor and I owned the trade-off doc that got it
   funded. That's architect work; I want the title to match the scope."

Beat 3 — the gap I closed (25s)
  "I knew governance was my weak pillar — I'd applied contracts but
   never designed the standard. So I built a PII-classification and
   data-contract model for our billing domain and got it through a
   security review. That closed the gap I'd have been screened on."

Beat 4 — the scope I want (15s)
  "What I want to be accountable for is the platform blueprint — the
   reference designs, the standards, the cost envelope — so twelve
   engineers can move fast without re-litigating the same decisions."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Beat 1 reframes the ambition from "next level" to "already doing the work" — the single most important move, because it turns the question from &lt;em&gt;are you ready?&lt;/em&gt; into &lt;em&gt;why isn't your title accurate yet?&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Beat 2 supplies proof in the architect vocabulary: a &lt;em&gt;standard&lt;/em&gt; (not a pipeline), a &lt;em&gt;build-vs-buy call&lt;/em&gt; (not a bug fix), a &lt;em&gt;trade-off doc that got funded&lt;/em&gt; (business outcome, not technical output). Every noun signals the lane change.&lt;/li&gt;
&lt;li&gt;Beat 3 is the counter-intuitive move: you name your weakest pillar out loud. This disarms the interviewer's biggest objection before they raise it, and it demonstrates the self-assessment discipline the role requires.&lt;/li&gt;
&lt;li&gt;Beat 4 states the scope you want in architect terms — accountability for the blueprint — and ties it to a business value ("twelve engineers move fast without re-litigating"). You are selling leverage, not seniority.&lt;/li&gt;
&lt;li&gt;The whole script is under two minutes and rehearsed to sound unrehearsed. It does the job of the entire first round: it proves you understand the role, have done the work, know your gaps, and want the right scope for the right reason.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Beat&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;What the interviewer concludes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pull&lt;/td&gt;
&lt;td&gt;reframe ambition as fit&lt;/td&gt;
&lt;td&gt;"the title is lagging the scope"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proof&lt;/td&gt;
&lt;td&gt;evidence in architect vocabulary&lt;/td&gt;
&lt;td&gt;"they've done real architect work"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gap closed&lt;/td&gt;
&lt;td&gt;disarm the objection&lt;/td&gt;
&lt;td&gt;"self-aware; already closed the weak pillar"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;sell leverage over seniority&lt;/td&gt;
&lt;td&gt;"wants accountability for the right reasons"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never answer "why architect?" with "it's the next level." Answer with the architect-shaped decisions you already drive, the weakest pillar you deliberately closed, and the blueprint-level scope you want to own — leverage over the org, not a rung on the ladder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the DE-to-architect transition
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You're clearly a strong data engineer. Convince me you're ready to be a data architect — not just senior — and tell me the one gap you'd close in your first ninety days and how."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a decisions-owned narrative plus a scoped 90-day governance closeout
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Answer structure — "ready for architect" in four moves
======================================================

Move 1 — decisions I already own (proof of lane change)
  "I already own the modeling standard for our analytics domain and I
   drove the log-based-vs-vendor CDC call. Both were decisions, not
   deliverables — that's the architect column of the RACI."

Move 2 — the honest gap (self-assessment)
  "My weakest pillar on the six-pillar ladder is governance. I score
   myself 3+ on modeling, integration, and cost, but only a 2 on
   governance because I've applied contracts, not designed the standard."

Move 3 — the scoped 90-day closeout (the plan)
  "First 90 days I'd close it with one real deliverable, not a course:
   a data-contract + PII-classification standard for one high-value
   domain, reviewed by security, adopted by one team. Scoped to one
   domain so it ships; templated so it generalises."

Move 4 — how I'd measure it (accountability)
  "Success = one domain with published contracts, a classification
   applied to every column, and a lineage view the auditor accepts.
   That converts my governance score from a 2 to a 3 with evidence."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# 90-Day Governance Closeout — Billing Domain (scoped)&lt;/span&gt;

&lt;span class="gu"&gt;## Goal&lt;/span&gt;
Move governance from "applied" to "standard-setting" with one shippable artifact.

&lt;span class="gu"&gt;## Weeks 1-3 — inventory &amp;amp; classify&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Catalog every table/column in the billing domain
&lt;span class="p"&gt;-&lt;/span&gt; Classify columns: public / internal / confidential / PII
&lt;span class="p"&gt;-&lt;/span&gt; Output: a classification matrix (column -&amp;gt; tier -&amp;gt; masking rule)

&lt;span class="gu"&gt;## Weeks 4-8 — data contracts&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Define a contract schema (owner, SLA, schema version, breaking-change policy)
&lt;span class="p"&gt;-&lt;/span&gt; Write contracts for the 5 highest-traffic billing tables
&lt;span class="p"&gt;-&lt;/span&gt; Wire a CI check that fails a PR on a breaking schema change

&lt;span class="gu"&gt;## Weeks 9-12 — lineage &amp;amp; sign-off&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Stand up column-level lineage for the domain (catalog tool or dbt exposures)
&lt;span class="p"&gt;-&lt;/span&gt; Security review + auditor walkthrough
&lt;span class="p"&gt;-&lt;/span&gt; Output: a governed domain other teams can copy as a template
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The CI data-contract check (Weeks 4-8): fail on a breaking change.&lt;/span&gt;
&lt;span class="c1"&gt;-- Compares the live schema against the committed contract snapshot.&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;      &lt;span class="c1"&gt;-- expected columns from the checked-in contract&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_nullable&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;contract_billing_orders_v3&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;               &lt;span class="c1"&gt;-- actual columns in the warehouse right now&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_nullable&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'billing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'orders'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'BREAKING: column removed or retyped'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;violation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;expected_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;actual_type&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;          &lt;span class="c1"&gt;-- column dropped&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt;  &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;   &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;   &lt;span class="c1"&gt;-- type changed&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NO'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'YES'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;-- nullability relaxed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;two owned decisions&lt;/td&gt;
&lt;td&gt;proves you're already in the A column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Move 2&lt;/td&gt;
&lt;td&gt;six-pillar self-score&lt;/td&gt;
&lt;td&gt;names the gap before the interviewer does&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;one scoped domain, 90 days&lt;/td&gt;
&lt;td&gt;shows delivery discipline, not a wish list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Move 4&lt;/td&gt;
&lt;td&gt;measurable success criteria&lt;/td&gt;
&lt;td&gt;frames governance as an accountable outcome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contract CI check&lt;/td&gt;
&lt;td&gt;schema vs contract&lt;/td&gt;
&lt;td&gt;governance made executable, not aspirational&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lineage sign-off&lt;/td&gt;
&lt;td&gt;auditor accepts&lt;/td&gt;
&lt;td&gt;evidence the gap is truly closed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After this answer, the interviewer has heard you name architect-shaped decisions you already own, self-assess honestly on the exact pillar they were about to probe, and propose a scoped, measurable 90-day closeout that produces a reusable template rather than a personal certificate. The SQL contract check turns "I'll improve governance" into "here is the mechanism that enforces it in CI" — the difference between an aspiration and a design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal probed&lt;/th&gt;
&lt;th&gt;Weak candidate&lt;/th&gt;
&lt;th&gt;This answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lane change understood&lt;/td&gt;
&lt;td&gt;"it's more senior"&lt;/td&gt;
&lt;td&gt;owns decisions, not just deliverables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-awareness&lt;/td&gt;
&lt;td&gt;"no real gaps"&lt;/td&gt;
&lt;td&gt;names governance as the weak pillar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery discipline&lt;/td&gt;
&lt;td&gt;"I'd take a course"&lt;/td&gt;
&lt;td&gt;scoped 90-day shippable standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance depth&lt;/td&gt;
&lt;td&gt;vague&lt;/td&gt;
&lt;td&gt;executable contract check + lineage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accountability&lt;/td&gt;
&lt;td&gt;none stated&lt;/td&gt;
&lt;td&gt;measurable success criteria&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Decisions over deliverables&lt;/strong&gt;&lt;/strong&gt; — the architect is accountable for outcomes others implement. Leading with two owned decisions (a modeling standard, a build-vs-buy call) proves you already live in the accountable column, which is the whole test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Honest self-assessment&lt;/strong&gt;&lt;/strong&gt; — naming governance as your weakest pillar before the interviewer probes it converts a potential rejection into a demonstration of the self-awareness the role requires. Hiding the gap is the losing move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Scoped closeout&lt;/strong&gt;&lt;/strong&gt; — one domain, 90 days, one shippable standard. Scope is the architect's discipline: a plan that boils the ocean signals you don't yet think in deliverable increments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Governance as executable&lt;/strong&gt;&lt;/strong&gt; — the contract CI check and column-level lineage turn governance from a slide into a mechanism. Architects are graded on whether the standard is enforceable, not whether it sounds good.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the transition itself is the cost: closing one pillar to a 3 is roughly one quarter of deliberate, project-based effort per pillar. Budget two-to-four quarters to move a strong DE across the boundary, front-loaded on governance and cost — the two pillars DEs are furthest from and interviewers weight highest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;System-design problems for aspiring architects&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;
&lt;strong&gt;SQL problems on schemas, contracts, and metadata&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. The architect skill ladder: modeling, governance, cost
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The five competency pillars — where DEs are strong, where they're screened out, and how each is scored
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqfcqasixblfwjufvtepb.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqfcqasixblfwjufvtepb.jpeg" alt="Iconographic architect skill-ladder diagram — five competency pillars (data modeling, governance, integration, cost/performance, cloud) drawn as rising columns, each with a novice-to-architect fill gauge." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the architect skill ladder is five competency pillars — data modeling, governance and security, integration patterns, cost and performance, and cloud platforms — and the hiring bar is not a spike in one but a 3-out-of-4 across all five, because an architect who is brilliant at modeling and blind to governance produces designs that ship a compliance incident&lt;/strong&gt;. Every senior DE is already climbing two or three of these rungs at work; the transition is about the ones they never touch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The five pillars, and what "architect level" means on each.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data modeling.&lt;/strong&gt; Not "can write a CREATE TABLE" — can choose between dimensional (star/snowflake), fully normalized (3NF), and Data Vault for a given workload, and defend the choice on query pattern, change rate, and auditability. Architect level = you set the modeling standard and the team's tables conform to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance and security.&lt;/strong&gt; Catalog, column-level lineage, access control, PII classification, data contracts, retention and residency. This is the pillar DEs score lowest on and interviewers weight highest — because a governance miss is the one architecture mistake that ends up in a regulator's inbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration patterns.&lt;/strong&gt; Batch, micro-batch, streaming, CDC, request/response API integration, event-driven — the menu of how systems exchange data, and the latency/cost/complexity trade-off of each. Architect level = you match the pattern to the requirement instead of defaulting to the one you know.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost and performance.&lt;/strong&gt; Partitioning, clustering, storage tiering, compute right-sizing, and the FinOps discipline of attaching a dollar figure to a design. Architect level = every reference design carries a cost model, not just a data-flow diagram.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud platforms.&lt;/strong&gt; Deep on one major cloud's data stack, literate on the other two. Architect level = you can reason about portability, lock-in, and the managed-vs-self-hosted trade-off, not just wire up one vendor's services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Data modeling is the pillar DEs underrate — the three canonical models.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dimensional (star schema).&lt;/strong&gt; Fact tables surrounded by denormalized dimension tables. Optimised for analytical read patterns — wide scans, aggregations, slice-and-dice BI. The default for a warehouse's gold layer. Fewer joins, faster reads, controlled redundancy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalized (3NF).&lt;/strong&gt; Every fact stored once, relationships expressed by foreign keys. Optimised for write-heavy operational (OLTP) systems and for a warehouse's staging/silver layer where integrity matters more than read speed. More joins, less redundancy, cleaner writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Vault.&lt;/strong&gt; Hubs (business keys), links (relationships), satellites (attributes over time). Optimised for auditability, source-system agility, and highly regulated environments — you can add a source without breaking the model. Verbose to query; typically feeds a dimensional presentation layer on top.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Governance is the pillar that gets you hired or rejected — the five sub-competencies.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cataloging &amp;amp; lineage.&lt;/strong&gt; Every dataset discoverable; column-level lineage answers "where did this number come from?" for the auditor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access control &amp;amp; classification.&lt;/strong&gt; Every column tagged (public / internal / confidential / PII) with a masking and row-access policy attached to the tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data contracts.&lt;/strong&gt; Producer-consumer agreements with schema, SLA, and a breaking-change policy enforced in CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention &amp;amp; residency.&lt;/strong&gt; How long data lives and which jurisdiction it lives in — GDPR/CCPA-shaped requirements that constrain the physical design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality &amp;amp; observability.&lt;/strong&gt; Freshness, completeness, and validity checks with owners and alerts — governance is not just access, it's trust.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the competency-pillar rubric
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The pillar rubric defines what novice, practitioner, and architect look like on each of the five pillars, so both you and an interviewer can place you precisely. It is the scoring key behind the self-assessment from Section 1.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Novice.&lt;/strong&gt; Aware of the concept; can apply an existing pattern under guidance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practitioner.&lt;/strong&gt; Applies the pattern independently on their own component.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architect.&lt;/strong&gt; Sets the standard the whole platform builds against and defends the trade-off to the business.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Fill in the rubric for the modeling and governance pillars, and state the observable behaviour that distinguishes a practitioner from an architect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;Practitioner behaviour&lt;/th&gt;
&lt;th&gt;Architect behaviour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data modeling&lt;/td&gt;
&lt;td&gt;designs a correct star schema for their mart&lt;/td&gt;
&lt;td&gt;sets the org's modeling standard; picks star vs 3NF vs Vault per workload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;applies the existing classification tags&lt;/td&gt;
&lt;td&gt;designs the classification + contract + lineage standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;builds the pipeline the design specifies&lt;/td&gt;
&lt;td&gt;chooses batch vs streaming vs CDC on trade-offs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;reports their pipeline's spend&lt;/td&gt;
&lt;td&gt;owns the cost model and tiering policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;uses the cloud services set up for them&lt;/td&gt;
&lt;td&gt;reasons about portability and lock-in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pillar rubric — the practitioner -&amp;gt; architect line
==================================================

Pillar         | Novice (1)        | Practitioner (2-3)      | Architect (4)
---------------+-------------------+-------------------------+---------------------------
Data modeling  | reads a star      | builds a correct mart   | SETS the modeling standard
Governance     | knows PII exists  | applies the tags        | DESIGNS the classification model
Integration    | knows batch       | builds one pattern well | CHOOSES the pattern on trade-offs
Cost           | sees the bill     | reports own spend       | OWNS the cost model &amp;amp; tiering
Cloud          | uses the console  | ships on one service    | REASONS about lock-in &amp;amp; portability

The line between practitioner and architect is always the same verb:
  practitioner APPLIES the standard;  architect SETS the standard.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The rubric's power is that the practitioner-to-architect jump is the same verb on every pillar: the practitioner &lt;em&gt;applies&lt;/em&gt; a standard someone else set; the architect &lt;em&gt;sets&lt;/em&gt; the standard. Once you see that pattern, you can self-place instantly.&lt;/li&gt;
&lt;li&gt;On modeling, a practitioner builds a correct star schema; the architect decides &lt;em&gt;when&lt;/em&gt; a star schema is wrong (write-heavy source → 3NF; regulated multi-source → Vault). The choice, not the construction, is the architect skill.&lt;/li&gt;
&lt;li&gt;On governance, the gap is starkest: applying tags is a practitioner task; designing the classification taxonomy, the contract schema, and the lineage model is the architect task. This is why DEs who "do governance" still screen as practitioners.&lt;/li&gt;
&lt;li&gt;On integration, the architect chooses among batch/streaming/CDC/event-driven based on the requirement; the practitioner builds whichever one was chosen for them. Defaulting to your favourite pattern is a practitioner tell.&lt;/li&gt;
&lt;li&gt;On cost and cloud, the jump is from reporting/using to owning/reasoning. An architect attaches a dollar figure and a lock-in analysis to every design; a practitioner ships and lets someone else worry about the bill.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;The one question that reveals architect level&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data modeling&lt;/td&gt;
&lt;td&gt;"When is a star schema the wrong choice?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;"Show me your classification taxonomy and contract schema."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;"Why batch here and streaming there?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;"What does this design cost per month, and why that tier?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;"What's your lock-in exposure and exit cost?"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Score yourself with the verb test: on each pillar, do you &lt;em&gt;apply&lt;/em&gt; the standard or &lt;em&gt;set&lt;/em&gt; it? You need "set" on modeling plus governance and "apply-to-set in progress" on the rest before you interview as an architect.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a data-modeling worked example (star vs 3NF)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Modeling is the pillar you can prove in code, so architect interviews always include a modeling exercise. The canonical one: take an operational (3NF) order schema and design the dimensional (star) model an analytics team should query, explaining every denormalization choice. Do it end to end.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The source (3NF).&lt;/strong&gt; Normalized OLTP: &lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;customers&lt;/code&gt;, &lt;code&gt;products&lt;/code&gt;, &lt;code&gt;order_items&lt;/code&gt; — each fact once, joined by keys, optimised for writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The target (star).&lt;/strong&gt; A &lt;code&gt;fact_sales&lt;/code&gt; grain of one row per order line, surrounded by conformed dimensions &lt;code&gt;dim_customer&lt;/code&gt;, &lt;code&gt;dim_product&lt;/code&gt;, &lt;code&gt;dim_date&lt;/code&gt; — optimised for analytical reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The judgement.&lt;/strong&gt; What to denormalize, what to keep as a surrogate key, and how to handle a changing dimension.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given a 3NF order schema, design the star schema for sales analytics, and justify the grain and the surrogate keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source table (3NF)&lt;/th&gt;
&lt;th&gt;Star role&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;orders&lt;/td&gt;
&lt;td&gt;fact (header)&lt;/td&gt;
&lt;td&gt;dates, status → fact + dim_date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;order_items&lt;/td&gt;
&lt;td&gt;fact (grain)&lt;/td&gt;
&lt;td&gt;one fact row per line item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;customers&lt;/td&gt;
&lt;td&gt;dim_customer&lt;/td&gt;
&lt;td&gt;denormalize region, segment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;products&lt;/td&gt;
&lt;td&gt;dim_product&lt;/td&gt;
&lt;td&gt;denormalize category, brand&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SOURCE: normalized 3NF (operational) — every fact once, write-optimised&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;regions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;segment_id&lt;/span&gt;  &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;    &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;order_ts&lt;/span&gt;    &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;    &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;qty&lt;/span&gt;         &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unit_price_cents&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- TARGET: dimensional star (analytics) — read-optimised, controlled redundancy&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dim_date&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;date_key&lt;/span&gt;     &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- surrogate: YYYYMMDD&lt;/span&gt;
    &lt;span class="n"&gt;full_date&lt;/span&gt;    &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quarter&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_of_week&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;customer_key&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- SURROGATE key (not the source id)&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;-- natural/business key retained&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;         &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;region&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;-- denormalized from regions&lt;/span&gt;
    &lt;span class="n"&gt;segment&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;                    &lt;span class="c1"&gt;-- denormalized from segments&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dim_product&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;product_key&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- surrogate key&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt;   &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;         &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;-- denormalized from categories&lt;/span&gt;
    &lt;span class="n"&gt;brand&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;fact_sales&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sales_key&lt;/span&gt;     &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;date_key&lt;/span&gt;      &lt;span class="nb"&gt;INT&lt;/span&gt;    &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;dim_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;customer_key&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;product_key&lt;/span&gt;   &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;dim_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;      &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c1"&gt;-- degenerate dimension&lt;/span&gt;
    &lt;span class="n"&gt;qty&lt;/span&gt;           &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;revenue_cents&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;                 &lt;span class="c1"&gt;-- additive measure = qty * unit_price&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The grain is chosen first and drives everything: one &lt;code&gt;fact_sales&lt;/code&gt; row per &lt;code&gt;order_item&lt;/code&gt; (line item). Grain is the single most important modeling decision — the wrong grain (per-order instead of per-line) makes product-level analysis impossible.&lt;/li&gt;
&lt;li&gt;Dimensions denormalize the 3NF lookups: &lt;code&gt;region&lt;/code&gt; and &lt;code&gt;segment&lt;/code&gt; collapse into &lt;code&gt;dim_customer&lt;/code&gt;; &lt;code&gt;category&lt;/code&gt; and &lt;code&gt;brand&lt;/code&gt; collapse into &lt;code&gt;dim_product&lt;/code&gt;. This trades controlled redundancy for join elimination — a BI query touches one fact and three small dimensions instead of eight normalized tables.&lt;/li&gt;
&lt;li&gt;Every dimension gets a &lt;em&gt;surrogate&lt;/em&gt; key (&lt;code&gt;customer_key&lt;/code&gt;) distinct from the &lt;em&gt;natural&lt;/em&gt; key (&lt;code&gt;customer_id&lt;/code&gt;). Surrogate keys decouple the warehouse from source-system key churn and — crucially — enable slowly-changing-dimension history (a customer who changes segment gets a new surrogate, preserving the old fact rows' link to the old segment).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;order_id&lt;/code&gt; rides along as a &lt;em&gt;degenerate dimension&lt;/em&gt; — a dimension attribute with no dimension table — so you can still group by order without a &lt;code&gt;dim_order&lt;/code&gt;. &lt;code&gt;revenue_cents&lt;/code&gt; is a fully additive measure (safe to SUM across any dimension); storing it precomputed avoids per-query multiplication.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dim_date&lt;/code&gt; is a conformed dimension: pre-built with a &lt;code&gt;YYYYMMDD&lt;/code&gt; integer surrogate, shared by every fact table in the warehouse. Conformed dimensions are what let "sales by month" and "returns by month" line up — the architect owns the conformed set.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query pattern&lt;/th&gt;
&lt;th&gt;On 3NF source&lt;/th&gt;
&lt;th&gt;On star schema&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Revenue by category by quarter&lt;/td&gt;
&lt;td&gt;6-table join&lt;/td&gt;
&lt;td&gt;1 fact + 2 dims&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top customers by segment&lt;/td&gt;
&lt;td&gt;4-table join&lt;/td&gt;
&lt;td&gt;1 fact + 1 dim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write a new order&lt;/td&gt;
&lt;td&gt;1 clean insert&lt;/td&gt;
&lt;td&gt;N/A (analytics only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Track customer segment change over time&lt;/td&gt;
&lt;td&gt;not modeled&lt;/td&gt;
&lt;td&gt;SCD Type 2 on dim_customer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Choose the grain before anything else, give every dimension a surrogate key, denormalize lookups into dimensions, and keep measures additive. State out loud that the 3NF source is &lt;em&gt;correct for writes&lt;/em&gt; and the star is &lt;em&gt;correct for reads&lt;/em&gt; — the architect signal is knowing both are right for their job.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — an SCD Type 2 dimension for change history
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The follow-up to any modeling question is "how do you track a dimension attribute that changes over time?" — the slowly-changing-dimension (SCD) problem. Type 2 (keep full history via new rows) is the architect default, and knowing the surrogate-key/effective-date mechanics cold is a strong modeling signal.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The scenario.&lt;/strong&gt; A customer moves from segment "SMB" to "Enterprise". Historical facts must still attribute to "SMB"; new facts to "Enterprise".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The mechanism.&lt;/strong&gt; On change, expire the current dim row (&lt;code&gt;valid_to&lt;/code&gt;, &lt;code&gt;is_current = false&lt;/code&gt;) and insert a new row with a new surrogate key and &lt;code&gt;is_current = true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The payoff.&lt;/strong&gt; Facts join to the surrogate key that was current &lt;em&gt;at fact time&lt;/em&gt;, so history is preserved automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement SCD Type 2 on &lt;code&gt;dim_customer&lt;/code&gt; and show the state after a segment change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;customer_key&lt;/td&gt;
&lt;td&gt;surrogate PK, new per version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;customer_id&lt;/td&gt;
&lt;td&gt;natural/business key, stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;valid_from / valid_to&lt;/td&gt;
&lt;td&gt;version validity window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is_current&lt;/td&gt;
&lt;td&gt;fast filter for the live row&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SCD Type 2 dimension: history via versioned rows&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;customer_key&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- surrogate, changes per version&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt;      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;-- natural key, stable across versions&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;         &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;region&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;segment&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;valid_from&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;valid_to&lt;/span&gt;     &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;-- NULL = still current&lt;/span&gt;
    &lt;span class="n"&gt;is_current&lt;/span&gt;   &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;     &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Apply a change: customer 7 moves SMB -&amp;gt; Enterprise&lt;/span&gt;
&lt;span class="c1"&gt;-- 1. Expire the current version&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;    &lt;span class="n"&gt;valid_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Insert the new version (new surrogate key)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Acme Corp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'EMEA'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Enterprise'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Facts inserted before the change keep pointing at the OLD customer_key;&lt;/span&gt;
&lt;span class="c1"&gt;-- facts after the change resolve to the NEW customer_key via is_current.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The natural key &lt;code&gt;customer_id = 7&lt;/code&gt; is stable; the surrogate &lt;code&gt;customer_key&lt;/code&gt; changes with every version. This split is what makes Type 2 work — facts reference the surrogate, so they are frozen to the version that was current when they occurred.&lt;/li&gt;
&lt;li&gt;Step 1 expires the old row by stamping &lt;code&gt;valid_to = now()&lt;/code&gt; and &lt;code&gt;is_current = false&lt;/code&gt;. The old row is never deleted — that is the whole point of Type 2, full history.&lt;/li&gt;
&lt;li&gt;Step 2 inserts a new row with a fresh surrogate key, the new segment, &lt;code&gt;valid_from = now()&lt;/code&gt;, and &lt;code&gt;is_current = true&lt;/code&gt;. There are now two rows for customer 7, distinguishable by their validity windows.&lt;/li&gt;
&lt;li&gt;A fact-loading job resolves &lt;code&gt;customer_id&lt;/code&gt; to &lt;code&gt;customer_key&lt;/code&gt; by joining on &lt;code&gt;customer_id AND is_current&lt;/code&gt; (for streaming loads) or on &lt;code&gt;customer_id AND fact_ts BETWEEN valid_from AND COALESCE(valid_to, 'infinity')&lt;/code&gt; (for backfills) — the latter attributes each historical fact to the correct version.&lt;/li&gt;
&lt;li&gt;Analytical queries "revenue by segment over time" now correctly show the customer's revenue under SMB before the change and Enterprise after — because the facts carry the surrogate that encodes the segment-at-the-time. Type 1 (overwrite) would have rewritten history; Type 2 preserves it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_key&lt;/th&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;segment&lt;/th&gt;
&lt;th&gt;valid_from&lt;/th&gt;
&lt;th&gt;valid_to&lt;/th&gt;
&lt;th&gt;is_current&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;SMB&lt;/td&gt;
&lt;td&gt;2024-01-01&lt;/td&gt;
&lt;td&gt;2026-08-04&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;245&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;2026-08-04&lt;/td&gt;
&lt;td&gt;(null)&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any dimension whose attribute changes and whose history matters, use SCD Type 2: stable natural key, versioned surrogate key, validity window, &lt;code&gt;is_current&lt;/code&gt; flag. Reach for Type 1 (overwrite) only when history genuinely does not matter — and say why out loud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on modeling a domain end-to-end
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Model a subscription-billing domain end to end. Give me the operational schema, the warehouse star schema for revenue analytics, how you'd handle a customer changing plans mid-cycle, and the one governance control you'd attach to the model."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a 3NF source, a conformed star, SCD Type 2 plans, and a PII-classification control
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. OPERATIONAL (3NF) — write-optimised source of truth&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;plans&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;plan_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monthly_price_cents&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;subscription_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;plan_id&lt;/span&gt;     &lt;span class="nb"&gt;INT&lt;/span&gt;    &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;plans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;started_at&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ended_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;invoice_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;subscription_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscription_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;period_start&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period_end&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount_cents&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 2. WAREHOUSE (star) — read-optimised revenue analytics&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dim_date&lt;/span&gt;     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_key&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;                     &lt;span class="c1"&gt;-- SCD Type 2 for plan/segment history&lt;/span&gt;
    &lt;span class="n"&gt;customer_key&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;plan_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_to&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;fact_revenue&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;revenue_key&lt;/span&gt;   &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;date_key&lt;/span&gt;      &lt;span class="nb"&gt;INT&lt;/span&gt;    &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;dim_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;customer_key&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;invoice_id&lt;/span&gt;    &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;-- degenerate dimension&lt;/span&gt;
    &lt;span class="n"&gt;mrr_cents&lt;/span&gt;     &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;-- monthly recurring revenue (additive)&lt;/span&gt;
    &lt;span class="n"&gt;is_new&lt;/span&gt;        &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_churn&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;       &lt;span class="c1"&gt;-- movement flags for MRR waterfall&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 3. Plan change mid-cycle = SCD Type 2 version bump on dim_customer,&lt;/span&gt;
&lt;span class="c1"&gt;--    plus proration handled in fact_revenue as two partial-period rows.&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;valid_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Enterprise'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Governance control: classify + mask PII (email) with a policy tied to a tag.&lt;/span&gt;
&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="s1"&gt;'classification=PII'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim_customer_masked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim_customer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- email deliberately excluded from the analytics view&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;3NF&lt;/td&gt;
&lt;td&gt;write integrity for the billing OLTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse&lt;/td&gt;
&lt;td&gt;star + conformed dim_date&lt;/td&gt;
&lt;td&gt;fast MRR analytics across time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grain&lt;/td&gt;
&lt;td&gt;one fact_revenue row per invoice period&lt;/td&gt;
&lt;td&gt;supports the MRR waterfall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan change&lt;/td&gt;
&lt;td&gt;SCD Type 2 on dim_customer&lt;/td&gt;
&lt;td&gt;preserves revenue-by-plan history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Movement flags&lt;/td&gt;
&lt;td&gt;is_new / is_churn&lt;/td&gt;
&lt;td&gt;enables new/expansion/churn breakdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;email tagged PII, excluded from analytics view&lt;/td&gt;
&lt;td&gt;least-privilege by construction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The design carries revenue at the invoice-period grain, attributes each period to the plan that was current when it was billed (via the SCD Type 2 surrogate), flags new and churned revenue for the MRR waterfall, and — critically — attaches a governance control at modeling time by classifying &lt;code&gt;email&lt;/code&gt; as PII and building the analytics dimension without it. The architect signal is that governance is &lt;em&gt;in the model&lt;/em&gt;, not bolted on afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Analytics question&lt;/th&gt;
&lt;th&gt;Answered by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MRR by month&lt;/td&gt;
&lt;td&gt;SUM(mrr_cents) over dim_date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue by plan over time&lt;/td&gt;
&lt;td&gt;fact_revenue × SCD Type 2 dim_customer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New vs churned MRR&lt;/td&gt;
&lt;td&gt;is_new / is_churn flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue by country&lt;/td&gt;
&lt;td&gt;dim_customer.country&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can analysts see customer email?&lt;/td&gt;
&lt;td&gt;no — excluded by the masked view&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Grain-first modeling&lt;/strong&gt;&lt;/strong&gt; — choosing "one fact per invoice period" before drawing anything makes the MRR waterfall and proration natural. The wrong grain would make plan-level revenue analysis impossible to reconstruct later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Conformed dimensions&lt;/strong&gt;&lt;/strong&gt; — a shared &lt;code&gt;dim_date&lt;/code&gt; (and a shared &lt;code&gt;dim_customer&lt;/code&gt;) lets every fact table line up on time and customer, which is what makes cross-domain analytics coherent. The architect owns the conformed set for the whole platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;SCD Type 2&lt;/strong&gt;&lt;/strong&gt; — versioned surrogate keys preserve revenue-by-plan history through a mid-cycle change. Overwriting (Type 1) would silently rewrite last quarter's numbers — a correctness bug that surfaces at board-reporting time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Governance in the model&lt;/strong&gt;&lt;/strong&gt; — tagging &lt;code&gt;email&lt;/code&gt; as PII and shaping the analytics view to exclude it is least-privilege by construction. Governance attached at modeling time is cheaper and safer than a masking policy retrofitted after an audit finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the star adds controlled storage redundancy (denormalized dimensions) in exchange for order-of-magnitude fewer joins per analytical query — O(1 fact + few dims) versus O(many normalized joins). The SCD Type 2 history grows the dimension slowly; partitioning &lt;code&gt;fact_revenue&lt;/code&gt; by &lt;code&gt;date_key&lt;/code&gt; keeps scan cost proportional to the queried window, not the table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Dimensional modeling&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — dimensional-modeling&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Star-schema and SCD dimensional-modeling problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/dimensional-modeling" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;
&lt;strong&gt;SQL problems on joins, grain, and aggregation&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Certifications that actually move the needle
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Which data architect certification pays off — mapped to the pillar it proves and the role it targets
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a data architect certification is a signal, not a skill — it opens a door and proves a floor, but it never proves the judgement the role is actually graded on, so the right strategy is to earn the one or two certs that map to your weakest hire-relevant pillar and your target role, then spend the rest of your effort on a portfolio that proves the judgement a cert cannot&lt;/strong&gt;. The mistake is collecting certs as trophies; the discipline is choosing the cert that closes a specific gap for a specific role.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvuhigcynnwzwrhk7fvc8.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvuhigcynnwzwrhk7fvc8.jpeg" alt="Iconographic data architect certification matrix — cert badges (cloud, enterprise-architecture, platform) mapped by rows to skill pillars and an ROI meter, with a cert-vs-portfolio balance scale." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The three certification families and what each signals.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud data certifications.&lt;/strong&gt; The major-cloud data/analytics/architect tracks (for example the AWS, Google Cloud, and Azure data-engineer and data/solutions-architect certifications). They prove you can wire a cloud data stack and reason about its managed services — the integration, cost, and cloud pillars. Highest ROI for platform/lakehouse architect roles at cloud-native companies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise-architecture certifications.&lt;/strong&gt; TOGAF and comparable enterprise-architecture frameworks. They prove you speak the language of business capability, application, and technology layers and can produce architecture artifacts a large enterprise expects. Highest ROI for enterprise-architect and data-architect roles in large, governance-heavy organisations (banks, insurers, healthcare, government).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform certifications.&lt;/strong&gt; Vendor tracks for the data platform itself (for example Snowflake, Databricks, and dbt certifications). They prove depth on a specific platform's modeling, performance, and governance features. Highest ROI when the target role names that platform in the job description.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to prioritise by target role.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud-lakehouse architect (startup / scale-up).&lt;/strong&gt; One cloud data/architect cert on the company's primary cloud + one platform cert (Databricks or Snowflake). Skip TOGAF — it reads as enterprise ceremony in a fast-moving shop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise data architect (large regulated org).&lt;/strong&gt; TOGAF (or equivalent) first — it is often a literal checkbox in the JD — plus one cloud architect cert for credibility. The enterprise-architecture framework is the differentiator here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform / data-platform architect.&lt;/strong&gt; The platform cert that matches the stack (Snowflake/Databricks/dbt) + one cloud cert. Depth on the named platform beats breadth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any role, weak on cost.&lt;/strong&gt; A cloud architect cert doubles as a cost-and-performance credential because the exams heavily test right-sizing and storage tiering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The cert-vs-portfolio trade-off — what each actually proves.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A cert proves a floor.&lt;/strong&gt; It says "this person knows the services and the vocabulary." It gets your résumé past the screen and gives a non-technical recruiter a checkbox. It does &lt;em&gt;not&lt;/em&gt; prove you can make a trade-off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A portfolio proves judgement.&lt;/strong&gt; Reference designs, ADRs, a governed data model, a cost model — these show you &lt;em&gt;choosing&lt;/em&gt;, which is the entire architect job. A portfolio survives the technical interview; a cert only survives the recruiter screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The verdict.&lt;/strong&gt; Certs are necessary-but-not-sufficient for many roles and pure-optional for others; a portfolio is close to universally decisive. Spend the minimum cert effort to clear the screen, then over-invest in the portfolio that wins the panel.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — a certification comparison matrix
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The comparison matrix maps each cert family to the pillar it proves, the role it targets, and a rough ROI, so you can pick deliberately instead of collecting. ROI here is signal-per-unit-effort for &lt;em&gt;your&lt;/em&gt; target role, not the cert's absolute prestige.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The columns.&lt;/strong&gt; Cert family → pillar proven → best-fit role → effort → ROI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The read.&lt;/strong&gt; Your highest-ROI cert is the one whose pillar matches your weakest hire-relevant gap &lt;em&gt;and&lt;/em&gt; whose role matches your target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trap.&lt;/strong&gt; A high-prestige cert with low ROI for your target role (e.g. TOGAF for a startup lakehouse role) is a waste of a quarter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the cert comparison matrix and pick the highest-ROI cert for two candidates: one targeting a scale-up lakehouse role, one targeting an enterprise bank.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cert family&lt;/th&gt;
&lt;th&gt;Pillar proven&lt;/th&gt;
&lt;th&gt;Best-fit role&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud data/architect&lt;/td&gt;
&lt;td&gt;integration, cost, cloud&lt;/td&gt;
&lt;td&gt;cloud-lakehouse architect&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise-architecture (TOGAF)&lt;/td&gt;
&lt;td&gt;governance, blueprint vocabulary&lt;/td&gt;
&lt;td&gt;enterprise data architect&lt;/td&gt;
&lt;td&gt;medium-high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform (Snowflake/Databricks/dbt)&lt;/td&gt;
&lt;td&gt;modeling, performance&lt;/td&gt;
&lt;td&gt;platform architect&lt;/td&gt;
&lt;td&gt;low-medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Certification comparison matrix  (ROI = signal per unit effort, by target role)
===============================================================================

Cert family            | Proves pillar          | Best-fit role          | Effort | ROI*
-----------------------+------------------------+------------------------+--------+-----
Cloud data / architect | integration, cost,cloud| cloud-lakehouse arch.  | med    | HIGH  (scale-up)
Enterprise-arch (TOGAF)| governance, blueprint  | enterprise data arch.  | med-hi | HIGH  (bank/gov)
Platform (Snowflake/   | modeling, performance  | platform architect     | low-med| HIGH  (named stack)
  Databricks/dbt)      |                        |                        |        |
Extra cloud cert #2    | portability literacy   | any                    | med    | LOW   (diminishing)
Second EA framework    | vocabulary (again)     | none new               | high   | LOW   (redundant)

* ROI is role-dependent. TOGAF is HIGH for a bank, LOW for a 40-person startup.

Candidate A (scale-up lakehouse):  Cloud data/architect + one platform cert. Skip TOGAF.
Candidate B (enterprise bank):     TOGAF + one cloud architect cert. Platform cert optional.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The matrix's key column is ROI, and ROI is explicitly role-dependent — the same TOGAF cert is HIGH for a bank and LOW for a startup. Reading the cert in isolation of the target role is the classic mistake.&lt;/li&gt;
&lt;li&gt;Candidate A (scale-up) picks the cloud data/architect cert (proves integration + cost + cloud, the pillars a lakehouse role tests) plus a platform cert on the stack the company runs. TOGAF is skipped because enterprise ceremony is a negative signal in a fast shop.&lt;/li&gt;
&lt;li&gt;Candidate B (bank) inverts it: TOGAF first because it is frequently a literal JD requirement and proves the blueprint vocabulary a governance-heavy org expects, plus one cloud architect cert for technical credibility.&lt;/li&gt;
&lt;li&gt;The two LOW-ROI rows are the traps: a second cloud cert (diminishing returns once you can reason about one cloud) and a second EA framework (redundant vocabulary). Both consume a quarter and add no new signal.&lt;/li&gt;
&lt;li&gt;The whole matrix reduces to a two-factor pick: match the pillar to your weakest hire-relevant gap, match the role to your target. Everything else is trophy-hunting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Highest-ROI cert(s)&lt;/th&gt;
&lt;th&gt;What to skip&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scale-up lakehouse&lt;/td&gt;
&lt;td&gt;cloud data/architect + platform&lt;/td&gt;
&lt;td&gt;TOGAF&lt;/td&gt;
&lt;td&gt;ceremony reads negative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise bank&lt;/td&gt;
&lt;td&gt;TOGAF + cloud architect&lt;/td&gt;
&lt;td&gt;second platform cert&lt;/td&gt;
&lt;td&gt;JD checkbox + credibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weak-on-cost DE&lt;/td&gt;
&lt;td&gt;cloud architect&lt;/td&gt;
&lt;td&gt;platform-only cert&lt;/td&gt;
&lt;td&gt;exam tests right-sizing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strong generalist&lt;/td&gt;
&lt;td&gt;one cert, then portfolio&lt;/td&gt;
&lt;td&gt;any second cert&lt;/td&gt;
&lt;td&gt;diminishing returns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Pick certs by two factors only — the pillar they close and the role they target — and cap yourself at two before pivoting to portfolio. A third cert almost always has lower ROI than the first ADR or reference design you could have written instead.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a 6-month cert + portfolio plan
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The highest-leverage plan interleaves one cert with portfolio artifacts so the cert's study reinforces a real deliverable. Six months, roughly one artifact a month, one cert in the middle — the cert proves the floor, the artifacts prove the judgement.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The structure.&lt;/strong&gt; Months 1–2 close the weakest pillar with a real project; month 3 earn the cert that certifies it; months 4–6 build the portfolio that proves judgement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The principle.&lt;/strong&gt; Never study a cert in the abstract — pair every study block with an artifact so the knowledge lands in something you can show.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the six-month plan for a DE weak on governance and cost, targeting a cloud-lakehouse architect role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Month&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1–2&lt;/td&gt;
&lt;td&gt;governance + cost&lt;/td&gt;
&lt;td&gt;data-contract standard + cost model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;cloud architect cert&lt;/td&gt;
&lt;td&gt;the certification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4–5&lt;/td&gt;
&lt;td&gt;reference designs&lt;/td&gt;
&lt;td&gt;governed lakehouse + streaming design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;ADRs + presentation&lt;/td&gt;
&lt;td&gt;ADR set + a design talk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# 6-Month DE -&amp;gt; Data Architect Plan (cloud-lakehouse target)&lt;/span&gt;

&lt;span class="gu"&gt;## Month 1-2 — close the weakest pillars (governance + cost)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Data-contract + PII-classification standard for one domain (shippable)
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Cost model for one warehouse workload: storage tiering + compute right-sizing
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Wire a CI contract check that fails PRs on breaking schema changes
  OUTPUT: two portfolio artifacts + a measurable governance/cost win

&lt;span class="gu"&gt;## Month 3 — the cert (proves the floor)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Cloud data/architect certification on the company's primary cloud
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Pair each study module with a note mapping the service to a design decision
  OUTPUT: the cert + a services-to-decisions cheat sheet

&lt;span class="gu"&gt;## Month 4-5 — reference designs (prove judgement)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Governed lakehouse reference design (bronze/silver/gold + governance overlay)
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Streaming vs batch trade-off design for one real use case
  OUTPUT: two reference designs with NFRs and cost envelopes

&lt;span class="gu"&gt;## Month 6 — communication (prove stewardship)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] 5 ADRs documenting real decisions (with alternatives + trade-offs)
&lt;span class="p"&gt;-&lt;/span&gt; [ ] One internal design talk / RFC walkthrough
  OUTPUT: an ADR set + evidence you can lead a design conversation

&lt;span class="gu"&gt;## Success criteria&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Six-pillar self-score now 3+ across the board, with a 4 in modeling or governance
&lt;span class="p"&gt;-&lt;/span&gt; A portfolio you can screen-share in the panel: contracts, cost model, 2 designs, ADRs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Months 1–2 attack the weakest pillars first (governance and cost) with &lt;em&gt;shippable&lt;/em&gt; artifacts, not courses — a contract standard and a cost model that also become portfolio pieces. This front-loads the gap that gets DEs rejected.&lt;/li&gt;
&lt;li&gt;Month 3 places the cert &lt;em&gt;after&lt;/em&gt; the hands-on work, so the exam reinforces knowledge you already applied. Pairing each study module with a "service → decision" note converts rote memorisation into architect reasoning.&lt;/li&gt;
&lt;li&gt;Months 4–5 build the artifacts that actually win the panel: two reference designs, each carrying NFRs and a cost envelope. These prove the judgement no cert can certify.&lt;/li&gt;
&lt;li&gt;Month 6 closes on communication — five ADRs and a design talk — because stewardship is the sixth pillar and the one that makes the other five visible to the org.&lt;/li&gt;
&lt;li&gt;The success criteria are explicit and measurable: a 3+ across all six pillars, one 4, and a screen-shareable portfolio. The plan is done when you can &lt;em&gt;show&lt;/em&gt; the judgement, not just claim it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Month&lt;/th&gt;
&lt;th&gt;Deliverable&lt;/th&gt;
&lt;th&gt;Pillar advanced&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1–2&lt;/td&gt;
&lt;td&gt;contract standard + cost model&lt;/td&gt;
&lt;td&gt;governance, cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;cloud architect cert&lt;/td&gt;
&lt;td&gt;cloud, integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4–5&lt;/td&gt;
&lt;td&gt;two reference designs&lt;/td&gt;
&lt;td&gt;integration, cost, modeling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;ADR set + design talk&lt;/td&gt;
&lt;td&gt;communication&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Interleave the cert between portfolio artifacts, never in isolation — study months bracketed by shipping months. The cert clears the recruiter screen; the artifacts win the panel, and the panel is where the offer is decided.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — an ROI scoring rubric for the next cert
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; When you are tempted by a third or fourth cert, run it through an ROI rubric that scores it on gap-closed, role-fit, effort, and shelf-life. A cert that scores low on any dimension is a trophy, and trophies do not win panels.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The dimensions.&lt;/strong&gt; Gap closed (does it fix a weak pillar?), role fit (does the JD want it?), effort (weeks), shelf-life (how long the signal lasts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The threshold.&lt;/strong&gt; Only pursue a cert scoring high on gap-closed &lt;em&gt;and&lt;/em&gt; role-fit. Prestige alone is not on the rubric.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Score three candidate certs for a DE who already holds one cloud cert, and decide which (if any) to pursue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate cert&lt;/th&gt;
&lt;th&gt;Gap closed?&lt;/th&gt;
&lt;th&gt;Role fit?&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Second cloud cert&lt;/td&gt;
&lt;td&gt;no (already literate)&lt;/td&gt;
&lt;td&gt;weak&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform cert (target stack)&lt;/td&gt;
&lt;td&gt;yes (modeling depth)&lt;/td&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;td&gt;low-med&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TOGAF&lt;/td&gt;
&lt;td&gt;partial (governance vocab)&lt;/td&gt;
&lt;td&gt;depends on target&lt;/td&gt;
&lt;td&gt;med-high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ROI rubric for the next cert. Pursue only if roi &amp;gt;= threshold.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cert_roi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap_closed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role_fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effort_weeks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shelf_life_yrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# gap_closed, role_fit on 0..3; higher is better. Effort in the denominator.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;gap_closed&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;role_fit&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;shelf_life_yrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;effort_weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;second_cloud_cert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap_closed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role_fit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effort_weeks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;shelf_life_yrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform_cert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap_closed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role_fit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effort_weeks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;shelf_life_yrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;togaf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap_closed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role_fit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effort_weeks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shelf_life_yrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;roi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cert_roi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PURSUE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;roi&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gap_closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SKIP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; roi=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;roi&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# second_cloud_cert  roi=0.5   -&amp;gt; SKIP
# platform_cert      roi=2.25  -&amp;gt; PURSUE
# togaf              roi=0.8   -&amp;gt; SKIP  (unless the target JD demands it)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The rubric puts effort in the denominator, so a cheap cert that closes a real gap beats an expensive prestige cert. This is the correct optimisation for a transition on a clock.&lt;/li&gt;
&lt;li&gt;The second cloud cert scores gap-closed = 0 (you are already cloud-literate) and thus fails the &lt;code&gt;gap_closed &amp;gt;= 2&lt;/code&gt; gate regardless of ROI. Redundant certs are the most common wasted quarter.&lt;/li&gt;
&lt;li&gt;The platform cert scores highest: it closes a real modeling/performance gap on the target stack, matches the role strongly, and is low effort. Clear PURSUE.&lt;/li&gt;
&lt;li&gt;TOGAF is the nuanced case: it scores PURSUE-worthy &lt;em&gt;only if&lt;/em&gt; the target JD demands it (role_fit jumps to 3 for a bank). The rubric correctly makes it target-dependent rather than universally good or bad.&lt;/li&gt;
&lt;li&gt;The gate &lt;code&gt;gap_closed &amp;gt;= 2&lt;/code&gt; is deliberate: no cert is worth pursuing if it does not close a genuine pillar gap, no matter how prestigious. Prestige is not a pillar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cert&lt;/th&gt;
&lt;th&gt;ROI&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Second cloud cert&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;SKIP&lt;/td&gt;
&lt;td&gt;closes no gap; redundant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform cert&lt;/td&gt;
&lt;td&gt;2.25&lt;/td&gt;
&lt;td&gt;PURSUE&lt;/td&gt;
&lt;td&gt;closes modeling gap; strong role fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TOGAF&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;td&gt;SKIP unless JD demands&lt;/td&gt;
&lt;td&gt;target-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Run every candidate cert through the ROI rubric with a hard gate on gap-closed — if a cert does not fix a weak, hire-relevant pillar, skip it however prestigious it is. Two well-chosen certs plus a portfolio beats five trophies every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on certifications for architects
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Do certifications actually matter for a data architect, or is it all portfolio? Walk me through how you'd decide which certs to invest in, what a cert proves versus what it doesn't, and how you'd spend a fixed six months if you were making the jump."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a signal-vs-judgement framing plus a role-targeted, ROI-gated plan
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Answer structure — "do certs matter?" in four moves
===================================================

Move 1 — what a cert IS (a signal, a floor)
  "A cert proves a floor and clears the recruiter screen. It says I know
   the services and the vocabulary. It does not prove I can make a
   trade-off, which is the actual architect job."

Move 2 — what a portfolio IS (judgement, decisive)
  "A portfolio proves judgement: reference designs, ADRs, a governed
   model, a cost model. That survives the technical panel. So I treat
   certs as necessary-not-sufficient and portfolio as decisive."

Move 3 — how I'd pick (two factors + a gate)
  "I pick certs by two factors: the pillar they close and the role they
   target, with a hard gate that they must fix a weak, hire-relevant
   pillar. A second cloud cert fails the gate; a platform cert on the
   target stack passes it."

Move 4 — the fixed six months (interleaved)
  "I'd interleave: months 1-2 close governance and cost with shippable
   artifacts, month 3 earn the one cloud/architect cert, months 4-6
   build two reference designs and an ADR set. Cert clears the screen;
   portfolio wins the panel."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The decision rule I'd state out loud, made concrete.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pick_certs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weak_pillars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;picks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloud&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;weak_pillars&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;weak_pillars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloud data/architect cert on the primary cloud&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target_role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enterprise&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;governance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;weak_pillars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TOGAF / enterprise-architecture cert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;modeling&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;weak_pillars&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;target_role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform cert on the named stack (Snowflake/Databricks/dbt)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# cap at two; the rest of the effort is portfolio
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_certs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scale-up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;governance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloud&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="c1"&gt;# -&amp;gt; ['cloud data/architect cert on the primary cloud']  (+ portfolio for governance)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_certs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enterprise&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;governance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="c1"&gt;# -&amp;gt; ['cloud data/architect cert on the primary cloud', 'TOGAF / enterprise-architecture cert']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Evidence it signals&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;cert = floor/signal&lt;/td&gt;
&lt;td&gt;you understand what a cert can't prove&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;portfolio = judgement&lt;/td&gt;
&lt;td&gt;you know what the panel actually tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;pick by pillar + role, gated&lt;/td&gt;
&lt;td&gt;disciplined, not trophy-hunting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;interleave over six months&lt;/td&gt;
&lt;td&gt;delivery-oriented, measurable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pick_certs()&lt;/td&gt;
&lt;td&gt;cap at two&lt;/td&gt;
&lt;td&gt;you stop before diminishing returns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The answer refuses the false binary ("certs vs portfolio") and instead sequences them: certs clear the screen, the portfolio wins the panel, and the picking rule is a disciplined two-factor gate rather than trophy accumulation. The &lt;code&gt;pick_certs&lt;/code&gt; function makes the reasoning executable — the same target role and weak-pillar set always yield the same defensible pick, capped at two so the remaining effort flows to the portfolio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question probed&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;This answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Do certs matter?&lt;/td&gt;
&lt;td&gt;"yes, get all of them" / "no, useless"&lt;/td&gt;
&lt;td&gt;"signal yes, judgement no"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How to pick&lt;/td&gt;
&lt;td&gt;"the prestigious ones"&lt;/td&gt;
&lt;td&gt;pillar + role, gated on gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cert vs portfolio&lt;/td&gt;
&lt;td&gt;"certs" or "portfolio"&lt;/td&gt;
&lt;td&gt;sequence both; portfolio decisive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Six-month spend&lt;/td&gt;
&lt;td&gt;"study for exams"&lt;/td&gt;
&lt;td&gt;interleave cert + shippable artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Signal vs judgement&lt;/strong&gt;&lt;/strong&gt; — the core reframe. A cert is a signal (floor, screen-clearing); judgement is what the panel grades. Stating the distinction proves you understand what each instrument can and cannot do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Two-factor pick with a gate&lt;/strong&gt;&lt;/strong&gt; — choosing by pillar-closed and role-fit, gated on fixing a weak hire-relevant pillar, is the discipline that separates a strategist from a collector. The gate is what kills the redundant second cloud cert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Interleaving&lt;/strong&gt;&lt;/strong&gt; — pairing cert study with shippable artifacts means the knowledge lands in something demonstrable, and the timeline produces both the screen-clearing floor and the panel-winning portfolio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cap at two&lt;/strong&gt;&lt;/strong&gt; — diminishing returns are real; the third cert almost always has lower ROI than the first reference design. Naming the cap signals you optimise effort, an architect trait.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — a cert is roughly 4–10 focused weeks; a portfolio artifact is 2–4 weeks each. The optimal allocation for a six-month transition is one cert (~one month of the six) and four-to-five artifacts (the rest) — because the panel outcome correlates with judgement evidence, not certificate count.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems that build a real portfolio&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems for pipeline and integration depth&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Architecture interviews: blueprints &amp;amp; trade-offs
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The architect design interview — scope, NFRs, a reference design you can defend, and the trade-offs behind it
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the data architect interview is not "build the pipeline" — it is "scope the problem, attach non-functional requirements with real numbers, draw a reference design, and defend it on cost, latency, governance, and reliability trade-offs" — and the candidates who score highest lead with governance and cost, not with the cleverest data flow&lt;/strong&gt;. The DE design interview asks &lt;em&gt;can you build it?&lt;/em&gt;; the architect design interview asks &lt;em&gt;can you decide it, defend it, and would twelve engineers succeed building against your blueprint?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmpp1nenwsrl1tqllbu3.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmpp1nenwsrl1tqllbu3.jpeg" alt="Iconographic architecture-interview diagram — a whiteboard blueprint of a governed lakehouse (ingest, bronze/silver/gold, governance overlay) with a trade-off matrix comparing cost, latency and governance." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The five-phase structure of every architect design interview.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1 — scope.&lt;/strong&gt; Clarify the problem before drawing anything: who consumes this, what's the data volume, what's the freshness requirement, what compliance regime applies. Weak candidates start drawing; strong candidates start asking. "What's the SLA?" and "Is any of this PII?" are the first two questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2 — NFRs.&lt;/strong&gt; Attach numbers: cost budget, latency target (p50/p99), availability (nines), governance requirements (retention, residency, access), scale (rows/day, growth rate). NFRs are what turn a doodle into an architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 3 — reference design.&lt;/strong&gt; Draw the blueprint: sources → ingestion → storage layers → transformation → serving → consumers, with a governance overlay across the whole thing. Name the pattern (lakehouse, streaming, batch, medallion) and the concrete services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 4 — trade-offs.&lt;/strong&gt; For every major choice, name the alternative and why you rejected it: batch vs streaming, managed vs self-hosted, one big warehouse vs domain marts. The trade-off narrative &lt;em&gt;is&lt;/em&gt; the interview.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 5 — risks &amp;amp; evolution.&lt;/strong&gt; What breaks at 10× scale, what the migration path is, what the failure modes are, how you'd roll it out. Architects think in year-three terms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The reference-architecture menu — the four you must be able to draw cold.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch warehouse (medallion / lakehouse).&lt;/strong&gt; Sources → batch ingest → bronze (raw) → silver (cleaned/conformed) → gold (dimensional marts) → BI. The default for analytics; cheap, simple, minutes-to-hours freshness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming.&lt;/strong&gt; Sources → event bus (Kafka/Kinesis/PubSub) → stream processor (Flink/Spark Structured Streaming) → serving store + lakehouse. For sub-minute freshness; more expensive and operationally heavier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda / Kappa hybrid.&lt;/strong&gt; A speed layer (streaming) plus a batch layer (reprocessing) — or Kappa's single streaming path that replays for reprocessing. For use cases that need both real-time and correct-eventually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governed enterprise lakehouse.&lt;/strong&gt; Any of the above with a mandatory governance overlay: catalog, column-level lineage, tag-based access control, data contracts between domains. The enterprise default; governance is a first-class layer, not an afterthought.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The NFR numbers to attach — memorise these ranges.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency.&lt;/strong&gt; Batch = minutes to hours; micro-batch = seconds to minutes; streaming = sub-second to seconds. State p50 and p99, not just "fast."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Storage tiering (hot/warm/cold), compute right-sizing, and the biggest lever — scan reduction via partitioning and clustering. Always attach a rough monthly figure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability.&lt;/strong&gt; Three nines (99.9% ≈ 8.8h/yr down) is typical for analytics; four nines for serving. Do not over-engineer analytics to five nines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance.&lt;/strong&gt; Retention (e.g. 7 years for financial), residency (data stays in-region), access (least-privilege, tag-based), lineage (column-level, auditor-ready).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — a reference-architecture trade-off matrix
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful interview artifact is a trade-off matrix comparing the reference architectures on the NFR axes. It proves you can &lt;em&gt;choose&lt;/em&gt; rather than default, which is the architect skill under test.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The axes.&lt;/strong&gt; Latency, cost, operational complexity, governance fit, best use case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The move.&lt;/strong&gt; When asked to design, put this matrix on the whiteboard, then justify which row you pick for the stated requirement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The signal.&lt;/strong&gt; Naming the axis you optimise for (and the one you sacrifice) is the trade-off fluency interviewers grade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the reference-architecture trade-off matrix and pick the right architecture for two requirements: hourly executive dashboards, and real-time fraud scoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Ops complexity&lt;/th&gt;
&lt;th&gt;Governance fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Batch lakehouse&lt;/td&gt;
&lt;td&gt;minutes–hours&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;high (easy to govern)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;sub-second–seconds&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;medium (harder lineage)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda/Kappa hybrid&lt;/td&gt;
&lt;td&gt;mixed&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;very high&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governed enterprise lakehouse&lt;/td&gt;
&lt;td&gt;minutes–hours&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;very high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reference-architecture trade-off matrix
=======================================

Architecture             | Latency          | Cost | Ops  | Governance | Best for
-------------------------+------------------+------+------+------------+---------------------------
Batch lakehouse (medall.)| minutes-hours    | low  | low  | high       | analytics, BI, reporting
Streaming                | sub-second-secs  | high | high | medium     | real-time scoring/alerts
Lambda / Kappa hybrid    | mixed            | high | v.hi | medium     | real-time + reprocessing
Governed enterprise LH   | minutes-hours    | med  | med  | VERY HIGH  | regulated, multi-BU

Requirement A: hourly exec dashboards
  -&amp;gt; Batch lakehouse. Latency (hourly) is met by batch; optimise cost + governance,
     sacrifice real-time you don't need.

Requirement B: real-time fraud scoring
  -&amp;gt; Streaming. Latency (sub-second) is the hard NFR; accept high cost + ops,
     add a governance overlay for the PII in transaction data.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The matrix makes the choice mechanical: read the requirement's hardest NFR, find the row that meets it at lowest cost/complexity, and name what you sacrifice. That sentence — "I optimise X and sacrifice Y" — is the trade-off fluency signal.&lt;/li&gt;
&lt;li&gt;Requirement A (hourly dashboards) has a soft latency NFR (hourly), so streaming is over-engineering. Batch lakehouse meets the latency for a fraction of the cost and is the easiest to govern — the right call is the &lt;em&gt;cheaper&lt;/em&gt; one, and saying so is senior.&lt;/li&gt;
&lt;li&gt;Requirement B (fraud scoring) has a hard latency NFR (sub-second), which eliminates batch entirely. Streaming is mandatory; the architect accepts its high cost and ops burden because the requirement demands it, then adds governance for the PII.&lt;/li&gt;
&lt;li&gt;The Lambda/Kappa row is deliberately rarely picked — it is the highest-complexity option and only justified when you genuinely need both real-time serving and periodic reprocessing. Reaching for it by default is an over-engineering tell.&lt;/li&gt;
&lt;li&gt;The governed enterprise lakehouse row wins whenever the context is regulated or multi-business-unit, because governance jumps from a nice-to-have to the hardest NFR. Recognising when governance is the binding constraint is the top architect signal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Chosen architecture&lt;/th&gt;
&lt;th&gt;Optimised for&lt;/th&gt;
&lt;th&gt;Sacrificed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hourly exec dashboards&lt;/td&gt;
&lt;td&gt;batch lakehouse&lt;/td&gt;
&lt;td&gt;cost, governance&lt;/td&gt;
&lt;td&gt;real-time (not needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time fraud scoring&lt;/td&gt;
&lt;td&gt;streaming&lt;/td&gt;
&lt;td&gt;latency&lt;/td&gt;
&lt;td&gt;cost, ops simplicity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulated multi-BU platform&lt;/td&gt;
&lt;td&gt;governed enterprise lakehouse&lt;/td&gt;
&lt;td&gt;governance&lt;/td&gt;
&lt;td&gt;some latency/cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time + reprocessing&lt;/td&gt;
&lt;td&gt;Lambda/Kappa&lt;/td&gt;
&lt;td&gt;both&lt;/td&gt;
&lt;td&gt;operational simplicity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Put the trade-off matrix on the whiteboard, read the requirement's hardest NFR, pick the cheapest row that meets it, and say out loud what you optimise and what you sacrifice. Choosing the &lt;em&gt;cheaper&lt;/em&gt; architecture when the requirement allows it is a stronger signal than reaching for the fanciest one.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a whiteboard governed-lakehouse walkthrough
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common architect prompt is "design a governed lakehouse." The winning approach is a repeatable skeleton you walk left-to-right: sources → ingest → medallion layers → serving → consumers, with a governance overlay across all of it, narrating NFRs and trade-offs as you go.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The skeleton.&lt;/strong&gt; Five columns (sources, ingest, storage/medallion, transform/serve, consumers) plus a governance band on top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The narration.&lt;/strong&gt; At each column, state the choice, the alternative, and the NFR it satisfies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The finish.&lt;/strong&gt; End on governance and cost — the two pillars weakest candidates forget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Walk the governed-lakehouse skeleton for a mid-size company's analytics platform, naming the pattern and one trade-off per layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Alternative rejected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ingest&lt;/td&gt;
&lt;td&gt;CDC + batch files&lt;/td&gt;
&lt;td&gt;full-refresh nightly (too slow, no deletes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;object store + open table format&lt;/td&gt;
&lt;td&gt;proprietary warehouse only (lock-in)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medallion&lt;/td&gt;
&lt;td&gt;bronze/silver/gold&lt;/td&gt;
&lt;td&gt;one flat layer (no reprocessing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serve&lt;/td&gt;
&lt;td&gt;warehouse + BI + feature store&lt;/td&gt;
&lt;td&gt;BI only (blocks ML)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;catalog + lineage + tag access&lt;/td&gt;
&lt;td&gt;ad-hoc grants (audit fails)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Governed lakehouse — whiteboard skeleton (left -&amp;gt; right)
=======================================================

[ SOURCES ]      [ INGEST ]        [ MEDALLION STORAGE ]         [ SERVE ]         [ CONSUMERS ]
 OLTP DBs   --&amp;gt;   CDC (log-based)   bronze (raw, immutable)  --&amp;gt;  warehouse   --&amp;gt;   BI / dashboards
 SaaS APIs  --&amp;gt;   API pulls    --&amp;gt;  silver (clean, conformed)--&amp;gt;  (gold marts)      ML / feature store
 events     --&amp;gt;   stream (Kafka)    gold (dimensional marts) --&amp;gt;  serving DB        data science

   ======================  GOVERNANCE OVERLAY (spans all layers)  ======================
   catalog + discovery | column-level lineage | tag-based access (PII/confidential) | data contracts | retention

NFRs stated per layer:
  ingest:  freshness -&amp;gt; CDC for sub-minute source sync; batch for slow SaaS
  storage: cost -&amp;gt; object store + open table format (Iceberg/Delta) avoids lock-in
  serve:   latency -&amp;gt; gold marts pre-aggregated for BI p99 &amp;lt; 2s
  govern:  audit -&amp;gt; column lineage + 7-yr retention + residency in-region
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start at sources and move strictly left-to-right so the interviewer can follow — never jump around the diagram. Name the ingest pattern per source: CDC (log-based) for OLTP freshness, API pulls for SaaS, a stream for events. Stating &lt;em&gt;why&lt;/em&gt; CDC over nightly full-refresh (deletes + freshness) is the trade-off signal.&lt;/li&gt;
&lt;li&gt;Storage uses an object store plus an open table format (Iceberg/Delta) rather than a proprietary warehouse-only design — the trade-off is a little more setup for dramatically less vendor lock-in, an architect-level cost/portability call.&lt;/li&gt;
&lt;li&gt;The medallion layers (bronze raw/immutable, silver cleaned/conformed, gold dimensional marts) exist so you can reprocess from bronze when logic changes — the alternative (one flat layer) makes reprocessing impossible. This is where the modeling pillar meets the architecture pillar.&lt;/li&gt;
&lt;li&gt;Serving splits into a warehouse for BI and a feature store for ML, because BI-only serving blocks the data-science consumers — an architect designs for all consumers, not just dashboards.&lt;/li&gt;
&lt;li&gt;The governance overlay is drawn &lt;em&gt;last but emphasised most&lt;/em&gt;: catalog, column-level lineage, tag-based access for PII/confidential, data contracts between domains, and retention/residency. Ending on governance and cost is the deliberate move that separates the architect answer from the DE answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;NFR satisfied&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ingest&lt;/td&gt;
&lt;td&gt;CDC + batch + stream&lt;/td&gt;
&lt;td&gt;freshness per source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;object store + open table format&lt;/td&gt;
&lt;td&gt;cost, no lock-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medallion&lt;/td&gt;
&lt;td&gt;bronze/silver/gold&lt;/td&gt;
&lt;td&gt;reprocessing, quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serve&lt;/td&gt;
&lt;td&gt;warehouse + feature store&lt;/td&gt;
&lt;td&gt;latency, all consumers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;catalog + lineage + tag access&lt;/td&gt;
&lt;td&gt;audit, compliance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Walk the skeleton left-to-right, state one trade-off and one NFR per layer, and finish on the governance overlay and the cost envelope. If you run out of time, cut the middle detail — never cut governance and cost, because those are the two pillars the panel is specifically checking you don't forget.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a partitioning + row-access governance snippet
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Architect design answers land better with one concrete artifact that proves you can go from blueprint to enforceable policy. The best two-for-one is a partitioning scheme (the biggest cost lever) plus a row-access policy (governance made executable) on the same table.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partitioning.&lt;/strong&gt; Partition the fact table by date so queries scan only the window they need — the single biggest cost reduction in a warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row-access policy.&lt;/strong&gt; Restrict rows by the querying user's business unit — governance enforced at query time, not by hoping analysts filter correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The payoff.&lt;/strong&gt; One snippet demonstrates both the cost and governance pillars concretely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the partitioning DDL and a row-access policy that limits each business unit to its own rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Partition by event_date&lt;/td&gt;
&lt;td&gt;scan reduction / cost&lt;/td&gt;
&lt;td&gt;cost &amp;amp; performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster by customer_id&lt;/td&gt;
&lt;td&gt;co-locate hot filters&lt;/td&gt;
&lt;td&gt;performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row-access policy by BU&lt;/td&gt;
&lt;td&gt;least-privilege at query time&lt;/td&gt;
&lt;td&gt;governance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Partitioning: the biggest cost lever. Scan only the queried window.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;fact_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_key&lt;/span&gt;    &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_date&lt;/span&gt;   &lt;span class="nb"&gt;DATE&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- partition key&lt;/span&gt;
    &lt;span class="n"&gt;business_unit&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- governance key&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount_cents&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;RANGE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;fact_events_2026_08&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;OF&lt;/span&gt; &lt;span class="n"&gt;fact_events&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-01'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-09-01'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- A query with WHERE event_date &amp;gt;= '2026-08-01' prunes to one partition.&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Row-access governance policy (Snowflake-style syntax shown for clarity):&lt;/span&gt;
&lt;span class="c1"&gt;--    each user only sees rows for their own business unit.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ACCESS&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;bu_isolation&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bu&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;bu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_ROLE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                    &lt;span class="c1"&gt;-- role name == business unit&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;IS_ROLE_IN_SESSION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PLATFORM_ADMIN'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;-- admins see everything&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;fact_events&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ACCESS&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;bu_isolation&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;business_unit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 3. The cost proof: a governed, partitioned query scans one month, one BU.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;business_unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;fact_events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-08-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-09-01'&lt;/span&gt;  &lt;span class="c1"&gt;-- partition prune&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;business_unit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Analyst in role FINANCE_BU sees only FINANCE_BU rows (row-access policy);&lt;/span&gt;
&lt;span class="c1"&gt;-- scans ~1/N of the table (partition pruning). Cost + governance in one query.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Partitioning by &lt;code&gt;event_date&lt;/code&gt; is the biggest cost lever in the warehouse: a query filtered to August scans one monthly partition instead of the whole table, cutting scan cost (and bill) by the ratio of window to table.&lt;/li&gt;
&lt;li&gt;The row-access policy &lt;code&gt;bu_isolation&lt;/code&gt; enforces least-privilege &lt;em&gt;at query time&lt;/em&gt; — an analyst in the &lt;code&gt;FINANCE_BU&lt;/code&gt; role physically cannot read other business units' rows, regardless of how they write the query. Governance you can bypass by forgetting a &lt;code&gt;WHERE&lt;/code&gt; clause is not governance.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;PLATFORM_ADMIN&lt;/code&gt; escape hatch is deliberate and auditable: admins see everything, but the policy names exactly who and why. Architects design the exception explicitly rather than leaving a silent backdoor.&lt;/li&gt;
&lt;li&gt;Steps 1 and 2 compose: the final query prunes to one partition (cost) &lt;em&gt;and&lt;/em&gt; filters to one business unit (governance) automatically. One artifact demonstrates both pillars, which is exactly what an architect answer should do.&lt;/li&gt;
&lt;li&gt;Stating the numbers out loud — "this prunes scan cost to roughly 1/N and enforces BU isolation the analyst cannot bypass" — is the difference between drawing a box labelled "governance" and proving the control works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query context&lt;/th&gt;
&lt;th&gt;Rows scanned&lt;/th&gt;
&lt;th&gt;Rows visible&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FINANCE_BU analyst, August filter&lt;/td&gt;
&lt;td&gt;one partition&lt;/td&gt;
&lt;td&gt;FINANCE_BU only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MARKETING_BU analyst, August filter&lt;/td&gt;
&lt;td&gt;one partition&lt;/td&gt;
&lt;td&gt;MARKETING_BU only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PLATFORM_ADMIN, full scan&lt;/td&gt;
&lt;td&gt;all partitions&lt;/td&gt;
&lt;td&gt;all rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No date filter (anti-pattern)&lt;/td&gt;
&lt;td&gt;all partitions&lt;/td&gt;
&lt;td&gt;still BU-scoped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Back every architecture answer with one enforceable artifact — partitioning for cost, a row-access or masking policy for governance — on the same table. A blueprint with a concrete, bypass-proof control reads as an architect; a blueprint of unlabelled boxes reads as a diagram.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on designing a governed enterprise lakehouse
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design a governed enterprise lakehouse for five business units sharing one platform. Cover the scope questions you'd ask, the NFRs you'd attach, the reference design, how you'd isolate and govern the five BUs, the cost model, and the one trade-off you'd flag as the biggest risk."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a medallion lakehouse with tag-based governance, BU isolation, and a cost envelope
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 1 — SCOPE (ask before drawing)
====================================
  - Who consumes: BI, ML, and each BU's analysts? -&amp;gt; multi-consumer serving
  - Data volume + growth: rows/day, YoY growth? -&amp;gt; sizing + partitioning
  - Freshness per source: which need sub-minute, which nightly? -&amp;gt; ingest mix
  - Compliance: PII? residency? retention? audit? -&amp;gt; governance overlay
  - Isolation: must BUs be prevented from seeing each other's data? -&amp;gt; row-access

Phase 2 — NFRs (attach numbers)
===============================
  - Latency:   BI gold marts p99 &amp;lt; 2s; ingest freshness CDC &amp;lt; 1 min, SaaS hourly
  - Cost:      partition + tier to keep scan cost ~1/N; rough $X/month envelope
  - Availability: 99.9% for analytics serving
  - Governance: column lineage, 7-yr retention, in-region residency, tag-based PII
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 3 — REFERENCE DESIGN (medallion + governance overlay)
===========================================================
 sources -&amp;gt; ingest(CDC + API + stream) -&amp;gt; bronze -&amp;gt; silver(conformed) -&amp;gt; gold(BU marts) -&amp;gt; serve(BI+ML)
 GOVERNANCE OVERLAY: catalog | column lineage | tag-based access (PII/confidential) | data contracts | retention

Phase 4 — BU ISOLATION + GOVERNANCE (the crux)
==============================================
  - One physical platform, logical isolation via a business_unit column + row-access policy
  - Shared conformed dimensions (dim_date, dim_customer) so cross-BU reporting is possible
  - Per-BU gold marts; a platform-admin role for cross-BU governance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Phase 4 made concrete: tag-based classification + row-access + partitioning&lt;/span&gt;
&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;silver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="s1"&gt;'classification=PII'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- tag&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ACCESS&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;bu_isolation&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bu&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;bu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_ROLE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;IS_ROLE_IN_SESSION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PLATFORM_ADMIN'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fact_revenue&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ACCESS&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;bu_isolation&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;business_unit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Phase 2 cost lever made concrete: partition gold facts by month&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fact_revenue&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;RANGE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;-- scan ~1/N&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;ask 5 questions first&lt;/td&gt;
&lt;td&gt;never draw before you know the SLA and PII&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NFRs&lt;/td&gt;
&lt;td&gt;attach numbers&lt;/td&gt;
&lt;td&gt;p99 &amp;lt; 2s, 99.9%, 7-yr retention, in-region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference design&lt;/td&gt;
&lt;td&gt;medallion + governance overlay&lt;/td&gt;
&lt;td&gt;reprocessing + audit-ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BU isolation&lt;/td&gt;
&lt;td&gt;one platform, row-access policy&lt;/td&gt;
&lt;td&gt;shared cost, logical isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conformed dims&lt;/td&gt;
&lt;td&gt;shared dim_date/dim_customer&lt;/td&gt;
&lt;td&gt;cross-BU reporting stays possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;partition + tier&lt;/td&gt;
&lt;td&gt;scan cost ~1/N of table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Biggest risk flagged&lt;/td&gt;
&lt;td&gt;governance drift across BUs&lt;/td&gt;
&lt;td&gt;needs contracts + a platform-admin owner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The answer scopes before drawing, attaches concrete NFR numbers, draws a medallion lakehouse with a first-class governance overlay, and solves the crux — five BUs on one platform — with logical isolation (a &lt;code&gt;business_unit&lt;/code&gt; column plus a bypass-proof row-access policy) over shared physical infrastructure and shared conformed dimensions. It ends by naming the single biggest risk (governance drift across autonomous BUs) and its mitigation (enforced data contracts and a named platform-admin owner), which is the year-three thinking architects are graded on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;How the design meets it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5 BUs, one platform&lt;/td&gt;
&lt;td&gt;shared infra + row-access isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-BU reporting&lt;/td&gt;
&lt;td&gt;shared conformed dimensions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PII compliance&lt;/td&gt;
&lt;td&gt;tag-based classification + masking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost efficiency&lt;/td&gt;
&lt;td&gt;partitioning + storage tiering (scan ~1/N)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit readiness&lt;/td&gt;
&lt;td&gt;column lineage + 7-yr retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Biggest risk&lt;/td&gt;
&lt;td&gt;governance drift → contracts + admin owner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Scope before design&lt;/strong&gt;&lt;/strong&gt; — asking the five scope questions (consumers, volume, freshness, compliance, isolation) before drawing is the phase weak candidates skip. The answer's quality is bounded by the questions you ask first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;NFRs with numbers&lt;/strong&gt;&lt;/strong&gt; — p99 &amp;lt; 2s, 99.9%, 7-year retention, in-region residency. Numbers turn a doodle into an architecture and give the panel something concrete to probe, which is what you want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Logical isolation over physical&lt;/strong&gt;&lt;/strong&gt; — one platform with a row-access policy and a &lt;code&gt;business_unit&lt;/code&gt; column shares cost across BUs while enforcing least-privilege at query time. Separate physical platforms per BU would multiply cost and fragment governance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Conformed dimensions&lt;/strong&gt;&lt;/strong&gt; — shared &lt;code&gt;dim_date&lt;/code&gt;/&lt;code&gt;dim_customer&lt;/code&gt; keep cross-BU reporting coherent, so isolation does not preclude enterprise-wide analytics. Balancing isolation with conformance is the enterprise-architecture skill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — partitioning cuts scan cost to roughly O(window/table) instead of O(table); storage tiering pushes cold data to cheap storage; one shared platform amortises infra across five BUs. The design is defensible on a monthly dollar figure, which is what gets it funded — and naming the governance-drift risk shows you cost the &lt;em&gt;operational&lt;/em&gt; future, not just the build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on lakehouse and platform architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Indexing&lt;/span&gt;
&lt;span&gt;Topic — indexing&lt;/span&gt;
&lt;strong&gt;Indexing and partitioning problems for performance&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Signals architects are graded on &amp;amp; the transition plan
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The four signals, the portfolio that proves them, and the 18-month roadmap from DE to architect
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;an architect interview grades four signals — breadth across the pillars, trade-off fluency, governance-first thinking, and communication — and you make those signals legible with a portfolio (reference designs, ADRs, a governed model, a cost model) and a deliberate 18-month transition roadmap, because the role is won by demonstrated judgement, not claimed seniority&lt;/strong&gt;. The previous four sections taught the pillars; this one is about proving you have them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F572rsitiadjgi5k1wvjd.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F572rsitiadjgi5k1wvjd.jpeg" alt="Iconographic architect-signals and transition diagram — a four-dial signal rubric (breadth, trade-off fluency, governance-first, communication) beside a 0-to-18-month DE-to-architect roadmap timeline with portfolio milestones." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four signals interviewers actually score.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Breadth.&lt;/strong&gt; Can you reason across all five pillars in one answer, or do you retreat to the one you know? The tell is an answer that touches modeling &lt;em&gt;and&lt;/em&gt; governance &lt;em&gt;and&lt;/em&gt; cost in a single design, versus one that goes deep on ingestion and forgets the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off fluency.&lt;/strong&gt; Do you name the alternative and why you rejected it, in numbers? "I chose batch because the SLA is hourly and it's a third the cost of streaming" scores; "I chose batch because it's simpler" does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance-first thinking.&lt;/strong&gt; Do you raise PII, access, lineage, and retention &lt;em&gt;before&lt;/em&gt; being asked? The single strongest differentiator, because it is the pillar DEs forget and the one that ends up in a regulator's inbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communication.&lt;/strong&gt; Can you explain a trade-off to a mixed audience and defend it without defensiveness? The blueprint is worthless if the people funding and building it don't understand it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The portfolio that makes the signals legible — five artifacts.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reference designs (2–3).&lt;/strong&gt; Governed lakehouse, streaming, one hybrid — each with NFRs, a trade-off matrix, and a cost envelope. Proves breadth and trade-off fluency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ADRs (5+).&lt;/strong&gt; Architecture decision records for real decisions, each with context, options, decision, and consequences. Proves communication and judgement over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A governed data model.&lt;/strong&gt; One domain modeled end-to-end (3NF source → star + SCD Type 2) with classification, contracts, and lineage. Proves modeling and governance together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cost model.&lt;/strong&gt; A spreadsheet or doc costing one real workload, with tiering and right-sizing recommendations. Proves the cost pillar concretely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A design talk or RFC.&lt;/strong&gt; Evidence you can lead a design conversation and get a decision funded. Proves communication and stewardship.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The transition roadmap — 18 months, four phases.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Months 0–3 — close the gap.&lt;/strong&gt; Fix the two weakest pillars (usually governance and cost) with shippable artifacts. Score yourself; attack the biggest gap first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Months 3–9 — build the portfolio.&lt;/strong&gt; Ship reference designs and ADRs on real decisions at work. Volunteer for the cross-team design nobody owns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Months 9–15 — credential + broaden.&lt;/strong&gt; Earn the one role-targeted cert; broaden the weakest remaining pillar; get a governed model and cost model into the portfolio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Months 15–18 — lead + interview.&lt;/strong&gt; Lead a real design end-to-end, present it, then interview with a portfolio that proves all four signals.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the architect signal scoring rubric
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The signal rubric is what an interviewer actually fills in, so knowing it lets you engineer your answers to hit every box. Each signal is scored on whether it appeared &lt;em&gt;unprompted&lt;/em&gt;, &lt;em&gt;when prompted&lt;/em&gt;, or &lt;em&gt;not at all&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The scale.&lt;/strong&gt; 2 = raised unprompted, 1 = present when asked, 0 = absent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bar.&lt;/strong&gt; An offer typically needs a 2 on governance-first and trade-off fluency, and at least a 1 on breadth and communication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The lever.&lt;/strong&gt; You can move governance and trade-offs from 1 to 2 simply by raising them before being asked — a free point.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Fill in the signal rubric with the behaviour that earns each score, and name the two free points most candidates leave on the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Score 0&lt;/th&gt;
&lt;th&gt;Score 1&lt;/th&gt;
&lt;th&gt;Score 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Breadth&lt;/td&gt;
&lt;td&gt;one pillar only&lt;/td&gt;
&lt;td&gt;covers most when asked&lt;/td&gt;
&lt;td&gt;integrates all in one answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade-off fluency&lt;/td&gt;
&lt;td&gt;"it's simpler"&lt;/td&gt;
&lt;td&gt;names alternative when asked&lt;/td&gt;
&lt;td&gt;names alternative + numbers unprompted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance-first&lt;/td&gt;
&lt;td&gt;never mentions&lt;/td&gt;
&lt;td&gt;mentions when asked&lt;/td&gt;
&lt;td&gt;raises PII/access/lineage first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Communication&lt;/td&gt;
&lt;td&gt;jargon, defensive&lt;/td&gt;
&lt;td&gt;clear when pushed&lt;/td&gt;
&lt;td&gt;leads the room, invites challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Architect signal scoring rubric  (interviewer's actual sheet)
============================================================

Signal            | 0 (absent)        | 1 (when asked)          | 2 (unprompted)
------------------+-------------------+-------------------------+---------------------------
Breadth           | one pillar        | most pillars if probed  | all pillars in one design
Trade-off fluency | "it's simpler"    | alt named when asked    | alt + numbers, unprompted
Governance-first  | never raised      | raised when asked       | PII/access/lineage FIRST
Communication     | jargon/defensive  | clear under pushback    | leads + invites challenge

Offer bar: governance-first = 2, trade-off = 2, breadth &amp;gt;= 1, communication &amp;gt;= 1

The two FREE points most candidates leave on the table:
  1. Governance-first: raise PII/retention/access BEFORE the interviewer asks -&amp;gt; +1
  2. Trade-off fluency: attach a NUMBER to every choice -&amp;gt; +1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The rubric scores &lt;em&gt;when&lt;/em&gt; a signal appears, not just &lt;em&gt;whether&lt;/em&gt; — unprompted (2) beats prompted (1). This is the mechanism behind "lead with governance": you are literally converting a 1 into a 2 by raising it first.&lt;/li&gt;
&lt;li&gt;Governance-first is the highest-leverage signal because most candidates score 0–1 (they wait to be asked or never mention it), so raising PII/access/lineage unprompted is an immediate differentiator and a free point.&lt;/li&gt;
&lt;li&gt;Trade-off fluency's jump from 1 to 2 is "attach a number." "Batch because it's simpler" is a 1; "batch because the SLA is hourly and it's ~1/3 the cost of streaming" is a 2. The number is the whole difference.&lt;/li&gt;
&lt;li&gt;Breadth is scored by integration: touching all five pillars in one design answer (2) versus going deep on one (0). You engineer this by consciously running the pillar checklist — modeling, governance, integration, cost, cloud — through every design answer.&lt;/li&gt;
&lt;li&gt;Communication is scored on leadership and openness: inviting challenge ("push back on this — where would you deviate?") reads as a 2, defensiveness reads as a 0. Architects lead rooms; they do not defend turf.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Free-point move&lt;/th&gt;
&lt;th&gt;Score delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Governance-first&lt;/td&gt;
&lt;td&gt;raise PII/access/lineage unprompted&lt;/td&gt;
&lt;td&gt;1 → 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade-off fluency&lt;/td&gt;
&lt;td&gt;attach a number to every choice&lt;/td&gt;
&lt;td&gt;1 → 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breadth&lt;/td&gt;
&lt;td&gt;run the 5-pillar checklist per answer&lt;/td&gt;
&lt;td&gt;0 → 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Communication&lt;/td&gt;
&lt;td&gt;invite challenge on your own design&lt;/td&gt;
&lt;td&gt;1 → 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Engineer your answers against the rubric: raise governance first, attach a number to every trade-off, run the five-pillar checklist through every design, and invite challenge. Two of those are free points that most candidates simply forget to take.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a transition-roadmap doc
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The transition roadmap turns "I want to be an architect" into a dated, milestoned plan with portfolio checkpoints. Eighteen months, four phases, a deliverable per phase — reviewable by a mentor and updatable each quarter.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The phases.&lt;/strong&gt; Close-the-gap → build-the-portfolio → credential-and-broaden → lead-and-interview.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The checkpoints.&lt;/strong&gt; Each phase ends with a portfolio artifact and a re-score of the six pillars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The discipline.&lt;/strong&gt; No phase starts before the previous phase's artifact ships.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the 18-month DE-to-architect roadmap for a strong DE weak on governance and cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Months&lt;/th&gt;
&lt;th&gt;Deliverable&lt;/th&gt;
&lt;th&gt;Signal advanced&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Close the gap&lt;/td&gt;
&lt;td&gt;0–3&lt;/td&gt;
&lt;td&gt;contract standard + cost model&lt;/td&gt;
&lt;td&gt;governance, cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build portfolio&lt;/td&gt;
&lt;td&gt;3–9&lt;/td&gt;
&lt;td&gt;2 reference designs + 5 ADRs&lt;/td&gt;
&lt;td&gt;breadth, trade-offs, comms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential + broaden&lt;/td&gt;
&lt;td&gt;9–15&lt;/td&gt;
&lt;td&gt;cloud cert + governed model&lt;/td&gt;
&lt;td&gt;cloud, modeling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lead + interview&lt;/td&gt;
&lt;td&gt;15–18&lt;/td&gt;
&lt;td&gt;lead a design + interview&lt;/td&gt;
&lt;td&gt;communication, all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# DE -&amp;gt; Data Architect Transition Roadmap (18 months)&lt;/span&gt;

&lt;span class="gu"&gt;## Phase 1 (months 0-3) — CLOSE THE GAP&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Self-score the six pillars; identify the two weakest (here: governance, cost)
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Ship a data-contract + PII-classification standard for one domain
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Ship a cost model for one workload (tiering + right-sizing)
&lt;span class="p"&gt;-&lt;/span&gt; CHECKPOINT: governance &amp;amp; cost self-score 1 -&amp;gt; 3; two portfolio artifacts done

&lt;span class="gu"&gt;## Phase 2 (months 3-9) — BUILD THE PORTFOLIO&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Volunteer for the cross-team design nobody owns
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Ship 2 reference designs (governed lakehouse + streaming) with NFRs + cost
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Write 5 ADRs on real decisions (context / options / decision / consequences)
&lt;span class="p"&gt;-&lt;/span&gt; CHECKPOINT: breadth + trade-off signals demonstrable in artifacts

&lt;span class="gu"&gt;## Phase 3 (months 9-15) — CREDENTIAL + BROADEN&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Earn one role-targeted cert (cloud/architect on the primary cloud)
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Model one domain end-to-end (3NF -&amp;gt; star + SCD2 + classification)
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Broaden the weakest remaining pillar to a 3
&lt;span class="p"&gt;-&lt;/span&gt; CHECKPOINT: six-pillar score 3+ across the board, one 4

&lt;span class="gu"&gt;## Phase 4 (months 15-18) — LEAD + INTERVIEW&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Lead one real design end-to-end and present it (design talk / RFC)
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Assemble the portfolio: designs, ADRs, governed model, cost model
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Interview with a portfolio that proves all four signals
&lt;span class="p"&gt;-&lt;/span&gt; CHECKPOINT: offer, or a concrete gap list for the next cycle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Phase 1 attacks the two weakest pillars first with shippable artifacts, because the transition is bottlenecked by the gap, not by the strengths. The checkpoint is a re-score plus two portfolio pieces — measurable, not vague.&lt;/li&gt;
&lt;li&gt;Phase 2 is the longest (six months) because portfolio depth is what wins panels. Volunteering for the unowned cross-team design is the single highest-leverage move — it produces a real reference design &lt;em&gt;and&lt;/em&gt; the cross-team scope that reads as architect work.&lt;/li&gt;
&lt;li&gt;Phase 3 slots the cert in &lt;em&gt;after&lt;/em&gt; the portfolio has momentum, so it certifies real knowledge, and adds the two proof-of-modeling-and-governance artifacts (governed model). The checkpoint is the 3+ across all six pillars.&lt;/li&gt;
&lt;li&gt;Phase 4 is about demonstrated leadership: leading and presenting a real design is the capstone that proves the communication signal, then interviewing with the full portfolio. Even a "no" ends with a concrete gap list, so the cycle compounds.&lt;/li&gt;
&lt;li&gt;The gating discipline — no phase starts before the prior artifact ships — is what stops the roadmap from becoming a wish list. Architects finish things; the roadmap enforces that on the transition itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Exit checkpoint&lt;/th&gt;
&lt;th&gt;Portfolio state&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Close the gap&lt;/td&gt;
&lt;td&gt;governance/cost → 3&lt;/td&gt;
&lt;td&gt;contract standard + cost model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build portfolio&lt;/td&gt;
&lt;td&gt;breadth demonstrable&lt;/td&gt;
&lt;td&gt;2 designs + 5 ADRs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential + broaden&lt;/td&gt;
&lt;td&gt;3+ across all pillars&lt;/td&gt;
&lt;td&gt;+ cert + governed model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lead + interview&lt;/td&gt;
&lt;td&gt;offer or gap list&lt;/td&gt;
&lt;td&gt;full portfolio, presented&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Make the transition a dated, gated roadmap with a portfolio artifact at every checkpoint, and re-score the six pillars each quarter. A transition without milestones drifts for years; a transition with them typically completes in twelve-to-eighteen months.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — an ADR (architecture decision record) template
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; ADRs are the highest-ROI portfolio artifact because they prove judgement over time and cost almost nothing to write. Each records one decision with its context, the options considered, the decision, and the consequences — the exact shape of architect thinking made durable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The shape.&lt;/strong&gt; Context → options (with trade-offs) → decision → consequences (including the downside you accepted).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The value.&lt;/strong&gt; A stack of ADRs is a portable proof that you make reasoned trade-offs, not just technical choices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tell.&lt;/strong&gt; An ADR that omits the rejected options or the accepted downside is a decision log, not an ADR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write an ADR for the decision to use log-based CDC over nightly full-refresh, in the standard template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ADR field&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;need sub-minute freshness + deletes to warehouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Options&lt;/td&gt;
&lt;td&gt;full-refresh, timestamp CDC, log-based CDC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision&lt;/td&gt;
&lt;td&gt;log-based CDC (Debezium)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consequences&lt;/td&gt;
&lt;td&gt;+freshness/deletes; −ops (slot monitoring)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# ADR-014: Use log-based CDC for warehouse ingestion&lt;/span&gt;

&lt;span class="gu"&gt;## Status&lt;/span&gt;
Accepted (2026-08-04)

&lt;span class="gu"&gt;## Context&lt;/span&gt;
The warehouse needs sub-minute freshness and must capture physical deletes for
audit. The current nightly full-refresh is 24h stale and silently drops deletes.
Data volume rules out re-scanning the source table frequently.

&lt;span class="gu"&gt;## Options considered&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Nightly full-refresh (status quo) — simple; 24h stale; blind to deletes. REJECTED.
&lt;span class="p"&gt;2.&lt;/span&gt; Timestamp CDC (poll updated_at) — cheap, portable; blind to physical deletes
   without a reconcile; latency floored by poll interval. REJECTED for the delete gap.
&lt;span class="p"&gt;3.&lt;/span&gt; Log-based CDC (Debezium + pgoutput) — sub-second; captures deletes; ~0 source
   load; needs wal_level=logical + slot monitoring. SELECTED.

&lt;span class="gu"&gt;## Decision&lt;/span&gt;
Adopt log-based CDC via Debezium on a logical replication slot, publishing to Kafka.

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;
&lt;span class="p"&gt;+&lt;/span&gt; Sub-second freshness; native delete capture; near-zero source-table load.
&lt;span class="p"&gt;-&lt;/span&gt; New operational burden: replication-slot lag monitoring + disk-full defense
  (max_slot_wal_keep_size). Requires DBA to grant wal_level=logical.
&lt;span class="p"&gt;-&lt;/span&gt; Accepted downside: on-call must own slot-lag alerts from day one.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Status + Context fields frame &lt;em&gt;why the decision was needed&lt;/em&gt; — sub-minute freshness plus delete capture — so a future reader (or an interviewer) understands the constraint before the choice.&lt;/li&gt;
&lt;li&gt;The Options section lists all three candidates &lt;em&gt;with their trade-offs and an explicit REJECTED/SELECTED&lt;/em&gt;. This is the heart of the ADR and the proof of trade-off fluency — an ADR that lists only the chosen option proves nothing.&lt;/li&gt;
&lt;li&gt;Each rejected option names &lt;em&gt;why&lt;/em&gt; it lost: full-refresh is stale and delete-blind; timestamp CDC is delete-blind without a reconcile. Naming the specific disqualifier is the architect signal.&lt;/li&gt;
&lt;li&gt;The Consequences section records both the upside &lt;em&gt;and the accepted downside&lt;/em&gt; — new slot-monitoring burden, a DBA grant dependency. Owning the downside in writing is what separates an ADR from marketing.&lt;/li&gt;
&lt;li&gt;A stack of ten such ADRs is a portable, dated proof of reasoned judgement — the single most convincing portfolio artifact because it shows how you think, decision after decision, not just what you built.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ADR section&lt;/th&gt;
&lt;th&gt;Proves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;you scope before deciding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Options + trade-offs&lt;/td&gt;
&lt;td&gt;trade-off fluency, breadth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision&lt;/td&gt;
&lt;td&gt;you commit to a choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consequences (incl. downside)&lt;/td&gt;
&lt;td&gt;honesty, year-three thinking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Write an ADR for every non-trivial decision, always including the rejected options and the downside you accepted. Ten ADRs are worth more in a panel than any certificate, because they prove the one thing a cert cannot: how you make trade-offs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on critiquing and improving an architecture
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Here's an existing architecture: a nightly full-refresh from Postgres to a single flat warehouse table, analysts querying it directly with ad-hoc grants, no lineage. Critique it and propose a better one. I'm grading how you find the weaknesses, prioritise them, and defend your redesign."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a signal-ordered critique (governance-first) plus a medallion redesign with a migration path
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Critique — ordered by the signals I'm graded on
===============================================

1. GOVERNANCE (raise first) — ad-hoc grants + no lineage = audit failure waiting
   to happen; no PII classification; no retention policy. HIGHEST priority.
2. CORRECTNESS — nightly full-refresh is 24h stale AND blind to deletes; the flat
   table has no history (any overwrite loses the past).
3. COST — full-refresh re-scans the whole source nightly; the flat table scans
   everything on every query (no partitioning). Expensive and slow.
4. MODELING — one flat table means no conformed dimensions, no grain discipline,
   duplicated logic in every analyst query.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redesign — medallion lakehouse with governance overlay
======================================================
 Postgres --(log-based CDC)--&amp;gt; bronze(raw) --&amp;gt; silver(conformed) --&amp;gt; gold(star marts) --&amp;gt; BI
 GOVERNANCE OVERLAY: catalog | column lineage | tag-based access (PII) | data contracts | 7-yr retention

Key changes vs the old design:
  - full-refresh  -&amp;gt; log-based CDC        (freshness + deletes)
  - flat table    -&amp;gt; medallion + star     (history, grain, reprocessing)
  - ad-hoc grants -&amp;gt; tag-based row access  (governance, least-privilege)
  - no lineage    -&amp;gt; column-level lineage  (audit-ready)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- One concrete governance + cost improvement in the redesign:&lt;/span&gt;
&lt;span class="c1"&gt;-- classify PII, enforce row access, and partition the gold fact.&lt;/span&gt;
&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;silver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="s1"&gt;'classification=PII'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ACCESS&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;bu_isolation&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bu&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;bu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_ROLE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;IS_ROLE_IN_SESSION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PLATFORM_ADMIN'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fact_sales&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ACCESS&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;bu_isolation&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;business_unit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- gold.fact_sales PARTITION BY RANGE (date_key)  -&amp;gt; scan ~1/N instead of full table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;th&gt;Signal demonstrated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Critique order&lt;/td&gt;
&lt;td&gt;governance first, then correctness/cost/modeling&lt;/td&gt;
&lt;td&gt;governance-first thinking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prioritisation&lt;/td&gt;
&lt;td&gt;rank by risk (audit) not by ease&lt;/td&gt;
&lt;td&gt;judgement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redesign&lt;/td&gt;
&lt;td&gt;medallion + CDC + governance overlay&lt;/td&gt;
&lt;td&gt;breadth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration&lt;/td&gt;
&lt;td&gt;run both in parallel, cut over per consumer&lt;/td&gt;
&lt;td&gt;year-three thinking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concrete artifact&lt;/td&gt;
&lt;td&gt;classification + row access + partition&lt;/td&gt;
&lt;td&gt;trade-off made executable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downside owned&lt;/td&gt;
&lt;td&gt;CDC adds slot-monitoring ops&lt;/td&gt;
&lt;td&gt;honesty&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The critique leads with governance (the highest-risk weakness and the highest-scoring signal), prioritises by business risk rather than by what is easiest to fix, and proposes a medallion lakehouse with log-based CDC and a first-class governance overlay. It grounds the redesign in one concrete, bypass-proof artifact (PII classification + row-access policy + partitioning) and names the migration path (parallel-run, per-consumer cutover) plus the accepted downside (new slot-monitoring burden) — the full architect signal set in one answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Weakness&lt;/th&gt;
&lt;th&gt;Old design&lt;/th&gt;
&lt;th&gt;Redesign&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;ad-hoc grants, no lineage&lt;/td&gt;
&lt;td&gt;tag-based access + column lineage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness / deletes&lt;/td&gt;
&lt;td&gt;24h stale, delete-blind&lt;/td&gt;
&lt;td&gt;log-based CDC, sub-minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History&lt;/td&gt;
&lt;td&gt;flat overwrite&lt;/td&gt;
&lt;td&gt;medallion + SCD Type 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;full scan per query&lt;/td&gt;
&lt;td&gt;partitioned, scan ~1/N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modeling&lt;/td&gt;
&lt;td&gt;one flat table&lt;/td&gt;
&lt;td&gt;conformed star marts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Governance-first critique order&lt;/strong&gt;&lt;/strong&gt; — leading with the ad-hoc-grants/no-lineage weakness raises the highest-risk issue first and scores the top signal unprompted. Critiquing correctness before governance would leave the free point on the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Risk-based prioritisation&lt;/strong&gt;&lt;/strong&gt; — ranking weaknesses by business risk (audit failure) rather than by fix difficulty is the judgement signal. Architects triage by consequence, not by convenience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Medallion redesign&lt;/strong&gt;&lt;/strong&gt; — bronze/silver/gold plus a governance overlay fixes history, reprocessing, quality, and audit in one coherent pattern, demonstrating breadth across the pillars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Migration path&lt;/strong&gt;&lt;/strong&gt; — proposing a parallel-run with per-consumer cutover proves you think about &lt;em&gt;how the change lands&lt;/em&gt;, not just the target state — the year-three thinking that separates architects from designers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the redesign trades a new operational burden (slot monitoring) for large wins: partitioning cuts query scan to O(window) instead of O(table), CDC eliminates the nightly full-scan, and governance moves from O(manual audit) to O(automated lineage). Owning the downside in the same breath as the upside is the honesty the rubric rewards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design-critique and redesign problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;Dimensional modeling&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — dimensional-modeling&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Modeling problems for the architect portfolio&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/dimensional-modeling" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — DE → architect recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The lane change in one line.&lt;/strong&gt; The data engineer owns the implementation; the data architect owns the blueprint. You move from the &lt;em&gt;R&lt;/em&gt; (responsible, does the work) column to the &lt;em&gt;A&lt;/em&gt; (accountable, owns the outcome) column — and inherit the cost envelope and cross-team consistency that nobody else is positioned to hold. If you cannot name three responsibilities that flip from &lt;em&gt;you do it&lt;/em&gt; to &lt;em&gt;you own it&lt;/em&gt;, you are interviewing for senior-DE with an architect label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The six-pillar skill ladder.&lt;/strong&gt; Data modeling, governance &amp;amp; security, integration patterns, cost &amp;amp; performance, cloud platforms, and communication/stewardship. The hire bar is 3+ across all six with at least one 4 — a spike in modeling with a hole in governance reads as &lt;em&gt;specialist&lt;/em&gt;, not &lt;em&gt;architect&lt;/em&gt;. Score yourself, sort by gap, fix the biggest gap first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modeling signals.&lt;/strong&gt; Star schema (dimensional) for read-heavy analytics; 3NF for write-heavy operational and silver-layer integrity; Data Vault for auditable, multi-source, regulated environments. Choose the grain first, give every dimension a surrogate key, denormalize lookups into dimensions, keep measures additive, and use SCD Type 2 (versioned surrogate + validity window + &lt;code&gt;is_current&lt;/code&gt;) whenever dimension history matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance-first signals.&lt;/strong&gt; Catalog + column-level lineage + tag-based access (PII/confidential) + data contracts (enforced in CI) + retention/residency. Governance is the pillar DEs score lowest on and interviewers weight highest — raise PII, access, lineage, and retention &lt;em&gt;before&lt;/em&gt; being asked. It is a free point on the rubric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certifications — the two-factor pick.&lt;/strong&gt; Choose certs by the pillar they close and the role they target, with a hard gate that they fix a weak, hire-relevant pillar. Cloud data/architect cert for lakehouse roles; TOGAF/enterprise-architecture for large regulated orgs; platform cert (Snowflake/Databricks/dbt) when the JD names the stack. Cap at two, then pivot to portfolio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cert vs portfolio verdict.&lt;/strong&gt; A cert proves a floor and clears the recruiter screen; a portfolio proves judgement and wins the panel. Certs are necessary-not-sufficient for some roles and optional for others; a portfolio is close to universally decisive. Spend the minimum cert effort to clear the screen, then over-invest in the portfolio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The architecture interview — five phases.&lt;/strong&gt; Scope (ask before drawing) → NFRs (attach numbers: p99 latency, cost/month, 99.9% availability, retention/residency) → reference design (medallion + governance overlay) → trade-offs (name the alternative + why rejected, in numbers) → risks &amp;amp; evolution (what breaks at 10×, the migration path). Weak candidates start drawing; strong candidates start asking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reference-architecture menu.&lt;/strong&gt; Batch lakehouse (cheap, minutes-hours, easy to govern); streaming (sub-second, expensive, harder lineage); Lambda/Kappa hybrid (both, highest complexity — rarely the right default); governed enterprise lakehouse (any of the above with a mandatory governance overlay — the enterprise default). Pick the cheapest row that meets the hardest NFR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NFR numbers to memorize.&lt;/strong&gt; Latency: batch = minutes-hours, micro-batch = seconds-minutes, streaming = sub-second-seconds. Availability: 99.9% for analytics, 99.99% for serving — don't over-engineer analytics to five nines. Cost: partitioning is the biggest scan-reduction lever (~1/N); tier hot/warm/cold; always attach a monthly figure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforceable-artifact reflex.&lt;/strong&gt; Back every architecture answer with one concrete control: partitioning by date (cost — scan ~1/N) plus a row-access or masking policy tied to a classification tag (governance — bypass-proof at query time). A blueprint of unlabelled boxes reads as a diagram; a blueprint with an enforceable control reads as an architect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The four graded signals.&lt;/strong&gt; Breadth (all pillars in one answer), trade-off fluency (alternative + a number), governance-first (raise it unprompted), communication (lead the room, invite challenge). Offer bar: 2 on governance-first and trade-offs, 1+ on breadth and communication. The two free points most candidates forget: raise governance first, and attach a number to every trade-off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The portfolio — five artifacts.&lt;/strong&gt; 2–3 reference designs (with NFRs + cost), 5+ ADRs (context/options/decision/consequences — always include the rejected options and the accepted downside), one governed data model (3NF → star + SCD2 + classification), one cost model, and one design talk/RFC. Ten ADRs beat any certificate in a panel because they prove &lt;em&gt;how&lt;/em&gt; you decide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 18-month roadmap.&lt;/strong&gt; Months 0–3 close the two weakest pillars (usually governance + cost) with shippable artifacts; 3–9 build the portfolio (volunteer for the unowned cross-team design); 9–15 credential + broaden to 3+ across all pillars; 15–18 lead a real design, present it, and interview. Gate each phase on shipping the prior artifact — a transition without milestones drifts for years.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What does a data architect do?
&lt;/h3&gt;

&lt;p&gt;A data architect &lt;strong&gt;owns the blueprint&lt;/strong&gt; of an organisation's data systems — the reference architectures, the modeling and governance standards, the integration patterns, and the cost envelope that engineering teams build against — while being deliberately one level removed from the day-to-day pipeline code. Where a data engineer is accountable for a component (a pipeline, a table), the architect &lt;strong&gt;is accountable&lt;/strong&gt; for how components fit into a coherent system that survives scaling, multiple business units, and governance audits. The role spans six competencies: data modeling, governance and security, integration patterns, cost and performance, cloud platforms, and the communication that makes those decisions legible to engineers, product, finance, and compliance. In practice the architect spends their time on reference designs, data-contract and modeling standards, build-vs-buy decisions, and defending trade-offs to a mixed audience — not on being the on-call hero for any single pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data engineer vs data architect — what's the difference?
&lt;/h3&gt;

&lt;p&gt;The core difference &lt;strong&gt;is&lt;/strong&gt; accountability versus responsibility: the data engineer is &lt;em&gt;responsible&lt;/em&gt; (does the work — builds the DAG, writes the table) while the data architect &lt;strong&gt;is&lt;/strong&gt; &lt;em&gt;accountable&lt;/em&gt; (owns the outcome — sets the standard the DAG conforms to, decides whether the data belongs in the warehouse at all). A data engineer optimises for the sprint and the incident on a stack they know deeply; a data architect optimises for the three-year horizon across a breadth of domains — modeling, governance, integration, cost, cloud. The engineer talks to other engineers and a manager; the architect talks to engineers, product, finance, and often the C-suite, because the blueprint only matters if the people funding and building it both understand it. The most common failed transition is the engineer who takes the architect title but keeps doing the engineer's job, leaving the pure-architect responsibilities (cost envelope, cross-team consistency) unowned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which certifications should a data architect get?
&lt;/h3&gt;

&lt;p&gt;The certifications that &lt;strong&gt;move the needle&lt;/strong&gt; fall into three families, and the right pick depends on your target role, not on prestige. &lt;strong&gt;Cloud data/architect certifications&lt;/strong&gt; (the major-cloud data-engineer and solutions/data-architect tracks) prove the integration, cost, and cloud pillars and have the highest ROI for cloud-lakehouse and platform roles. &lt;strong&gt;Enterprise-architecture certifications&lt;/strong&gt; (TOGAF and equivalents) prove the blueprint vocabulary large regulated organisations expect and are often a literal checkbox in enterprise-architect job descriptions. &lt;strong&gt;Platform certifications&lt;/strong&gt; (Snowflake, Databricks, dbt) prove modeling and performance depth and are worth it when the job description names that stack. Pick by two factors only — the pillar the cert closes and the role it targets — with a hard gate that it must fix a weak, hire-relevant pillar, and cap yourself at two before pivoting your remaining effort to a portfolio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do you need certifications or a portfolio to become a data architect?
&lt;/h3&gt;

&lt;p&gt;You need &lt;strong&gt;both&lt;/strong&gt;, but they do different jobs and a portfolio &lt;strong&gt;is&lt;/strong&gt; the decisive one. A certification proves a floor — it says you know the services and the vocabulary, and it clears the recruiter screen and gives a non-technical gatekeeper a checkbox — but it never proves you can make a trade-off, which is the actual architect job. A portfolio &lt;strong&gt;proves&lt;/strong&gt; judgement: reference designs with NFRs and cost envelopes, ADRs documenting real decisions with their rejected alternatives, a governed data model, and a cost model. The portfolio is what survives the technical panel, where the offer is decided. The optimal strategy is to spend the minimum certification effort to clear the screen (one, maybe two role-targeted certs) and then over-invest in the portfolio artifacts that prove the judgement no exam can certify.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you prepare for the data architect interview?
&lt;/h3&gt;

&lt;p&gt;Prepare for the &lt;strong&gt;architecture design interview&lt;/strong&gt; as a five-phase structure rather than a build exercise: scope the problem (ask about consumers, volume, freshness, and compliance before drawing anything), attach NFRs with real numbers (p99 latency, monthly cost, 99.9% availability, retention and residency), draw a reference design (medallion lakehouse with a governance overlay is the safe default), defend every major choice with its rejected alternative and a number, and close on risks and the migration path. The interview grades four signals — breadth across the pillars, trade-off fluency, governance-first thinking, and communication — so engineer your answers to hit each: run the five-pillar checklist through every design, attach a number to every trade-off, and raise PII, access, and lineage &lt;em&gt;before&lt;/em&gt; being asked. Rehearse a governed-lakehouse whiteboard skeleton and a reference-architecture trade-off matrix until you can draw them cold, and bring a portfolio you can screen-share.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does it take to go from data engineer to data architect?
&lt;/h3&gt;

&lt;p&gt;For a strong mid-to-senior data engineer, the realistic transition &lt;strong&gt;is&lt;/strong&gt; roughly &lt;strong&gt;twelve to eighteen months&lt;/strong&gt; of deliberate, project-based effort — not calendar time coasting in the current role. The bottleneck is almost never the pillars you already have (modeling, integration) but the ones DEs are furthest from: governance and cost, each of which takes about a quarter of focused work to move from "applied" to "standard-setting." A workable roadmap is months 0–3 to close the two weakest pillars with shippable artifacts, months 3–9 to build a portfolio of reference designs and ADRs (volunteer for the cross-team design nobody owns), months 9–15 to earn one role-targeted certification and broaden to a 3+ across all six pillars, and months 15–18 to lead a real design end-to-end and interview. Gate each phase on shipping the prior artifact — a transition without milestones drifts for years, while a milestoned one typically completes inside eighteen months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt; for the system-design, reference-architecture, and design-critique problems every architect interview is built on.&lt;/li&gt;
&lt;li&gt;Build modeling muscle on the &lt;a href="https://pipecode.ai/explore/practice/topic/dimensional-modeling" rel="noopener noreferrer"&gt;dimensional-modeling practice library →&lt;/a&gt; for the star-schema, grain, conformed-dimension, and SCD Type 2 problems architects are graded on.&lt;/li&gt;
&lt;li&gt;Keep your foundations sharp with the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; and the &lt;a href="https://pipecode.ai/explore/practice/topic/indexing" rel="noopener noreferrer"&gt;indexing practice library →&lt;/a&gt; for the query, partitioning, and performance reps behind every cost trade-off.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the six-pillar skill ladder and the architecture trade-off matrix against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Build the judgement architects are hired for&lt;/h3&gt;

&lt;p&gt;Certifications prove a floor. PipeCode drills prove the judgement — when a star schema beats 3NF, when governance is the binding NFR, when the cheaper architecture is the right call, and how to defend a trade-off in numbers. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — design-first practice tuned for the modeling, governance, and trade-off decisions data architects are actually graded on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice design problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/dimensional-modeling" rel="noopener noreferrer"&gt;Practice modeling problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Data Platform Team Structure: Central Platform + Embedded DEs + Analytics Engineers</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Sat, 01 Aug 2026 13:32:01 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/data-platform-team-structure-central-platform-embedded-des-analytics-engineers-220n</link>
      <guid>https://dev.to/gowthampotureddi/data-platform-team-structure-central-platform-embedded-des-analytics-engineers-220n</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;data platform team structure&lt;/code&gt;&lt;/strong&gt; is the decision that quietly determines whether your data org ships trustworthy pipelines in weeks or drowns in a two-quarter backlog of ticket-driven requests — and it is the one architectural choice leaders get wrong most often because they treat it as an HR reporting-line question instead of a systems-design question. The shape of the data platform team — a single &lt;strong&gt;central data platform&lt;/strong&gt; everyone queues behind, &lt;strong&gt;embedded data engineers&lt;/strong&gt; sitting inside product squads, or a hybrid where a platform team paves the road and embedded engineers drive on it — decides how fast a new data product ships, who is accountable when a dashboard is wrong, whether governance is a bottleneck or a guardrail, and how the org scales from ten engineers to a hundred without collapsing into either chaos or gridlock. The trade-off does not live in "should we have a data team"; every company past its first analyst needs one. It lives in &lt;em&gt;which&lt;/em&gt; topology you pick and &lt;em&gt;what&lt;/em&gt; that topology costs you in latency, ownership, and standards drift.&lt;/p&gt;

&lt;p&gt;This guide is the leader-and-staff-engineer walkthrough you wished existed the first time someone asked "how should we structure the data org as we scale," or "central platform or embedded engineers — where do the DEs report," or "when do we adopt data mesh, and what does an &lt;strong&gt;analytics engineer&lt;/strong&gt; actually own." It walks through the framing (Conway's Law for data and the three canonical archetypes), the central platform team run as an internal product, embedded data engineers and the dotted-line reporting problem, analytics engineers and the dbt-era boundary between engineering and analysis, and finally scaling with &lt;strong&gt;team topologies&lt;/strong&gt;, data mesh, headcount ratios, and the anti-patterns that stall a &lt;strong&gt;data team org&lt;/strong&gt;. Each section pairs a teaching block with a Solution-Tail interview answer — a concrete artifact (a charter, a RACI, an org map), a step-by-step trace, an output table, then a concept-by-concept breakdown of why the structure works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrgreiqakhbobufzyvzy.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrgreiqakhbobufzyvzy.jpeg" alt="PipeCode blog header for data platform team structure — bold white headline 'Data Platform Team Structure' over a hero composition of a central platform hub linked to embedded-DE squad pods and an analytics-engineer bridge, on a dark gradient." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;system design practice library →&lt;/a&gt;, rehearse the modelling foundations on the &lt;a href="https://pipecode.ai/explore/practice/topic/dimensional-modeling" rel="noopener noreferrer"&gt;dimensional-modeling practice library →&lt;/a&gt;, and sharpen the pipeline axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why org structure is a data-architecture decision&lt;/li&gt;
&lt;li&gt;The central platform team: paved roads and self-serve&lt;/li&gt;
&lt;li&gt;Embedded data engineers and the dotted-line problem&lt;/li&gt;
&lt;li&gt;Analytics engineers and the dbt-era boundary&lt;/li&gt;
&lt;li&gt;Scaling the org: topologies, data mesh and anti-patterns&lt;/li&gt;
&lt;li&gt;Cheat sheet — data-team-structure recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why org structure is a data-architecture decision
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The org chart becomes the data architecture — Conway's Law is not a metaphor for data teams, it is the mechanism
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;your &lt;code&gt;data platform team structure&lt;/code&gt; is not a management detail bolted onto the architecture after the fact — it &lt;em&gt;is&lt;/em&gt; the architecture, because Conway's Law guarantees the pipelines, the ownership boundaries, the schemas, and the SLAs will mirror the communication structure of whoever builds them, and the three archetypes (centralized, embedded/decentralized, hybrid platform + embedded) each hard-code a different set of trade-offs between delivery speed, domain accuracy, and governance that cannot be undone by tooling alone&lt;/strong&gt;. A company that centralizes every data engineer into one team ships a warehouse that looks like a single monolith with a request queue in front of it. A company that scatters engineers into product squads with no coordinating function ships fifteen incompatible definitions of "active user." The structure you pick in year one becomes the migration you fight in year three, because every domain, dashboard, and downstream consumer hard-codes assumptions about who owns what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The three archetypes every data leader must be able to name.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Centralized.&lt;/strong&gt; All data engineers, analytics engineers, and analysts report into one data organization that serves the whole company. Strong standards, one warehouse, one set of tools — but the central team becomes a bottleneck the moment demand outpaces headcount, and it sits far from the domain knowledge locked inside product teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decentralized / embedded.&lt;/strong&gt; Data engineers are distributed into product or business squads, reporting to those squads. Fast iteration, deep domain context, clear product accountability — but standards drift, tools fragment, and there is nobody who owns the shared platform, so every squad rebuilds ingestion from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid (platform + embedded).&lt;/strong&gt; A central platform team owns the shared substrate (ingestion, orchestration, catalog, governance) and runs it as an internal product; embedded data engineers and analytics engineers sit in the domains and build on the paved road. This is the topology most scaling companies converge on, and the one senior interviewers expect you to defend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conway's Law for data — the mechanism, stated precisely.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The law.&lt;/strong&gt; Any organization that designs a system will produce a design whose structure is a copy of the organization's communication structure. For software this is famous; for data it is under-appreciated and more brutal, because data systems are &lt;em&gt;integration&lt;/em&gt; systems — their whole job is to join across domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The consequence.&lt;/strong&gt; If two teams do not talk, their data will not join cleanly. Two squads that own "orders" and "payments" separately, with no shared platform team forcing a common &lt;code&gt;customer_id&lt;/code&gt; contract, will produce two &lt;code&gt;customer&lt;/code&gt; dimensions that cannot be reconciled without a heroic quarter of remediation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The lever.&lt;/strong&gt; Because the law is a mechanism and not a curse, you can use it deliberately. Want a single source of truth for the customer? Give one team clear ownership of the customer data product and a contract everyone consumes. Want fast domain iteration? Embed engineers where the domain lives. The org chart is the API surface you are actually designing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What breaks at each stage of growth.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Early (centralized, 3–10 people).&lt;/strong&gt; Works well. One team, one warehouse, tight standards. Breaks when the request queue grows faster than the team — every stakeholder is waiting behind a ticket, and the central team is a hostage to its own backlog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mid (embedding starts, 10–40 people).&lt;/strong&gt; Companies push engineers into domains for speed. Works until nobody owns the shared platform — five squads maintain five Airflow instances, three metric definitions for revenue, and no shared lineage. This is the "silo stage."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale (hybrid or mesh, 40+ people).&lt;/strong&gt; The platform team is carved out to own the substrate; domains own their data products on top. Breaks only when governance is under-invested (ungoverned mesh = a thousand data products nobody can find) or the platform team drifts back into being a bottleneck by owning too much.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers and leaders actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you name &lt;strong&gt;all three archetypes&lt;/strong&gt; and their failure modes without prompting? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you invoke &lt;strong&gt;Conway's Law&lt;/strong&gt; as the &lt;em&gt;reason&lt;/em&gt; the structure matters, not as a buzzword? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you tie structure to &lt;strong&gt;ownership and accountability&lt;/strong&gt; ("who is paged when this dashboard is wrong") rather than to headcount? — required answer.&lt;/li&gt;
&lt;li&gt;Do you describe the hybrid platform-plus-embedded model as &lt;strong&gt;"platform paves the road, domains drive on it"&lt;/strong&gt; rather than as "some central, some embedded"? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you connect the org decision to a &lt;strong&gt;concrete failure the current structure is causing&lt;/strong&gt; rather than reorganizing for its own sake? — required answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the three-archetype comparison table
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful artifact for a data-org-structure discussion is a memorised 3×N comparison table. Every serious conversation about how to structure a data team converges on it within the first ten minutes; having it in your head is what separates a fluent leader from someone reciting org-chart clichés. Walk through building the table for a hypothetical mid-size company with three product lines (checkout, growth, logistics) that currently has one central data team of eight people and a two-quarter backlog.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Delivery speed.&lt;/strong&gt; How fast can a squad get a new data product or metric shipped without waiting on another team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain accuracy.&lt;/strong&gt; How correct the semantics are — does "active user" mean the same thing everywhere, and does whoever built the pipeline understand the business.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance and standards.&lt;/strong&gt; Whether there is one warehouse, one set of tools, consistent lineage, and enforced contracts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure mode.&lt;/strong&gt; The specific way each archetype breaks under load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the three-archetype comparison for this company and pick the archetype it should move toward given a two-quarter backlog and three domains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Archetype&lt;/th&gt;
&lt;th&gt;Delivery speed&lt;/th&gt;
&lt;th&gt;Domain accuracy&lt;/th&gt;
&lt;th&gt;Governance&lt;/th&gt;
&lt;th&gt;Primary failure mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Centralized&lt;/td&gt;
&lt;td&gt;slow (queue-bound)&lt;/td&gt;
&lt;td&gt;weak (far from domain)&lt;/td&gt;
&lt;td&gt;strong (one team)&lt;/td&gt;
&lt;td&gt;central bottleneck&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decentralized / embedded&lt;/td&gt;
&lt;td&gt;fast (in-squad)&lt;/td&gt;
&lt;td&gt;strong (in the domain)&lt;/td&gt;
&lt;td&gt;weak (fragmentation)&lt;/td&gt;
&lt;td&gt;silos + drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid (platform + embedded)&lt;/td&gt;
&lt;td&gt;fast on the paved road&lt;/td&gt;
&lt;td&gt;strong (embedded)&lt;/td&gt;
&lt;td&gt;strong (platform owns substrate)&lt;/td&gt;
&lt;td&gt;platform re-centralizes if it owns too much&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data-org archetype decision memo (1 page)
=========================================

Context
  - 8-person central data team, 2-quarter backlog.
  - 3 product lines: checkout, growth, logistics.
  - Symptom: every squad waits behind one ticket queue;
    domain teams complain the central team "does not
    understand our data."

Diagnosis (Conway's Law)
  - Centralized structure =&amp;gt; one integration point =&amp;gt; one
    queue =&amp;gt; bottleneck. Architecture mirrors org: a monolith
    warehouse with a request desk in front.

Options
  A. Stay centralized, add headcount.      -&amp;gt; queue shrinks
                                              but returns.
  B. Fully embed 1 DE per squad, no center. -&amp;gt; fast now,
                                              silos in 2 quarters.
  C. Hybrid: carve a 3-person platform team
     (ingestion, orchestration, catalog),
     embed 1 DE + 1 AE per domain.          -&amp;gt; paved road +
                                              domain speed.

Recommendation
  - Move to (C). Platform team owns the substrate as a product;
    domains own their data products on the road.
  - 90-day plan: publish paved-road ingestion + a single
    semantic layer contract; embed engineers into checkout and
    growth first (highest backlog), logistics next quarter.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The company is centralized, so by Conway's Law its architecture is a single integration point with a queue in front — exactly the bottleneck symptom the backlog describes. The structure is &lt;em&gt;causing&lt;/em&gt; the delivery problem; adding headcount (Option A) treats the symptom, not the mechanism.&lt;/li&gt;
&lt;li&gt;Full embedding (Option B) fixes speed immediately but removes the only thing keeping standards coherent. In two quarters the three domains will have three ingestion stacks and three definitions of revenue — the silo failure mode. Fast today, expensive to reconcile later.&lt;/li&gt;
&lt;li&gt;The hybrid (Option C) splits the problem: extract a small platform team to own the shared substrate as an internal product, and embed engineers into the domains for speed. This is the only option that improves delivery speed &lt;em&gt;and&lt;/em&gt; preserves governance.&lt;/li&gt;
&lt;li&gt;The sequencing matters. You do not embed everyone at once — you pave the road first (a self-serve ingestion path and a single semantic contract), then embed into the highest-backlog domains, so embedded engineers have a road to drive on instead of paving their own.&lt;/li&gt;
&lt;li&gt;The recommendation is written as a one-page memo, not an org chart. Leaders decide on the trade-off, not the boxes-and-lines; the boxes fall out of the trade-off once the platform-versus-domain boundary is agreed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Structure after move&lt;/th&gt;
&lt;th&gt;Owns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Platform team (new, 3 people)&lt;/td&gt;
&lt;td&gt;central, product-run&lt;/td&gt;
&lt;td&gt;ingestion, orchestration, catalog, contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout squad&lt;/td&gt;
&lt;td&gt;embedded DE + AE&lt;/td&gt;
&lt;td&gt;checkout data products on the paved road&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Growth squad&lt;/td&gt;
&lt;td&gt;embedded DE + AE&lt;/td&gt;
&lt;td&gt;growth metrics, experimentation tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logistics squad (next quarter)&lt;/td&gt;
&lt;td&gt;embedded DE + AE&lt;/td&gt;
&lt;td&gt;logistics data products&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never reorganize a data team by copying another company's org chart. Diagnose the failure mode first (bottleneck, silo, or ungoverned), map it to the archetype that is causing it via Conway's Law, then pick the structure whose failure mode you can afford. Write the trade-off as a one-page memo before you draw a single box.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a Conway's-Law mapping exercise
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most senior move in a data-org discussion is to run the mapping in reverse: given the data architecture you &lt;em&gt;want&lt;/em&gt;, derive the org structure that will naturally produce it. Codifying this makes the reorg defensible — you are not moving people for taste, you are aligning the communication structure with the target architecture. Walk through the mapping for a company that wants a single trusted "customer 360" data product consumed by every domain.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target architecture.&lt;/strong&gt; One canonical &lt;code&gt;customer&lt;/code&gt; data product with a published contract; every domain (orders, support, marketing) consumes it rather than rebuilding it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conway's implication.&lt;/strong&gt; A single, clearly-owned artifact requires a single, clearly-owned team with a stable contract and a communication channel to every consumer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The anti-pattern to avoid.&lt;/strong&gt; Three domains each maintaining their own &lt;code&gt;customer&lt;/code&gt; table means three teams, three schemas, no single owner — Conway's Law guarantees fragmentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Map the desired "customer 360" architecture to the team structure and contract that will produce it, and name the coordination mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target architecture property&lt;/th&gt;
&lt;th&gt;Org structure it requires&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One canonical customer data product&lt;/td&gt;
&lt;td&gt;One team with clear, sole ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stable schema every domain trusts&lt;/td&gt;
&lt;td&gt;A published data contract + versioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domains consume, do not fork&lt;/td&gt;
&lt;td&gt;A consumer channel (guild / contract review)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changes do not silently break consumers&lt;/td&gt;
&lt;td&gt;Contract tests in CI + deprecation policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Customer-360 ownership contract&lt;/span&gt;

&lt;span class="gs"&gt;**Owner:**&lt;/span&gt; Platform / Customer-data pod (single team, on-call rotation)
&lt;span class="gs"&gt;**Consumers:**&lt;/span&gt; orders, support, marketing, finance

&lt;span class="gu"&gt;### The contract&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`customer`&lt;/span&gt; data product exposes: &lt;span class="sb"&gt;`customer_id`&lt;/span&gt; (stable, never reused),
  &lt;span class="sb"&gt;`email_hash`&lt;/span&gt;, &lt;span class="sb"&gt;`first_seen_at`&lt;/span&gt;, &lt;span class="sb"&gt;`status`&lt;/span&gt;, &lt;span class="sb"&gt;`lifetime_value_cents`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Schema is versioned (&lt;span class="sb"&gt;`v1`&lt;/span&gt;, &lt;span class="sb"&gt;`v2`&lt;/span&gt;); breaking changes ship a new version
  and a 2-sprint deprecation window on the old one.
&lt;span class="p"&gt;-&lt;/span&gt; SLA: freshness &amp;lt; 1h, availability 99.9%, published on the catalog.

&lt;span class="gu"&gt;### Coordination mechanism&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Any consumer needing a new field files a change request in the
  data-guild channel; the owning pod triages weekly.
&lt;span class="p"&gt;-&lt;/span&gt; Contract tests run in the owner's CI: a PR that breaks &lt;span class="sb"&gt;`customer_id`&lt;/span&gt;
  stability or drops a field fails the build.

&lt;span class="gu"&gt;### Anti-pattern this prevents&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; No domain maintains its own &lt;span class="sb"&gt;`customer`&lt;/span&gt; table. Forking the customer
  entity is a governance violation, not a shortcut.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The target is a &lt;em&gt;single&lt;/em&gt; canonical artifact, so Conway's Law says you need a &lt;em&gt;single&lt;/em&gt; owning team. Spreading customer ownership across three domains would, by the same law, produce three customer definitions — the exact fragmentation you are trying to avoid.&lt;/li&gt;
&lt;li&gt;A stable schema that other teams trust requires a &lt;em&gt;published contract with versioning&lt;/em&gt;. Without it, the owning team ships a breaking change on a Tuesday and four dashboards break on Wednesday. The contract is the API between the owning team and its consumers.&lt;/li&gt;
&lt;li&gt;The coordination mechanism — a guild channel plus a weekly triage — is the "communication structure" Conway's Law is really about. It is the deliberate channel that lets consumers influence the product without forking it.&lt;/li&gt;
&lt;li&gt;Contract tests in the owner's CI turn the social agreement into an enforced one. A PR that breaks &lt;code&gt;customer_id&lt;/code&gt; stability fails the build; enforcement at the pipeline level is far more reliable than enforcement in a wiki page nobody reads.&lt;/li&gt;
&lt;li&gt;The explicit anti-pattern clause ("no domain maintains its own customer table") is what keeps the architecture from degrading. Without a stated prohibition, the first team under deadline pressure forks the entity and the single-source-of-truth is gone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Without the mapping&lt;/th&gt;
&lt;th&gt;With the mapping&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Number of customer definitions&lt;/td&gt;
&lt;td&gt;3+ (one per domain)&lt;/td&gt;
&lt;td&gt;1 (canonical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breaking-change blast radius&lt;/td&gt;
&lt;td&gt;silent, discovered in prod&lt;/td&gt;
&lt;td&gt;caught by contract tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer influence&lt;/td&gt;
&lt;td&gt;forking&lt;/td&gt;
&lt;td&gt;change request + triage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership when it's wrong&lt;/td&gt;
&lt;td&gt;ambiguous&lt;/td&gt;
&lt;td&gt;one on-call pod&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; To get the architecture you want, design the org that Conway's Law will translate into it: one canonical artifact needs one owning team plus a published, versioned, test-enforced contract and a real communication channel for consumers. If you cannot name the single owner, you do not have a data product — you have a fork waiting to happen.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — diagnose this failing data team
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Leaders and interviewers love a "here is a broken data team, tell me what is wrong and how you would fix it" scenario, because it tests whether you diagnose structure or just add tools. The skill is to map the &lt;em&gt;symptoms&lt;/em&gt; to the &lt;em&gt;archetype failure mode&lt;/em&gt; and prescribe a structural change, not a technology. Walk through a realistic broken team.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom set.&lt;/strong&gt; A 25-person data org, fully embedded (each of six squads has 3–4 data people, no central team). Six Airflow deployments. Four definitions of "monthly active user." Nobody can produce a company-wide revenue number without a week of reconciliation. New hires take two months to find where anything lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trap.&lt;/strong&gt; The naive fix is "buy a data catalog" or "standardize on one orchestrator." Tools do not fix a structural gap; they need an owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real diagnosis.&lt;/strong&gt; This is the classic embedded-silo failure mode: fast local delivery, zero shared substrate, no owner of cross-domain semantics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Diagnose the failing team by archetype and prescribe the minimal structural change that fixes the root cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Surface reading&lt;/th&gt;
&lt;th&gt;Structural root cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6 Airflow deployments&lt;/td&gt;
&lt;td&gt;"tooling sprawl"&lt;/td&gt;
&lt;td&gt;no platform owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 definitions of MAU&lt;/td&gt;
&lt;td&gt;"data quality issue"&lt;/td&gt;
&lt;td&gt;no owner of shared metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Week-long revenue reconciliation&lt;/td&gt;
&lt;td&gt;"reporting is slow"&lt;/td&gt;
&lt;td&gt;no canonical finance data product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2-month onboarding&lt;/td&gt;
&lt;td&gt;"docs are bad"&lt;/td&gt;
&lt;td&gt;no catalog, no single substrate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failing-team diagnosis + remediation
====================================

Archetype: fully decentralized / embedded (no center).
Failure mode: silos + standards drift (textbook).

Root cause (NOT tooling):
  There is no team that OWNS the shared substrate or the
  cross-domain semantic contracts. Six squads each optimize
  locally; nobody is accountable for the whole.

Minimal structural fix (do not re-centralize everything):
  1. Carve a 4-person platform + enabling team from the
     existing 25 (do not hire first; reallocate).
       - Owns: one orchestration standard, one catalog,
         the semantic layer for shared entities (user,
         revenue), and the paved-road ingestion path.
  2. Keep DEs/AEs embedded in the six squads, but add a
     dotted line to the platform team (a guild) for standards.
  3. Declare 2 cross-domain data products canonical:
     `active_user` and `revenue`; one squad owns each with
     a contract; other squads consume, do not fork.

What NOT to do:
  - Do not fully centralize (kills the delivery speed the
    embedding bought).
  - Do not "buy a catalog" and hope; a catalog with no owner
    is an empty catalog in 3 months.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The symptoms all point at one root: there is no owner of the shared substrate. Six Airflow deployments, four MAU definitions, and week-long reconciliation are not four separate problems — they are one structural gap (no platform + no cross-domain semantic owner) expressed four ways.&lt;/li&gt;
&lt;li&gt;The trap is to prescribe tools. A catalog, a single orchestrator, or a metrics layer are all necessary — but each needs a team that owns it. Buying tooling without assigning ownership recreates the silo with nicer logos.&lt;/li&gt;
&lt;li&gt;The fix is &lt;em&gt;additive and minimal&lt;/em&gt;: carve a small platform + enabling team from existing headcount rather than hiring, so it lands this quarter. Reallocation signals the change is structural, not a growth bet.&lt;/li&gt;
&lt;li&gt;Crucially, you keep the embedding. The embedded model bought real delivery speed; re-centralizing everything would trade one failure mode (silos) for another (bottleneck). The dotted-line guild preserves speed while restoring standards.&lt;/li&gt;
&lt;li&gt;Declaring exactly two entities canonical (&lt;code&gt;active_user&lt;/code&gt;, &lt;code&gt;revenue&lt;/code&gt;) is deliberate scope control. You fix the highest-pain cross-domain semantics first with contracts, rather than trying to boil the ocean of governance on day one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6 orchestrators, no owner&lt;/td&gt;
&lt;td&gt;1 paved-road standard, platform-owned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 MAU definitions&lt;/td&gt;
&lt;td&gt;1 canonical &lt;code&gt;active_user&lt;/code&gt; contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Week-long revenue reconcile&lt;/td&gt;
&lt;td&gt;1 canonical &lt;code&gt;revenue&lt;/code&gt; data product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No coordination&lt;/td&gt;
&lt;td&gt;dotted-line guild + weekly triage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; When a data team is failing, diagnose the archetype and its failure mode before you prescribe anything. Silos need a platform owner and shared contracts, not more tools; bottlenecks need embedding and self-serve, not more headcount. The fix is almost always structural, and the smallest structural change that removes the root cause beats the biggest tooling purchase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on data org structure
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You join as head of data at a 120-person company. The data function is a single centralized team of twelve with a two-quarter backlog; product teams route every request through a ticket queue and complain the data team does not understand their domains. Walk me through how you would restructure the data org, what you would sequence in the first 90 days, and how you would prevent the new structure from breaking as the company doubles."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a hybrid platform-plus-embedded reorg with a paved road and a 90-day sequence
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Target structure (hybrid platform + embedded)
=============================================

  Central Platform team (4)          &amp;lt;- product-run substrate
    - paved-road ingestion, orchestration standard, catalog,
      semantic layer for shared entities, governance guardrails
    - runs as an internal product with an SLA and a roadmap

  Enabling team (2, time-boxed)      &amp;lt;- upskills domains, then dissolves
    - embeds temporarily to bootstrap each domain onto the road

  Embedded pods (1 DE + 1 AE per domain, ~6 pods)
    - solid line to the domain (delivery + accountability)
    - dotted line to platform via a data guild (standards, craft)

  Analytics engineers
    - own transformation + semantic models per domain, on the
      platform's shared semantic layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## 90-day reorg sequence&lt;/span&gt;

&lt;span class="gu"&gt;### Days 0-30 — stabilize + pave&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Freeze the ticket queue triage into 2 lanes: platform work vs
  domain work. Stop being a request desk.
&lt;span class="p"&gt;-&lt;/span&gt; Carve the 4-person platform team from the existing 12
  (reallocate, do not hire yet). Publish the paved-road
  ingestion path and one orchestration standard.
&lt;span class="p"&gt;-&lt;/span&gt; Stand up a catalog + a single semantic layer skeleton.

&lt;span class="gu"&gt;### Days 30-60 — embed where the pain is&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Embed 1 DE + 1 AE into the top-2 backlog domains (checkout,
  growth). Solid line to the domain; dotted line to the guild.
&lt;span class="p"&gt;-&lt;/span&gt; Migrate those 2 domains onto the paved road; delete their
  bespoke ingestion.
&lt;span class="p"&gt;-&lt;/span&gt; Declare &lt;span class="sb"&gt;`active_user`&lt;/span&gt; and &lt;span class="sb"&gt;`revenue`&lt;/span&gt; canonical data products
  with contracts + contract tests in CI.

&lt;span class="gu"&gt;### Days 60-90 — scale + govern&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Embed into remaining domains. Enabling team rotates through
  each to bootstrap, then steps back.
&lt;span class="p"&gt;-&lt;/span&gt; Publish SLAs/SLOs for the platform (freshness, availability).
&lt;span class="p"&gt;-&lt;/span&gt; Establish the data guild: weekly standards triage, shared
  on-call for the platform, career ladder that values embedded
  and platform paths equally.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Before (centralized)&lt;/th&gt;
&lt;th&gt;After (hybrid)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delivery model&lt;/td&gt;
&lt;td&gt;ticket queue, one team&lt;/td&gt;
&lt;td&gt;self-serve paved road + embedded pods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain accuracy&lt;/td&gt;
&lt;td&gt;weak (team far from domain)&lt;/td&gt;
&lt;td&gt;strong (DE/AE in the domain)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;strong but bottlenecked&lt;/td&gt;
&lt;td&gt;platform owns substrate + contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership when wrong&lt;/td&gt;
&lt;td&gt;ambiguous ("data team")&lt;/td&gt;
&lt;td&gt;embedded pod (delivery) + platform (substrate)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bottleneck&lt;/td&gt;
&lt;td&gt;one queue for everything&lt;/td&gt;
&lt;td&gt;platform work vs domain work split&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling behavior&lt;/td&gt;
&lt;td&gt;linear headcount to keep up&lt;/td&gt;
&lt;td&gt;road scales, pods add per domain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the reorg, product squads stop queuing behind a central desk: they build on a self-serve paved road with embedded engineers who understand their domain, while a small platform team keeps the substrate and the shared contracts coherent. The two-quarter backlog drains because most requests are now self-serve; the platform team's roadmap is the queue for genuinely shared work, and cross-domain semantics stay consistent because &lt;code&gt;active_user&lt;/code&gt; and &lt;code&gt;revenue&lt;/code&gt; are owned data products, not per-squad forks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median request lead time&lt;/td&gt;
&lt;td&gt;weeks (queue-bound)&lt;/td&gt;
&lt;td&gt;days (self-serve + embedded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-domain metric definitions&lt;/td&gt;
&lt;td&gt;many, inconsistent&lt;/td&gt;
&lt;td&gt;canonical, contract-enforced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain context in pipelines&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;high (embedded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance owner&lt;/td&gt;
&lt;td&gt;overloaded central team&lt;/td&gt;
&lt;td&gt;dedicated platform team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling response&lt;/td&gt;
&lt;td&gt;add headcount to the queue&lt;/td&gt;
&lt;td&gt;pave once, add pods per domain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Conway's Law alignment&lt;/strong&gt;&lt;/strong&gt; — the reorg deliberately shapes the communication structure to match the target architecture: a shared substrate needs one owning platform team, domain speed needs embedded pods, and cross-domain truth needs owned data products with contracts. Structure and architecture stop fighting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Platform-as-product paved road&lt;/strong&gt;&lt;/strong&gt; — extracting the substrate into a self-serve product converts most of the backlog from "queue behind the central team" into "self-serve on the road," which is the only way delivery speed stops being headcount-bound.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Embedded pods with a dotted line&lt;/strong&gt;&lt;/strong&gt; — solid line to the domain buys accountability and domain accuracy; the dotted-line guild buys standards without re-centralizing, so you get speed and coherence instead of choosing one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Canonical data products&lt;/strong&gt;&lt;/strong&gt; — declaring &lt;code&gt;active_user&lt;/code&gt; and &lt;code&gt;revenue&lt;/code&gt; as owned, contract-tested products stops the metric fragmentation at its source; consumers consume instead of forking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one small platform team (4) + an enabling team (2, time-boxed) reallocated from existing headcount, plus a guild and contract-test CI. The eliminated cost is the compounding O(domains²) reconciliation of divergent definitions and the perpetual central-queue tax. Net: near-constant coordination cost per new domain instead of linear-or-worse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;System design problems on data org and ownership&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;
&lt;strong&gt;SQL problems on canonical metrics and reconciliation&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. The central platform team
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Run the central data platform as an internal product — paved roads, golden paths, and self-serve, not a ticket queue
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the &lt;code&gt;central data platform&lt;/code&gt; team is the group that owns the shared data substrate — ingestion, orchestration, storage, catalog, governance, and the semantic layer — and its defining move is to run that substrate as an &lt;em&gt;internal product&lt;/em&gt; with real users (the domain engineers), a paved-road golden path, published SLAs, and a funding model, rather than as a services desk that other teams file tickets against&lt;/strong&gt;. The difference between a platform team that scales the org and one that becomes the new bottleneck is entirely whether it thinks "product" (build the road once, let everyone self-serve) or "service" (do the work for everyone, one ticket at a time). Every senior data leader has watched a well-intentioned central team collapse under its own request queue because it never made the product shift.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsljymebefznj1xiyptsj.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsljymebefznj1xiyptsj.jpeg" alt="Iconographic central data platform diagram — a central platform hub emitting paved-road tooling (ingestion, orchestration, catalog) to self-serve squads, with SLA and platform-as-product chips." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform-as-product — the mindset shift that defines the role.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users are engineers.&lt;/strong&gt; The platform team's customers are the embedded DEs and AEs in the domains. Their "product" is measured by adoption and self-serve success, not by tickets closed. A capability nobody adopts is a failed feature, not a completed request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golden path over bespoke.&lt;/strong&gt; The team publishes an opinionated, well-supported "golden path" for the 80% case (ingest a source, schedule a job, register a dataset). Teams that stay on the golden path get support, upgrades, and SLAs for free; teams that go off-road own their own maintenance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roadmap, not backlog.&lt;/strong&gt; A product-run platform has a roadmap driven by user research (what are domains struggling with), not a backlog of one-off asks. This is the single most important cultural signal that a team has made the shift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-serve is the goal.&lt;/strong&gt; Every recurring request should become a self-serve capability. "I need a new source ingested" should be a config change a domain engineer makes, not a ticket the platform team fulfils.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Paved-road capabilities — what the platform actually owns.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion.&lt;/strong&gt; A self-serve path to land a new source into the warehouse/lake with schema detection, incremental loads, and monitoring — so domains do not each rebuild ingestion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration.&lt;/strong&gt; One blessed scheduler and a standard way to define, deploy, and observe pipelines. Domains write DAGs; the platform runs the runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catalog + lineage.&lt;/strong&gt; A single discoverable catalog with lineage, so a new hire can find the canonical &lt;code&gt;revenue&lt;/code&gt; table in minutes, not weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance guardrails.&lt;/strong&gt; Access control, PII tagging, data-contract enforcement, and quality checks provided &lt;em&gt;as guardrails on the paved road&lt;/em&gt;, so doing the right thing is the easy path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic / metrics layer.&lt;/strong&gt; The shared definitions of the entities and metrics everyone uses, exposed so domains build on them instead of forking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SLAs, SLOs, and the reliability contract.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publish them.&lt;/strong&gt; A platform without published SLOs is a platform whose users cannot plan. Freshness (data is &amp;lt; 1h old), availability (the warehouse is up 99.9%), and support response are the three every platform commits to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure and report.&lt;/strong&gt; SLOs mean nothing without dashboards and error budgets. When the platform burns its freshness budget, domain roadmaps slow — which is exactly the feedback loop that justifies platform investment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-call.&lt;/strong&gt; The platform team runs an on-call rotation for the substrate. If the shared ingestion path breaks at 2 a.m., the platform team is paged, not each domain individually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The funding model — the question that decides the team's fate.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Central cost center.&lt;/strong&gt; The platform is funded centrally as shared infrastructure. Simple, avoids per-team friction, but risks the platform being under-invested because "nobody's budget line."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chargeback / showback.&lt;/strong&gt; Domains are charged (or shown) the cost of their platform usage. Aligns incentives and surfaces waste, but adds accounting overhead and can push domains off the paved road to save money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The senior take.&lt;/strong&gt; Most scaling orgs run showback (visibility without hard billing) plus central funding for the core substrate, so the platform is properly resourced &lt;em&gt;and&lt;/em&gt; domains see the cost of their footprint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on the central platform team.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What does a central data platform team own?" — required answer: the shared substrate (ingestion, orchestration, catalog, governance, semantic layer) run as a product.&lt;/li&gt;
&lt;li&gt;"How do you keep a platform team from becoming a bottleneck?" — self-serve golden paths, roadmap over backlog, adoption metrics.&lt;/li&gt;
&lt;li&gt;"How do you fund a platform team?" — central funding for the core + showback for usage visibility.&lt;/li&gt;
&lt;li&gt;"How do you measure platform success?" — adoption, self-serve rate, SLO attainment, domain lead time — not tickets closed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — a platform-team charter and scope doc
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The first artifact a new platform team ships is not code — it is a charter that says what the team owns, what it explicitly does &lt;em&gt;not&lt;/em&gt; own, and how domains engage with it. A crisp charter is what prevents the team from silently sliding back into a services desk. Walk through writing one for a newly-carved 4-person platform team.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In scope.&lt;/strong&gt; The shared substrate and the golden paths on top of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out of scope.&lt;/strong&gt; Domain-specific pipelines and metrics — those belong to embedded engineers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engagement model.&lt;/strong&gt; Self-serve first; guild for standards; roadmap for shared asks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the platform-team charter that defines ownership boundaries and the engagement model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Charter section&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mission&lt;/td&gt;
&lt;td&gt;make domains productive and data trustworthy via a self-serve platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In scope&lt;/td&gt;
&lt;td&gt;ingestion, orchestration, catalog, governance, semantic layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out of scope&lt;/td&gt;
&lt;td&gt;domain pipelines, domain metrics, dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engagement&lt;/td&gt;
&lt;td&gt;self-serve golden path; guild for standards; roadmap for shared work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Success metrics&lt;/td&gt;
&lt;td&gt;adoption, self-serve rate, SLO attainment, domain lead time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Data Platform Team — Charter (v1)&lt;/span&gt;

&lt;span class="gu"&gt;## Mission&lt;/span&gt;
Make every domain team fast and every dataset trustworthy by
providing a self-serve data platform run as an internal product.

&lt;span class="gu"&gt;## What we own (in scope)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Ingestion**&lt;/span&gt;: self-serve source onboarding (config-driven),
  incremental loads, schema detection, ingest monitoring.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Orchestration**&lt;/span&gt;: one blessed scheduler + deploy/observe tooling.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Catalog + lineage**&lt;/span&gt;: single discoverable catalog for all datasets.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Governance guardrails**&lt;/span&gt;: access control, PII tagging, data
  contracts, quality checks — provided ON the paved road.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Semantic layer**&lt;/span&gt;: shared entity + metric definitions.

&lt;span class="gu"&gt;## What we do NOT own (out of scope)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Domain pipelines, domain-specific transformations, dashboards,
  and domain metric logic. Those are owned by embedded DEs/AEs.
&lt;span class="p"&gt;-&lt;/span&gt; We build the road; domains drive on it.

&lt;span class="gu"&gt;## How to engage with us&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Self-serve first**&lt;/span&gt;: golden-path docs cover the 80% case.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Data guild**&lt;/span&gt;: standards, RFCs, and craft questions — weekly.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Roadmap**&lt;/span&gt;: genuinely shared, cross-domain asks are triaged
   into the platform roadmap, not a ticket queue.

&lt;span class="gu"&gt;## Success metrics (reviewed monthly)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; % of domains on the golden path (adoption)
&lt;span class="p"&gt;-&lt;/span&gt; self-serve rate (requests resolved without platform toil)
&lt;span class="p"&gt;-&lt;/span&gt; SLO attainment (freshness, availability)
&lt;span class="p"&gt;-&lt;/span&gt; domain lead time (idea -&amp;gt; shipped data product)

&lt;span class="gu"&gt;## Explicit anti-goal&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; We are NOT a request desk. If a request recurs, we turn it into
  a self-serve capability rather than fulfilling it repeatedly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The mission is framed around &lt;em&gt;domain productivity and trust&lt;/em&gt;, not around the platform's own outputs. This keeps the team honest: the platform exists to make others fast, so its success is measured by their speed, not its ticket throughput.&lt;/li&gt;
&lt;li&gt;The in-scope list is the shared substrate — the things that are wasteful to rebuild per domain. Everything here has the property that centralizing it removes duplication without removing domain autonomy.&lt;/li&gt;
&lt;li&gt;The out-of-scope list is the most important section. By explicitly disowning domain pipelines and metrics, the charter prevents the team from being pulled into doing domain work, which is the road back to being a bottleneck.&lt;/li&gt;
&lt;li&gt;The engagement model puts self-serve first and demotes ticketed work to a roadmap for genuinely shared asks. This is the structural mechanism that keeps the team a product team rather than a services desk.&lt;/li&gt;
&lt;li&gt;The success metrics and the explicit anti-goal ("we are NOT a request desk") turn the culture into something measurable. When a request recurs, the charter obliges the team to productize it — that is the flywheel that drains the backlog over time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Charter answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who ingests a new source?&lt;/td&gt;
&lt;td&gt;domain self-serves on the golden path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who owns the checkout revenue metric?&lt;/td&gt;
&lt;td&gt;the embedded AE, not the platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who is paged if the scheduler is down?&lt;/td&gt;
&lt;td&gt;platform on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who decides a new shared capability?&lt;/td&gt;
&lt;td&gt;platform roadmap via the guild&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What if a request recurs?&lt;/td&gt;
&lt;td&gt;platform productizes it (anti-goal)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; A platform team's charter must state what it does &lt;em&gt;not&lt;/em&gt; own as clearly as what it does. The out-of-scope list and the "not a request desk" anti-goal are what keep the team productized. If your charter only lists responsibilities, you have written a services-desk job description, not a platform charter.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a paved-road capability map with SLOs
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Once the charter defines the boundary, the platform team publishes a capability map: for each capability, the golden-path way to use it, the self-serve status, and the SLO. This map is the platform's product catalog — it tells domains what they can rely on and what they still have to build themselves. Walk through building it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capability.&lt;/strong&gt; Ingestion, orchestration, catalog, quality, semantic layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golden path.&lt;/strong&gt; The blessed, supported way to use each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLO.&lt;/strong&gt; The reliability commitment for each.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the paved-road capability map and attach an SLO to each capability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Golden path&lt;/th&gt;
&lt;th&gt;Self-serve?&lt;/th&gt;
&lt;th&gt;SLO&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ingestion&lt;/td&gt;
&lt;td&gt;config-driven source onboarding&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;new source live &amp;lt; 1 day; freshness &amp;lt; 1h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orchestration&lt;/td&gt;
&lt;td&gt;standard DAG template + deploy CLI&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;99.9% scheduler availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catalog&lt;/td&gt;
&lt;td&gt;auto-registered on ingest&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;100% of golden-path datasets cataloged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data quality&lt;/td&gt;
&lt;td&gt;declarative checks in the DAG template&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;check failures alert &amp;lt; 5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic layer&lt;/td&gt;
&lt;td&gt;shared metric definitions repo&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;metric change reviewed &amp;lt; 2 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# paved-road ingestion — a domain engineer self-serves by adding&lt;/span&gt;
&lt;span class="c1"&gt;# a source config; the platform runtime does the rest.&lt;/span&gt;
&lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout_events&lt;/span&gt;
  &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
  &lt;span class="na"&gt;connector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres_cdc&lt;/span&gt;          &lt;span class="c1"&gt;# from the platform connector catalog&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warehouse.raw.checkout_events&lt;/span&gt;
  &lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;incremental&lt;/span&gt;                &lt;span class="c1"&gt;# platform handles watermarking&lt;/span&gt;
  &lt;span class="na"&gt;freshness_slo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;                &lt;span class="c1"&gt;# platform monitors + alerts&lt;/span&gt;
  &lt;span class="na"&gt;pii&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;column&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email&lt;/span&gt;
      &lt;span class="na"&gt;classification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pii&lt;/span&gt;          &lt;span class="c1"&gt;# governance guardrail: auto-masked&lt;/span&gt;
  &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-pod&lt;/span&gt;              &lt;span class="c1"&gt;# who is paged for domain logic&lt;/span&gt;
  &lt;span class="na"&gt;contract&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;schemas/checkout_events.v1.yaml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Platform-provided SLO query: freshness for every golden-path source.&lt;/span&gt;
&lt;span class="c1"&gt;-- Runs on a schedule; breaches page the platform on-call.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;source_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loaded_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_load&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loaded_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;staleness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;freshness_slo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loaded_at&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;freshness_slo&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;breaching&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ingest_audit&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;source_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;freshness_slo&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loaded_at&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;freshness_slo&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;staleness&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The capability map is a &lt;em&gt;product catalog&lt;/em&gt;: each row tells a domain engineer exactly how to use a capability, whether they can self-serve, and what reliability they can plan around. This is what makes the platform legible to its users.&lt;/li&gt;
&lt;li&gt;The ingestion golden path is config-driven — a domain engineer adds a &lt;code&gt;source&lt;/code&gt; YAML and the platform runtime handles watermarking, freshness monitoring, and PII masking. The domain writes ~12 lines of config instead of a bespoke pipeline, which is the entire point of the paved road.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;pii&lt;/code&gt; block is a governance guardrail delivered &lt;em&gt;on&lt;/em&gt; the road: because the golden path auto-masks tagged columns, doing the compliant thing is the default, not an extra step. Guardrails-on-the-path is how a platform enforces governance without policing.&lt;/li&gt;
&lt;li&gt;The freshness SLO is not just a promise — the platform ships the query that measures it and wires breaches to on-call. An SLO the platform cannot measure is marketing; an SLO with a query and a pager is a contract.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;owner&lt;/code&gt; field encodes the split cleanly: the platform is paged if the ingestion &lt;em&gt;runtime&lt;/em&gt; breaks, but the &lt;em&gt;domain pod&lt;/em&gt; owns the source's business logic and contract. The capability map makes the DE-versus-platform boundary operational.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Who acts&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New checkout source needed&lt;/td&gt;
&lt;td&gt;checkout DE adds YAML&lt;/td&gt;
&lt;td&gt;self-serve golden path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingestion runtime down&lt;/td&gt;
&lt;td&gt;platform on-call&lt;/td&gt;
&lt;td&gt;platform owns the runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;checkout_events schema wrong&lt;/td&gt;
&lt;td&gt;checkout pod&lt;/td&gt;
&lt;td&gt;domain owns the contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PII column added&lt;/td&gt;
&lt;td&gt;auto-masked by guardrail&lt;/td&gt;
&lt;td&gt;governance on the road&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness SLO breached&lt;/td&gt;
&lt;td&gt;platform on-call, then domain if source-side&lt;/td&gt;
&lt;td&gt;SLO query pages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Publish the platform as a capability map with a golden path and an SLO per capability. If a capability has no golden path, domains will build their own and you have a silo; if it has no SLO, domains cannot plan around it. The map is the platform's product; keep it honest by shipping the query that measures every SLO.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a data contract as the platform's enforcement primitive
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The platform's most powerful governance tool is the data contract: a machine-checkable declaration of a dataset's schema, semantics, and quality expectations, enforced in CI so a producer cannot ship a breaking change silently. Contracts are how a platform team enforces standards without becoming a review bottleneck. Walk through defining and enforcing one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Declare.&lt;/strong&gt; The producing team writes a contract for the dataset it owns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce.&lt;/strong&gt; The platform provides a CI check that fails a PR violating the contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consume.&lt;/strong&gt; Downstream teams read the contract to know what they can rely on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a data contract for the canonical &lt;code&gt;active_user&lt;/code&gt; product and the check that enforces it in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Contract element&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dataset&lt;/td&gt;
&lt;td&gt;analytics.active_user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;td&gt;growth-pod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guaranteed columns&lt;/td&gt;
&lt;td&gt;user_id, active_date, channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stability rule&lt;/td&gt;
&lt;td&gt;user_id never null, never reused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality rule&lt;/td&gt;
&lt;td&gt;no duplicate (user_id, active_date)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness&lt;/td&gt;
&lt;td&gt;&amp;lt; 3h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# contracts/active_user.v1.yaml — the canonical active-user product&lt;/span&gt;
&lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analytics.active_user&lt;/span&gt;
&lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;growth-pod&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;One row per user per active day. Canonical MAU/DAU source.&lt;/span&gt;

&lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user_id&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bigint&lt;/span&gt;
    &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;            &lt;span class="c1"&gt;# stability guarantee&lt;/span&gt;
    &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;not_null&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;positive&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;active_date&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;date&lt;/span&gt;
    &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;channel&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;accepted_values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ios&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;android&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;checks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;unique&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;user_id&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;active_date&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# no double counting&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;freshness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;column&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;loaded_at&lt;/span&gt;
      &lt;span class="na"&gt;max_lag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3h&lt;/span&gt;

&lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;breaking_change&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;new_version_required&lt;/span&gt;   &lt;span class="c1"&gt;# v1 -&amp;gt; v2, 2-sprint deprecation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Platform-provided CI gate: fail the PR if a change violates the contract.
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_contract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contract_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return a list of violations; empty list == pass.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contract_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;violations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="n"&gt;declared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
    &lt;span class="n"&gt;present&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_profile&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. No guaranteed column may disappear (breaking change).
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nullable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;present&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing guaranteed column: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Uniqueness check must hold.
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unique&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chk&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;table_profile&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dup_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate rows on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unique&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;violations&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_contract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;load_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CONTRACT VIOLATIONS:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# fails the build -&amp;gt; breaking change blocked
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contract OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The contract declares not just the schema but the &lt;em&gt;guarantees&lt;/em&gt; consumers rely on: &lt;code&gt;user_id&lt;/code&gt; is non-null and never reused, and &lt;code&gt;(user_id, active_date)&lt;/code&gt; is unique so nobody double-counts. These are the semantic promises that make the dataset a product rather than a table.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;accepted_values&lt;/code&gt; on &lt;code&gt;channel&lt;/code&gt; is a semantic guardrail — a pipeline that starts emitting &lt;code&gt;channel = "amp"&lt;/code&gt; fails the contract, catching a data-quality regression at the producer instead of in a downstream dashboard.&lt;/li&gt;
&lt;li&gt;The CI gate is what turns the contract from documentation into enforcement. A PR that drops &lt;code&gt;user_id&lt;/code&gt; or introduces duplicates fails the build; the producer cannot ship the breaking change silently. The platform provides the gate once; every contracted dataset reuses it.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;breaking_change: new_version_required&lt;/code&gt; policy encodes how change happens: you do not mutate &lt;code&gt;v1&lt;/code&gt; in place, you ship &lt;code&gt;v2&lt;/code&gt; with a deprecation window. This is how the platform lets producers evolve without breaking consumers overnight.&lt;/li&gt;
&lt;li&gt;This is the platform enforcing standards &lt;em&gt;without&lt;/em&gt; being a review bottleneck. The platform team does not manually review every dataset change — it ships the contract mechanism and the CI gate, and enforcement scales automatically across every domain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Producer action&lt;/th&gt;
&lt;th&gt;Contract gate result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add a nullable column&lt;/td&gt;
&lt;td&gt;pass (additive)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drop &lt;code&gt;user_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;fail (guaranteed column missing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Introduce duplicate (user_id, active_date)&lt;/td&gt;
&lt;td&gt;fail (uniqueness)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emit unexpected &lt;code&gt;channel&lt;/code&gt; value&lt;/td&gt;
&lt;td&gt;fail (accepted_values)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ship v2 with deprecation window&lt;/td&gt;
&lt;td&gt;pass (policy-compliant)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Give the platform team a data-contract primitive and a CI gate, and it enforces standards at the speed of every domain's pipeline instead of the speed of a central review queue. Contracts are how governance scales: the platform builds the guardrail once, and correct behavior becomes the default path for every producer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the central platform team
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your central data platform team of five is drowning — it has a 40-ticket backlog, domains complain it is slow, and leadership is asking whether to just disband it and embed everyone. Make the case for the platform team, then describe exactly how you would transform it from a services desk into a product team, including what you would build, what you would stop doing, and how you would measure the change."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a platform-as-product transformation with golden paths, showback funding, and adoption metrics
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## The case for the platform team&lt;/span&gt;
Disbanding it re-creates the silo failure mode: 6 domains each
rebuild ingestion, orchestration, and governance -&amp;gt; O(domains)
duplication + divergent standards. The platform team is not the
problem; being a &lt;span class="ge"&gt;*services desk*&lt;/span&gt; is the problem. Fix the operating
model, not the existence of the team.

&lt;span class="gu"&gt;## Transformation plan (services desk -&amp;gt; product)&lt;/span&gt;

&lt;span class="gu"&gt;### Stop doing&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Stop fulfilling one-off pipeline requests. Every recurring
  request becomes a self-serve capability instead.
&lt;span class="p"&gt;-&lt;/span&gt; Stop being the single approver on domain dataset changes;
  replace manual review with contract-test CI gates.

&lt;span class="gu"&gt;### Start building (golden paths)&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Self-serve ingestion (config-driven source onboarding).
&lt;span class="p"&gt;2.&lt;/span&gt; A standard DAG template + deploy CLI (self-serve orchestration).
&lt;span class="p"&gt;3.&lt;/span&gt; Auto-cataloging on ingest + lineage.
&lt;span class="p"&gt;4.&lt;/span&gt; Data-contract CI gate (governance without a review queue).
&lt;span class="p"&gt;5.&lt;/span&gt; Shared semantic layer for &lt;span class="sb"&gt;`user`&lt;/span&gt; and &lt;span class="sb"&gt;`revenue`&lt;/span&gt;.

&lt;span class="gu"&gt;### Funding&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Central funding for the core substrate + showback so domains
  SEE their platform footprint (aligns incentives, no hard billing
  friction that pushes teams off-road).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The metric that proves the transformation: self-serve rate.&lt;/span&gt;
&lt;span class="c1"&gt;-- Requests resolved WITHOUT platform-team toil / total requests.&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;reqs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                             &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;resolution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'self_serve'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;self_serve&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;self_serve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self_serve&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;nullif&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;self_serve_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reqs&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Before (services desk)&lt;/th&gt;
&lt;th&gt;After (product)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Work intake&lt;/td&gt;
&lt;td&gt;40-ticket backlog&lt;/td&gt;
&lt;td&gt;self-serve + roadmap for shared work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingestion&lt;/td&gt;
&lt;td&gt;platform builds each pipeline&lt;/td&gt;
&lt;td&gt;domains self-serve via config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;manual review bottleneck&lt;/td&gt;
&lt;td&gt;contract-test CI gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Funding&lt;/td&gt;
&lt;td&gt;central, "nobody's line"&lt;/td&gt;
&lt;td&gt;central core + showback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Success metric&lt;/td&gt;
&lt;td&gt;tickets closed&lt;/td&gt;
&lt;td&gt;self-serve rate + adoption + SLOs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team identity&lt;/td&gt;
&lt;td&gt;request desk&lt;/td&gt;
&lt;td&gt;internal product team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the transformation, the backlog stops growing because most requests become self-serve config changes domains make themselves; the platform team's roadmap becomes the queue only for genuinely shared work; governance is enforced by CI gates rather than a human review queue; and showback makes every domain's platform footprint visible so investment is justified by usage. The self-serve-rate metric climbs quarter over quarter, which is the objective proof that the team became a product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Open backlog&lt;/td&gt;
&lt;td&gt;40 tickets, growing&lt;/td&gt;
&lt;td&gt;shrinking; shared-only roadmap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-serve rate&lt;/td&gt;
&lt;td&gt;~10%&lt;/td&gt;
&lt;td&gt;70%+ target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance throughput&lt;/td&gt;
&lt;td&gt;review-queue bound&lt;/td&gt;
&lt;td&gt;CI-gate, per-pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain lead time&lt;/td&gt;
&lt;td&gt;weeks&lt;/td&gt;
&lt;td&gt;days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform investment case&lt;/td&gt;
&lt;td&gt;"cost center"&lt;/td&gt;
&lt;td&gt;showback-justified&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Platform-as-product operating model&lt;/strong&gt;&lt;/strong&gt; — the transformation changes &lt;em&gt;how the team works&lt;/em&gt; (product, roadmap, self-serve) rather than &lt;em&gt;whether it exists&lt;/em&gt;. Disbanding treats the symptom; productizing removes the root cause (services-desk intake) while keeping the anti-duplication benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Golden-path self-serve&lt;/strong&gt;&lt;/strong&gt; — converting recurring requests into self-serve capabilities is the only mechanism that breaks the headcount-bound backlog; each productized capability permanently removes a class of tickets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Contract-test CI gate&lt;/strong&gt;&lt;/strong&gt; — governance moves from a human review queue (O(changes) toil) to an automated gate (O(1) platform build, enforced per pipeline), so standards scale with the org instead of bottlenecking it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Showback funding&lt;/strong&gt;&lt;/strong&gt; — visibility of usage aligns incentives and justifies investment without the hard-billing friction that drives teams off the paved road.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — five platform engineers building golden paths once, plus a showback pipeline and CI gates. The eliminated cost is O(domains) duplicated substrate plus a perpetually growing ticket queue. Net: the platform's cost is amortized across every domain that self-serves on it, and the self-serve-rate metric makes that leverage measurable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on self-serve data platforms&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on ingestion and orchestration paved roads&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Embedded data engineers
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Embed data engineers in the domains for proximity and ownership — then solve the dotted-line problem before the silos form
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;embedded data engineers&lt;/code&gt; are DEs who sit inside product or business squads — reporting to the squad for delivery and accountability, working shoulder-to-shoulder with the engineers and PMs who own the domain — which buys deep domain context and fast iteration, but which creates the dotted-line problem: without a coordinating guild and a platform to build on, embedded engineers drift into silos, reinvent the same infrastructure, and diverge on standards until the org has as many data stacks as it has squads&lt;/strong&gt;. Embedding is the highest-leverage structure for delivery speed and the highest-risk structure for coherence, and the entire art is capturing the speed while paying down the silo risk with a deliberate reporting and guild model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4rh737to4435wq0l3vtn.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4rh737to4435wq0l3vtn.jpeg" alt="Iconographic embedded data engineers diagram — DEs placed inside product squads with a solid reporting line to the squad and a dotted guild line to the platform, plus a standards-enforcement chip." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why embed — the benefits that make it worth the risk.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Proximity to the domain.&lt;/strong&gt; An embedded DE hears the product discussions, understands why an order can be in five states, and models the data correctly the first time. Domain context is the single biggest determinant of pipeline correctness, and embedding maximizes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast iteration.&lt;/strong&gt; No cross-team ticket to change a schema — the DE and the product engineer are in the same standup. Lead time from "we need this metric" to "it is in the dashboard" collapses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear product accountability.&lt;/strong&gt; When the checkout dashboard is wrong, the checkout pod is accountable, not a distant central team. Ownership is unambiguous because the owner sits in the domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better prioritization.&lt;/strong&gt; Embedded engineers work on what the domain actually needs, ranked by domain value, rather than on whatever bubbled to the top of a shared queue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The dotted-line problem — the core tension of embedding.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solid line vs dotted line.&lt;/strong&gt; The classic model: embedded DEs report &lt;em&gt;solid line&lt;/em&gt; to their squad (day-to-day work, delivery, performance) and &lt;em&gt;dotted line&lt;/em&gt; to a central data function or guild (standards, craft, career development, tooling). The solid line owns &lt;em&gt;what&lt;/em&gt; they build; the dotted line owns &lt;em&gt;how well&lt;/em&gt; they build it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why the dotted line matters.&lt;/strong&gt; Without it, an embedded DE is a data team of one, disconnected from peers, with no standards pressure and no craft community. They will make locally-reasonable choices that are globally incoherent — a different orchestrator, a different testing approach, a fork of the customer table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure of pure solid-line.&lt;/strong&gt; If the DE reports &lt;em&gt;only&lt;/em&gt; to the squad, the squad optimizes for its own delivery and treats shared standards as overhead. Multiply by six squads and you have six incompatible stacks — the silo failure mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure of pure dotted-line.&lt;/strong&gt; If the "embedded" DE actually reports to the central team and is merely seconded to the squad, they get treated as an outsider, deprioritized, and never gain real domain context — the worst of both worlds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The guild model — how to make the dotted line real.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What a guild is.&lt;/strong&gt; A cross-cutting community of practice: all the embedded DEs (and AEs) across squads meet regularly, own shared standards, review each other's designs, and collectively steward the craft. It is the dotted line made concrete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the guild owns.&lt;/strong&gt; Coding standards, the golden-path adoption push, shared RFCs for cross-domain decisions, onboarding, and the career ladder for embedded engineers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guild + platform together.&lt;/strong&gt; The guild sets standards; the platform team provides the paved road that makes those standards the easy path. Guild without platform is a book club; platform without guild is a road nobody agrees to drive on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Standards enforcement without central control.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contracts over review.&lt;/strong&gt; Enforce shared standards through the platform's data-contract CI gates and shared DAG templates, not through a central team reviewing every PR. This scales; central review does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golden-path defaults.&lt;/strong&gt; Make the standard the default by shipping templates and scaffolding. An embedded DE who scaffolds a new pipeline from the golden-path template inherits the standards for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Career-path parity.&lt;/strong&gt; Ensure embedded and platform tracks have equal promotion ceilings. If the only way to get promoted is to join the central team, your best embedded engineers leave the domains and the model collapses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on embedded DEs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Central vs embedded — where do DEs report?" — required answer: solid line to the domain, dotted line to a guild/platform for standards.&lt;/li&gt;
&lt;li&gt;"How do you stop embedded engineers from siloing?" — guild + platform paved road + contract-enforced standards + career parity.&lt;/li&gt;
&lt;li&gt;"What is the risk of embedding?" — standards drift, infra duplication, isolated engineers, divergent metrics.&lt;/li&gt;
&lt;li&gt;"When would you NOT embed?" — very early stage (too few people) or a capability that is genuinely shared and better centralized.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the embedding RACI matrix
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The cleanest way to settle "who owns what" between an embedded DE, the squad, the platform, and the guild is a RACI matrix (Responsible, Accountable, Consulted, Informed). It makes the solid-line/dotted-line split concrete for each activity and pre-empts the turf ambiguity that kills embedded models. Walk through building one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Actors.&lt;/strong&gt; Embedded DE, squad lead, platform team, data guild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activities.&lt;/strong&gt; Domain pipeline delivery, tooling choice, standards, career development, incident response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the RACI for an embedded DE across the five activities that most often cause ownership disputes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Embedded DE&lt;/th&gt;
&lt;th&gt;Squad lead&lt;/th&gt;
&lt;th&gt;Platform team&lt;/th&gt;
&lt;th&gt;Data guild&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Domain pipeline delivery&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tooling / infra choice&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coding + data standards&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Career development / promo&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain data incident&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reading the embedded-DE RACI
============================
R = Responsible (does the work)
A = Accountable (answerable; one per row)
C = Consulted   (two-way input)
I = Informed    (one-way notified)

Key resolutions this RACI encodes:
  - Pipeline delivery: DE does it (R), squad lead owns the
    outcome (A). This is the SOLID line -&amp;gt; the domain.
  - Tooling choice: platform is Accountable (A) so the org does
    not fragment; DE is Responsible for using it well. Domains
    do not each pick their own orchestrator.
  - Standards: the guild is Accountable (A). This is the DOTTED
    line -&amp;gt; craft coherence across squads.
  - Career/promo: shared A between squad lead (day-to-day) and
    guild (craft bar). Prevents "you must join the central team
    to get promoted."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pipeline delivery has the DE as Responsible and the squad lead as Accountable — this is the solid line encoded precisely. The domain owns the outcome; the embedded engineer does the work inside the domain. Nobody outside the squad is on the hook for domain delivery.&lt;/li&gt;
&lt;li&gt;Tooling and infra choice flips the accountability to the platform team. This is the anti-silo control: individual squads do not get to pick their own orchestrator or warehouse, because that is exactly how six incompatible stacks arise. The DE is Responsible for using the shared tooling well, not for choosing new tooling.&lt;/li&gt;
&lt;li&gt;Standards are the guild's accountability — the dotted line made operational. The DE follows and contributes to standards; the squad lead is only Informed, which stops a delivery-focused squad lead from overriding craft standards for short-term speed.&lt;/li&gt;
&lt;li&gt;Career development is deliberately a &lt;em&gt;shared&lt;/em&gt; accountability between the squad lead and the guild. The squad lead knows the day-to-day performance; the guild guards the craft bar and ensures the embedded path has the same ceiling as the platform path. This is the single most important row for retention.&lt;/li&gt;
&lt;li&gt;Incident response mirrors delivery: the DE responds, the squad lead is accountable for the domain's data being right. The platform is Consulted because the incident might be in the shared substrate, in which case it escalates to platform on-call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dispute&lt;/th&gt;
&lt;th&gt;RACI resolution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Can our squad use its own scheduler?"&lt;/td&gt;
&lt;td&gt;No — platform is Accountable for tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Who fixes the wrong checkout metric?"&lt;/td&gt;
&lt;td&gt;Embedded DE (R), squad lead (A)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Who sets the testing standard?"&lt;/td&gt;
&lt;td&gt;The guild (A)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Can I get promoted staying embedded?"&lt;/td&gt;
&lt;td&gt;Yes — guild + squad lead share promo accountability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Substrate is down mid-incident"&lt;/td&gt;
&lt;td&gt;Escalate to platform (Consulted -&amp;gt; on-call)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Settle embedded-team ownership with a RACI before the first turf dispute, not after. The two rows that matter most are tooling (platform Accountable, to stop fragmentation) and career development (guild + squad shared, to stop your best embedded engineers from leaving). If those two are ambiguous, the embedded model degrades into silos or attrition.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a data-guild charter that operationalizes the dotted line
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The guild is the mechanism that makes the dotted line real, but a guild without a charter is just a recurring meeting that decays into a status update. A guild charter defines its purpose, membership, decision rights, and rituals so it actually stewards standards. Walk through writing one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose.&lt;/strong&gt; Steward the craft and standards across all embedded data engineers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision rights.&lt;/strong&gt; What the guild can decide vs recommend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rituals.&lt;/strong&gt; The concrete recurring activities that keep it alive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the data-guild charter that makes the dotted-line reporting relationship operational.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Charter element&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;shared standards, craft, and career coherence for embedded DEs/AEs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Membership&lt;/td&gt;
&lt;td&gt;all embedded DEs + AEs; platform sends a liaison&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision rights&lt;/td&gt;
&lt;td&gt;owns standards + golden-path adoption; recommends on tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rituals&lt;/td&gt;
&lt;td&gt;weekly design review, RFC process, quarterly standards refresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Career role&lt;/td&gt;
&lt;td&gt;maintains the leveling rubric for embedded engineers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Data Guild — Charter&lt;/span&gt;

&lt;span class="gu"&gt;## Purpose&lt;/span&gt;
Give every embedded data engineer a craft home: shared standards,
peer design review, and a career ladder that does not require
leaving the domain. This IS the dotted line.

&lt;span class="gu"&gt;## Membership&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; All embedded DEs and analytics engineers across squads.
&lt;span class="p"&gt;-&lt;/span&gt; A platform-team liaison (keeps the road and the standards aligned).
&lt;span class="p"&gt;-&lt;/span&gt; Rotating facilitator (not a permanent "guild lead" -&amp;gt; shared ownership).

&lt;span class="gu"&gt;## Decision rights&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Owns (can decide):**&lt;/span&gt; coding/data standards, testing conventions,
  the golden-path adoption bar, the RFC process, onboarding for new
  embedded engineers.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Recommends (advisory):**&lt;/span&gt; tooling/infra choices -&amp;gt; final call sits
  with the platform team (see RACI).
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Escalates:**&lt;/span&gt; cross-domain data-product ownership disputes -&amp;gt;
  head of data.

&lt;span class="gu"&gt;## Rituals&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Weekly design review**&lt;/span&gt;: any non-trivial pipeline or model design
  gets a 20-min peer review before it ships.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**RFC process**&lt;/span&gt;: cross-domain decisions (a new shared entity, a
  standard change) go through a written RFC with guild sign-off.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Quarterly standards refresh**&lt;/span&gt;: prune, update, and re-publish
  standards so they do not rot.

&lt;span class="gu"&gt;## Career role&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Maintains the leveling rubric so an embedded DE can reach staff/
  principal without transferring to the platform team.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The purpose statement names the guild as &lt;em&gt;the dotted line&lt;/em&gt; explicitly. This is important: it tells embedded engineers that the guild is where their craft home and career coherence live, not a bureaucratic overhead meeting. The dotted line is a community, not a reporting box.&lt;/li&gt;
&lt;li&gt;Membership includes a platform liaison, which keeps the guild's standards and the platform's paved road in sync. A guild that sets standards the platform does not support produces standards nobody can follow; the liaison closes that loop.&lt;/li&gt;
&lt;li&gt;The decision rights are split precisely to match the RACI: the guild &lt;em&gt;owns&lt;/em&gt; standards and craft (can decide) but only &lt;em&gt;recommends&lt;/em&gt; on tooling (platform decides). This prevents the guild from becoming a shadow platform team while still giving it real authority over how work is done.&lt;/li&gt;
&lt;li&gt;The rituals are what keep the guild from decaying. Weekly design review catches divergence before it ships; the RFC process gives cross-domain decisions a written, reviewable form; the quarterly refresh stops standards from rotting into ignored wiki pages.&lt;/li&gt;
&lt;li&gt;The career-role clause is the retention mechanism. By maintaining a leveling rubric that lets embedded engineers reach staff/principal in place, the guild removes the incentive for the best engineers to abandon the domains — which is the failure mode that silently kills embedded models over 18 months.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Without a guild charter&lt;/th&gt;
&lt;th&gt;With a guild charter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Embedded DEs isolated, no peers&lt;/td&gt;
&lt;td&gt;craft community + peer review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standards decided ad hoc per squad&lt;/td&gt;
&lt;td&gt;guild-owned, refreshed quarterly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-domain decisions litigated repeatedly&lt;/td&gt;
&lt;td&gt;written RFC + sign-off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promotion requires joining central team&lt;/td&gt;
&lt;td&gt;in-place staff/principal ladder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standards rot&lt;/td&gt;
&lt;td&gt;quarterly refresh keeps them live&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; A guild without decision rights and rituals is a status meeting; give it clear ownership of standards, an RFC process, and a career ladder, and it becomes the dotted line that keeps embedded engineers coherent without a central command. The quarterly refresh and the in-place career ladder are the two clauses that decide whether the guild survives its first year.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the reporting-line decision for a new domain
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A concrete recurring decision: a new domain is spinning up and needs data engineering. Do you embed a DE, extend the platform, or start with the guild providing part-time support? The answer depends on domain maturity, data complexity, and shared-versus-specific needs. Walk through the decision for three new domains.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signals.&lt;/strong&gt; Domain data complexity, delivery cadence, how much is genuinely shared vs domain-specific.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Options.&lt;/strong&gt; Full embed, platform-supported self-serve, or guild-supported part-time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-pattern.&lt;/strong&gt; Embedding a lone DE into a domain with no paved road to build on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Decide the reporting/support model for three new domains and justify each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;New domain&lt;/th&gt;
&lt;th&gt;Data complexity&lt;/th&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;th&gt;Shared vs specific&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;mostly specific&lt;/td&gt;
&lt;td&gt;full embed (DE + AE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal tools&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;slow&lt;/td&gt;
&lt;td&gt;mostly shared&lt;/td&gt;
&lt;td&gt;platform self-serve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Experimentation&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;shared framework, specific data&lt;/td&gt;
&lt;td&gt;embed AE + platform framework&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reporting-line decision tree (new domain)
=========================================

Q1. Is the domain's data complex AND its cadence fast?
      yes -&amp;gt; embedding is justified (proximity pays off)
      no  -&amp;gt; go to Q2

Q2. Is the need mostly SHARED (not domain-specific)?
      yes -&amp;gt; platform self-serve (do NOT embed a lone DE)
      no  -&amp;gt; lightweight guild support until it grows

Q3. If embedding: is there a paved road to build on?
      yes -&amp;gt; embed DE (+ AE if modelling-heavy)
      no  -&amp;gt; pave the minimal road FIRST, then embed
             (never drop a lone DE into a greenfield with no
              platform -&amp;gt; that is how silos are born)

Applied:
  Payments        -&amp;gt; Q1 yes -&amp;gt; embed DE + AE (complex, fast, specific)
  Internal tools  -&amp;gt; Q1 no, Q2 yes -&amp;gt; platform self-serve
  Experimentation -&amp;gt; Q1 yes, but framework is shared -&amp;gt;
                     embed 1 AE for domain models +
                     consume the platform's experimentation framework
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Payments is complex, fast-moving, and mostly domain-specific — the textbook case for full embedding. The proximity of an embedded DE and AE to the payments domain pays off immediately in correctness and speed, and there is little shared logic to centralize.&lt;/li&gt;
&lt;li&gt;Internal tools is low-complexity, slow-cadence, and mostly shared needs. Embedding a dedicated DE here would be waste — the DE would be underutilized and isolated. Platform self-serve is correct: the internal-tools team uses the golden path for its modest needs.&lt;/li&gt;
&lt;li&gt;Experimentation is the nuanced case: the &lt;em&gt;data&lt;/em&gt; is domain-specific but the &lt;em&gt;framework&lt;/em&gt; (assignment, exposure logging, stats) is shared. The right answer is a hybrid — embed an AE for the domain-specific models, but have them consume a platform-owned experimentation framework rather than build one.&lt;/li&gt;
&lt;li&gt;The decision tree's Q3 encodes the most important anti-pattern check: never embed a lone DE into a greenfield domain with no paved road. Without a platform to build on, that DE will pave their own road, and you have manufactured a silo. Pave the minimal road first, then embed.&lt;/li&gt;
&lt;li&gt;Reporting lines follow the decision: embedded engineers (payments, experimentation) get solid line to the domain and dotted line to the guild; the self-serve domain (internal tools) has no embedded engineer and simply consumes the platform. The structure matches the need rather than applying one template everywhere.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Reporting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;embed DE + AE&lt;/td&gt;
&lt;td&gt;solid: payments; dotted: guild&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal tools&lt;/td&gt;
&lt;td&gt;platform self-serve&lt;/td&gt;
&lt;td&gt;none embedded; consumes platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Experimentation&lt;/td&gt;
&lt;td&gt;embed AE + shared framework&lt;/td&gt;
&lt;td&gt;solid: experimentation; dotted: guild&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Do not apply one structural template to every domain. Embed where data is complex, fast-moving, and domain-specific; use platform self-serve where needs are shared and light; and never embed a lone engineer into a domain with no paved road to build on. Match the structure to the domain's complexity and cadence, and always pave before you embed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on embedded data engineers
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your company embedded data engineers into six product squads a year ago. Delivery got faster, but now there are six orchestration setups, three definitions of revenue, embedded engineers say they feel isolated and two just quit for 'a real data team,' and leadership wants to yank everyone back to central. Design the reporting and coordination model that keeps the delivery speed but fixes the silos, isolation, and attrition — and explain what you would tell leadership."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using solid-line/dotted-line reporting, a chartered guild, a paved road, and career parity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model: embedded delivery + guild coordination + platform road
=============================================================

  Embedded DE/AE in each squad
    - SOLID line -&amp;gt; squad (delivery, prioritization, accountability)
    - DOTTED line -&amp;gt; data guild (standards, craft, career)

  Data guild (all embedded engineers + platform liaison)
    - owns standards, RFCs, design review, leveling rubric
    - weekly design review; quarterly standards refresh

  Platform team
    - owns the paved road the standards ride on (one orchestrator,
      contracts, catalog, semantic layer)

  Fixes mapped to symptoms:
    6 orchestrators  -&amp;gt; platform owns tooling (RACI); migrate to one
    3 revenue defs   -&amp;gt; declare canonical `revenue` product + contract
    isolation        -&amp;gt; guild = craft community + peer review
    attrition        -&amp;gt; in-place staff/principal ladder (career parity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## What I would tell leadership&lt;/span&gt;

Do NOT re-centralize. Yanking everyone back trades the silo
problem for the bottleneck problem you left behind a year ago,
and throws away the domain speed you paid for.

The problem is not embedding; it is embedding WITHOUT the two
things that make it work:
&lt;span class="p"&gt;  1.&lt;/span&gt; A coordinating dotted line (a chartered guild).
&lt;span class="p"&gt;  2.&lt;/span&gt; A shared paved road (a platform team) so engineers build on
     common rails instead of each paving their own.

Add those two. Keep the embedding. Declare the 2-3 highest-pain
cross-domain entities canonical with contracts. Give embedded
engineers a real career ladder so the best ones stay.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Structural fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6 orchestration setups&lt;/td&gt;
&lt;td&gt;no tooling owner&lt;/td&gt;
&lt;td&gt;platform Accountable for tooling (RACI); consolidate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 revenue definitions&lt;/td&gt;
&lt;td&gt;no cross-domain semantic owner&lt;/td&gt;
&lt;td&gt;canonical &lt;code&gt;revenue&lt;/code&gt; product + contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;engineers isolated&lt;/td&gt;
&lt;td&gt;no craft community&lt;/td&gt;
&lt;td&gt;chartered guild + weekly design review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 resignations&lt;/td&gt;
&lt;td&gt;no in-place career path&lt;/td&gt;
&lt;td&gt;staff/principal ladder for embedded track&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;leadership wants re-centralize&lt;/td&gt;
&lt;td&gt;conflating "embedding" with "embedding done wrong"&lt;/td&gt;
&lt;td&gt;keep embedding, add guild + platform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the change, the six squads keep their embedded engineers and their delivery speed, but now build on one orchestrator and one paved road; revenue has a single contract-enforced definition; the guild gives isolated engineers a peer community and a design-review ritual; and the in-place career ladder removes the reason the two engineers left. Leadership gets the coherence they wanted without paying the bottleneck tax of re-centralization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Orchestration stacks&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1 (platform-owned)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue definitions&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1 (canonical, contracted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedded-engineer attrition&lt;/td&gt;
&lt;td&gt;rising&lt;/td&gt;
&lt;td&gt;stabilized (career parity)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery speed&lt;/td&gt;
&lt;td&gt;fast (kept)&lt;/td&gt;
&lt;td&gt;fast (kept)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standards coherence&lt;/td&gt;
&lt;td&gt;drifting&lt;/td&gt;
&lt;td&gt;guild-owned + contract-enforced&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Solid-line/dotted-line split&lt;/strong&gt;&lt;/strong&gt; — solid line to the squad preserves the delivery speed and accountability embedding bought; dotted line to the guild restores craft coherence. Keeping both is what distinguishes "embedding done right" from the silo failure mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Chartered guild&lt;/strong&gt;&lt;/strong&gt; — the guild converts isolated engineers into a community with design review and shared standards, directly fixing both the isolation-driven attrition and the standards drift, without a central command structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Platform paved road&lt;/strong&gt;&lt;/strong&gt; — one orchestrator and shared contracts give embedded engineers common rails, which is the only durable fix for infrastructure duplication; standards enforced by contract CI scale across all six squads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Career parity&lt;/strong&gt;&lt;/strong&gt; — an in-place staff/principal ladder removes the "leave for a real data team" incentive; retention of senior embedded engineers is what keeps domain context from resetting every 18 months.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — a guild (mostly time, not headcount), a platform team owning tooling, and a one-time orchestrator consolidation. The eliminated cost is O(squads) duplicated infrastructure, O(squads) divergent metric reconciliation, and the recurring cost of re-hiring and re-onboarding after attrition. Net: coordination cost is near-constant per squad instead of growing with fragmentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on team ownership and reporting models&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;
&lt;strong&gt;SQL problems on cross-domain metric consistency&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Analytics engineers and the dbt-era boundary
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The analytics engineer is the dbt-era bridge — owning transformation and the semantic layer between the data engineer's pipelines and the analyst's dashboards
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the &lt;code&gt;analytics engineer&lt;/code&gt; is the role the modern data stack created to fill the gap between the data engineer (who builds ingestion and infrastructure) and the data analyst (who builds dashboards and answers questions) — the AE owns the &lt;em&gt;transformation&lt;/em&gt; layer, turning raw ingested data into clean, tested, documented, business-ready models (typically in dbt) and often the semantic/metrics layer on top, applying software-engineering rigor (version control, testing, CI, modularity) to analytics work that used to live in untested SQL scripts and spreadsheet exports&lt;/strong&gt;. The AE role exists because dbt and the ELT pattern moved transformation out of the pipeline and into the warehouse, and someone with one foot in engineering and one in analytics had to own that middle. Getting the DE↔AE↔analyst boundary clear is what stops the same model being built three times or falling through the cracks entirely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpcvx7ww8c3dof4lyc5h7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpcvx7ww8c3dof4lyc5h7.jpeg" alt="Iconographic analytics engineer boundary diagram — a layered ownership stack from raw ingestion (DE) through transformation and semantic layer (AE) to dashboards (analyst), with a bridge glyph marking the AE role." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the analytics engineer actually does.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Owns transformation.&lt;/strong&gt; Takes raw, ingested tables and builds the staging, intermediate, and mart models that the business consumes. In the modern stack this is dbt: modular SQL models, refs, sources, and a DAG the AE owns end to end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Applies engineering rigor to analytics.&lt;/strong&gt; Version control, code review, tests (uniqueness, not-null, referential integrity, freshness), CI, documentation, and modularity — the practices DEs take for granted, applied to the transformation layer that analysts previously wrote as one-off scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owns the semantic/metrics layer.&lt;/strong&gt; Increasingly the AE owns the shared definitions of metrics (revenue, active users, retention) so every downstream tool computes them the same way. This is the "single source of truth for metrics" that the org relies on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bridges the vocabulary.&lt;/strong&gt; The AE speaks SQL and dbt fluently (engineering) and understands the business questions (analytics), so they translate "we need to understand churn" into a tested, documented &lt;code&gt;churn&lt;/code&gt; model the analyst can trust.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The DE ↔ AE ↔ analyst boundary — who owns which layer.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data engineer.&lt;/strong&gt; Owns ingestion, the pipeline runtime, infrastructure, orchestration, and the raw/source layer landing in the warehouse. The DE gets trustworthy raw data into the warehouse reliably and at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics engineer.&lt;/strong&gt; Owns the transformation from raw to business-ready: staging, intermediate, and mart models; tests; documentation; and typically the semantic layer. The AE turns raw data into trusted, reusable models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data analyst.&lt;/strong&gt; Owns the last mile: exploration, dashboards, ad-hoc analysis, and turning trusted models into insight and recommendations for the business. The analyst answers questions using the AE's models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data scientist.&lt;/strong&gt; Owns modelling, experimentation analysis, and ML features — often consuming the AE's marts as clean inputs rather than wrangling raw data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where the boundary blurs (and how to settle it).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DE vs AE on transformation.&lt;/strong&gt; In some orgs DEs write transformations; in the modern stack AEs do. The clean split: DEs own the pipeline and the raw layer; AEs own everything from staging onward. If your DEs are writing dbt marts, you either do not have AEs or you have mislabeled them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AE vs analyst on modelling.&lt;/strong&gt; Analysts sometimes build their own models when the AE is a bottleneck — which recreates the untested-SQL problem. The fix: AEs own reusable models; analysts do analysis on top. If analysts are maintaining production models, the AE layer is under-resourced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who owns the semantic layer.&lt;/strong&gt; The most-contested question. The strongest answer: the AE owns the semantic layer as code, in the same repo and CI as the dbt models, so metrics are versioned, tested, and consistent across every consuming tool. It is neither a BI-tool setting nor a DE responsibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The modern-data-stack ownership map.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion (Fivetran/Airbyte/custom).&lt;/strong&gt; DE / platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warehouse (Snowflake/BigQuery/Databricks).&lt;/strong&gt; DE / platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transformation (dbt).&lt;/strong&gt; Analytics engineer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic/metrics layer (dbt metrics / semantic layer / metric store).&lt;/strong&gt; Analytics engineer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BI / dashboards (Looker/Tableau/Mode).&lt;/strong&gt; Data analyst, on the semantic layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse ETL / activation.&lt;/strong&gt; DE/AE depending on org.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on analytics engineers.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What is an analytics engineer?" — required answer: the role that owns tested, documented transformation (dbt) between DE pipelines and analyst dashboards.&lt;/li&gt;
&lt;li&gt;"Who owns the semantic layer?" — strong answer: the AE, as versioned tested code alongside the dbt models.&lt;/li&gt;
&lt;li&gt;"DE vs AE vs analyst — where are the lines?" — DE owns raw + infra, AE owns transformation + semantics, analyst owns insight.&lt;/li&gt;
&lt;li&gt;"Why did the AE role emerge?" — dbt/ELT moved transformation into the warehouse; someone with engineering rigor and business context had to own it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the role-boundary matrix (DE vs AE vs DA vs DS)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single artifact that settles most "who does what" disputes in a modern data team is a role-boundary matrix mapping each stack layer and each responsibility to a role. It stops the same model being built twice and stops layers falling through the cracks. Walk through building it for a team with all four roles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layers.&lt;/strong&gt; Ingestion, raw, transformation, semantic layer, dashboards, ML features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles.&lt;/strong&gt; Data engineer, analytics engineer, data analyst, data scientist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule.&lt;/strong&gt; Each layer has exactly one owning role; adjacent roles are consulted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the role-boundary matrix and identify the two layers most likely to be contested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer / responsibility&lt;/th&gt;
&lt;th&gt;Data engineer&lt;/th&gt;
&lt;th&gt;Analytics engineer&lt;/th&gt;
&lt;th&gt;Data analyst&lt;/th&gt;
&lt;th&gt;Data scientist&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ingestion + pipeline runtime&lt;/td&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw / source layer&lt;/td&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;td&gt;Consulted&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transformation (staging → marts)&lt;/td&gt;
&lt;td&gt;Consulted&lt;/td&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;td&gt;Consulted&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic / metrics layer&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;td&gt;Consulted&lt;/td&gt;
&lt;td&gt;Consulted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboards + ad-hoc analysis&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Consulted&lt;/td&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML features + experimentation&lt;/td&gt;
&lt;td&gt;Consulted&lt;/td&gt;
&lt;td&gt;Consulted&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role-boundary rules (one owner per layer)
=========================================
DE   owns: ingestion, runtime, raw/source, infra, orchestration.
AE   owns: transformation (staging/intermediate/marts), tests,
           docs, and the semantic/metrics layer.
DA   owns: dashboards, exploration, ad-hoc analysis, insight.
DS   owns: ML features, model training, experiment analysis.

Contested layers (watch these):
  1. Transformation: DEs sometimes write marts. Rule -&amp;gt; if you have
     AEs, DEs stop at the raw layer; AEs own staging onward.
  2. Semantic layer: BI tool vs dbt vs DE. Rule -&amp;gt; AE owns it AS
     CODE (versioned, tested, in CI), not as a BI-tool setting.

Smell tests:
  - Analysts maintaining production models -&amp;gt; AE layer under-resourced.
  - DEs writing business marts -&amp;gt; AE role missing or mislabeled.
  - Metrics differ across dashboards -&amp;gt; no single semantic owner.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The matrix assigns exactly one Owner per layer, which is the whole point — ambiguous ownership is what causes the same &lt;code&gt;revenue&lt;/code&gt; model to be built by a DE, an AE, and an analyst independently. One owner per layer, adjacent roles Consulted.&lt;/li&gt;
&lt;li&gt;The DE owns everything up to and including the raw/source layer: ingestion, runtime, infra, and landing trustworthy raw data. The AE is Consulted on the raw layer because they consume it, but they do not own the pipeline.&lt;/li&gt;
&lt;li&gt;The AE owns transformation and the semantic layer — the two layers that define the modern AE role. The DE and analyst are Consulted (they produce inputs and consume outputs) but the AE is accountable for the models being tested, documented, and correct.&lt;/li&gt;
&lt;li&gt;The two contested layers are transformation and the semantic layer. Transformation is contested because DEs historically wrote it; the rule is that with AEs present, DEs stop at raw. The semantic layer is contested because it can live in the BI tool, in dbt, or with DEs; the rule is AE-owned-as-code.&lt;/li&gt;
&lt;li&gt;The smell tests turn the matrix into a diagnostic. Analysts maintaining production models means the AE layer is under-resourced; DEs writing business marts means the AE role is missing; metrics differing across dashboards means no single semantic owner. Each smell maps to a boundary the matrix would have prevented.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Boundary violated&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same metric, 3 values&lt;/td&gt;
&lt;td&gt;no single semantic owner&lt;/td&gt;
&lt;td&gt;AE owns semantic layer as code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analysts maintaining prod models&lt;/td&gt;
&lt;td&gt;AE layer under-resourced&lt;/td&gt;
&lt;td&gt;staff more AEs / clarify ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DEs writing marts&lt;/td&gt;
&lt;td&gt;AE role missing/mislabeled&lt;/td&gt;
&lt;td&gt;DEs stop at raw; hire/assign AEs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marts untested&lt;/td&gt;
&lt;td&gt;rigor not applied to transformation&lt;/td&gt;
&lt;td&gt;AE brings tests + CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML features hand-built from raw&lt;/td&gt;
&lt;td&gt;DS reinventing transformation&lt;/td&gt;
&lt;td&gt;DS consumes AE marts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Draw a role-boundary matrix with exactly one owner per stack layer, and treat the two contested layers — transformation and the semantic layer — as AE-owned by default. When metrics disagree across dashboards, you almost always have no single semantic owner; assign it to the AE as versioned, tested code and the disagreement disappears.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the layered-ownership map as text
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A role-boundary matrix says who owns each layer; a layered-ownership map shows the &lt;em&gt;flow&lt;/em&gt; — how data moves through the layers and where each role's responsibility begins and ends. Drawing it as an ASCII map makes the handoffs explicit and is a great whiteboard artifact. Walk through building it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flow.&lt;/strong&gt; Source → raw → staging → intermediate → marts → semantic layer → dashboards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handoffs.&lt;/strong&gt; The points where ownership passes from one role to the next.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contracts.&lt;/strong&gt; What each handoff guarantees.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draw the layered-ownership map from source system to dashboard and annotate each handoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Handoff guarantee to next layer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source → raw&lt;/td&gt;
&lt;td&gt;DE&lt;/td&gt;
&lt;td&gt;data landed, fresh, schema-detected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;raw → staging&lt;/td&gt;
&lt;td&gt;AE&lt;/td&gt;
&lt;td&gt;typed, cleaned, renamed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;staging → marts&lt;/td&gt;
&lt;td&gt;AE&lt;/td&gt;
&lt;td&gt;business entities, tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;marts → semantic&lt;/td&gt;
&lt;td&gt;AE&lt;/td&gt;
&lt;td&gt;metrics defined once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;semantic → dashboards&lt;/td&gt;
&lt;td&gt;DA&lt;/td&gt;
&lt;td&gt;consistent numbers everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layered ownership map: source -&amp;gt; dashboard
==========================================

  [Source systems]          Postgres, Stripe, events
        |  DE owns: ingestion, runtime, freshness
        v
  [ raw / source layer ]    warehouse.raw.*        &amp;lt;-- DE handoff:
        |                                              landed + fresh
        |  AE owns from here down -------------------------------
        v
  [ staging ]               stg_* : typed, renamed, cleaned
        |
        v
  [ intermediate ]          int_* : joins, reusable logic
        |
        v
  [ marts ]                 dim_* / fct_* : business entities, TESTED
        |
        v
  [ semantic / metrics ]    metrics defined ONCE (revenue, MAU, churn)
        |  AE handoff: one definition per metric
        |  DA owns from here -----------------------------------
        v
  [ dashboards / analysis ] Looker/Tableau/Mode -&amp;gt; insight

Handoff contracts:
  DE -&amp;gt; AE : raw is fresh (&amp;lt; SLO) and schema is registered.
  AE -&amp;gt; DA : marts are tested; metrics have one definition.
  Break a contract -&amp;gt; the downstream layer breaks; contracts
  make the blame boundary unambiguous.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The map makes the DE→AE handoff explicit: the DE owns everything down to the raw layer and guarantees it is landed and fresh. Everything below raw is the AE's territory. This single horizontal line resolves most DE-vs-AE disputes.&lt;/li&gt;
&lt;li&gt;The staging → intermediate → marts progression is the AE's dbt DAG. Staging types and renames; intermediate holds reusable join logic; marts are the tested business entities (&lt;code&gt;dim_customer&lt;/code&gt;, &lt;code&gt;fct_orders&lt;/code&gt;). The AE owns this entire chain and its tests.&lt;/li&gt;
&lt;li&gt;The marts → semantic handoff is where metrics get defined once. Instead of each dashboard computing revenue its own way, the semantic layer defines &lt;code&gt;revenue&lt;/code&gt; a single time and every consumer references it. This is the AE's highest-leverage responsibility.&lt;/li&gt;
&lt;li&gt;The semantic → dashboards handoff passes ownership to the analyst, with the guarantee that the numbers are consistent everywhere because they come from one definition. The analyst builds insight on trusted models rather than re-deriving metrics.&lt;/li&gt;
&lt;li&gt;The handoff contracts make the blame boundary unambiguous. If a dashboard is wrong, you walk the map: is raw stale (DE), is a mart failing tests (AE), or is the dashboard mis-querying (DA)? The layered map turns "the data is wrong" into a specific, ownable failure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Where on the map&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard shows stale data&lt;/td&gt;
&lt;td&gt;raw layer not fresh&lt;/td&gt;
&lt;td&gt;DE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;dim_customer&lt;/code&gt; has duplicates&lt;/td&gt;
&lt;td&gt;marts failing tests&lt;/td&gt;
&lt;td&gt;AE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two dashboards disagree on revenue&lt;/td&gt;
&lt;td&gt;no single semantic definition&lt;/td&gt;
&lt;td&gt;AE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard filters wrong&lt;/td&gt;
&lt;td&gt;dashboard query&lt;/td&gt;
&lt;td&gt;DA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML model fed raw instead of marts&lt;/td&gt;
&lt;td&gt;DS bypassing the map&lt;/td&gt;
&lt;td&gt;DS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Draw the layered-ownership map as a flow with explicit handoff contracts, and every "the data is wrong" incident becomes a walk down the map to a single owner. The two handoffs that carry the most weight are DE→AE (raw is fresh and registered) and AE→DA (marts are tested and metrics are defined once); enforce those two and the middle takes care of itself.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a tested dbt model with a semantic definition
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The clearest way to show what an AE owns is the artifact itself: a dbt model that transforms raw data into a tested business entity, plus the semantic definition of a metric built on it. This is the AE's day job made concrete. Walk through a &lt;code&gt;fct_orders&lt;/code&gt; model and a &lt;code&gt;revenue&lt;/code&gt; metric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model.&lt;/strong&gt; Transform &lt;code&gt;raw.orders&lt;/code&gt; into a clean, tested &lt;code&gt;fct_orders&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests.&lt;/strong&gt; Uniqueness, not-null, referential integrity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric.&lt;/strong&gt; Define &lt;code&gt;revenue&lt;/code&gt; once, on top of &lt;code&gt;fct_orders&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the dbt model, its tests, and the semantic metric definition an AE owns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fct_orders.sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;clean, business-ready orders fact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fct_orders.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tests + docs (the contract)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;revenue&lt;/code&gt; metric&lt;/td&gt;
&lt;td&gt;single definition of revenue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- models/marts/fct_orders.sql  (owned by the analytics engineer)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;      &lt;span class="c1"&gt;-- staging, not raw&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt;
        &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;total_cents&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;            &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;revenue_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- business unit&lt;/span&gt;
        &lt;span class="n"&gt;ordered_at&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;               &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'test'&lt;/span&gt;                              &lt;span class="c1"&gt;-- exclude test orders&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# models/marts/fct_orders.yml — the tests ARE the contract&lt;/span&gt;
&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fct_orders&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;One&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;row&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;per&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;real&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Canonical&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fact."&lt;/span&gt;
    &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_id&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;unique&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;not_null&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;        &lt;span class="c1"&gt;# no duplicate orders&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;relationships&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;               &lt;span class="c1"&gt;# referential integrity&lt;/span&gt;
              &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('dim_customer')&lt;/span&gt;
              &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;

&lt;span class="c1"&gt;# semantic layer: revenue defined ONCE for every consumer&lt;/span&gt;
&lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revenue&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(USD)"&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('fct_orders')&lt;/span&gt;
    &lt;span class="na"&gt;calculation_method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum&lt;/span&gt;
    &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue_usd&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;order_date&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The model reads from &lt;code&gt;stg_orders&lt;/code&gt; (staging), not &lt;code&gt;raw.orders&lt;/code&gt;. This encodes the layering: the DE lands raw, the AE's staging layer cleans and types it, and the mart builds business entities on staging. The AE never reaches into raw directly from a mart.&lt;/li&gt;
&lt;li&gt;The transformation applies business logic the analyst should not have to re-derive: converting cents to dollars, excluding test orders, and casting to a date. Doing this once in the mart means every downstream consumer gets consistent &lt;code&gt;revenue_usd&lt;/code&gt;, not fifteen slightly different conversions.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;.yml&lt;/code&gt; tests are the contract, enforced in dbt's CI. &lt;code&gt;unique&lt;/code&gt; + &lt;code&gt;not_null&lt;/code&gt; on &lt;code&gt;order_id&lt;/code&gt; guarantees no double-counted orders; the &lt;code&gt;relationships&lt;/code&gt; test guarantees every order references a real customer. A change that breaks these fails the build — the same contract-enforcement pattern the platform uses, applied at the model level.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;metrics&lt;/code&gt; block defines &lt;code&gt;revenue&lt;/code&gt; exactly once, as &lt;code&gt;sum(revenue_usd)&lt;/code&gt; over &lt;code&gt;fct_orders&lt;/code&gt;. Every BI tool and every analyst references this metric rather than writing their own SUM, which is how the semantic layer guarantees consistent numbers everywhere.&lt;/li&gt;
&lt;li&gt;This single pair of files is the AE role in miniature: engineering rigor (version control, tests, CI, modularity via &lt;code&gt;ref&lt;/code&gt;) applied to analytics, producing a tested business entity and a single-definition metric that the analyst builds insight on and the DE never has to touch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;th&gt;What they get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Analyst in Looker&lt;/td&gt;
&lt;td&gt;references &lt;code&gt;revenue&lt;/code&gt; metric — consistent by construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Another AE's model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ref('fct_orders')&lt;/code&gt; — reuses the tested fact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt CI&lt;/td&gt;
&lt;td&gt;fails build if &lt;code&gt;order_id&lt;/code&gt; dupes or FK breaks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data scientist&lt;/td&gt;
&lt;td&gt;clean &lt;code&gt;fct_orders&lt;/code&gt; as an ML input, not raw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auditor&lt;/td&gt;
&lt;td&gt;docs + tests describe the contract&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The AE's deliverable is a tested, documented, single-definition model — not a query. If your metrics live as ad-hoc SUMs in dashboards instead of as versioned semantic definitions on tested marts, you do not have an analytics-engineering layer; you have analysts re-deriving numbers and the inconsistency that follows. Define each metric once, test the model under it, and let CI enforce the contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the analytics engineer boundary
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your company has data engineers and data analysts but no analytics engineers. Symptoms: DEs are swamped writing business transformations they do not have context for, analysts maintain a tangle of untested SQL that produces three different revenue numbers, and nobody owns the semantic layer. Make the case for introducing the analytics-engineering function, define its boundary with DEs and analysts, and describe how you would stand it up and who owns the semantic layer."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a defined AE function, a role-boundary contract, and an AE-owned semantic layer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where AE fits (the missing middle)
==================================

  Data Engineer      -&amp;gt; ingestion, runtime, raw/source layer, infra
        | handoff: raw is fresh + registered
  Analytics Engineer -&amp;gt; staging -&amp;gt; marts (dbt, TESTED) + semantic layer
        | handoff: marts tested; every metric defined once
  Data Analyst       -&amp;gt; dashboards, exploration, insight

Problem today: no AE -&amp;gt; the middle is done by whoever is free
  -&amp;gt; DEs write marts without domain context (swamped + wrong)
  -&amp;gt; analysts write untested SQL (3 revenue numbers)
  -&amp;gt; semantic layer is nobody's -&amp;gt; inconsistency by default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Stand-up plan for the AE function&lt;/span&gt;

&lt;span class="gu"&gt;### Case&lt;/span&gt;
The transformation + semantic middle is currently orphaned.
DEs and analysts are both doing it badly because it is neither's
core role. An AE function owns it with engineering rigor.

&lt;span class="gu"&gt;### How to stand it up&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Reskill/hire: promote SQL-strong analysts into AE roles or
   hire AEs; they need SQL + dbt + business context.
&lt;span class="p"&gt;2.&lt;/span&gt; Move all business transformations OUT of DE pipelines and
   OUT of analyst scripts INTO a dbt project the AEs own.
&lt;span class="p"&gt;3.&lt;/span&gt; Add tests (unique, not_null, relationships, freshness) to
   every mart -&amp;gt; the tests are the contract, enforced in CI.
&lt;span class="p"&gt;4.&lt;/span&gt; Declare the semantic layer AE-owned, as code, in the dbt repo.
   Define &lt;span class="sb"&gt;`revenue`&lt;/span&gt; ONCE. All BI tools reference it.

&lt;span class="gu"&gt;### Boundary&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; DE stops at raw/source. AE owns staging onward + semantics.
&lt;span class="p"&gt;-&lt;/span&gt; Analysts do analysis on AE models; they do NOT maintain
  production models.

&lt;span class="gu"&gt;### Who owns the semantic layer&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; The analytics engineer, as versioned + tested code alongside
  the dbt models. NOT a BI-tool setting, NOT the DE.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Before (no AE)&lt;/th&gt;
&lt;th&gt;After (AE function)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Business transformation&lt;/td&gt;
&lt;td&gt;DEs (no context) + analysts (no rigor)&lt;/td&gt;
&lt;td&gt;AEs (context + rigor) in dbt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;none on transformations&lt;/td&gt;
&lt;td&gt;unique/not_null/relationships in CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue definition&lt;/td&gt;
&lt;td&gt;3 different numbers&lt;/td&gt;
&lt;td&gt;1 semantic definition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DE workload&lt;/td&gt;
&lt;td&gt;swamped with marts&lt;/td&gt;
&lt;td&gt;back to pipelines + infra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyst workload&lt;/td&gt;
&lt;td&gt;maintaining SQL tangle&lt;/td&gt;
&lt;td&gt;analysis on trusted models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic layer owner&lt;/td&gt;
&lt;td&gt;nobody&lt;/td&gt;
&lt;td&gt;AE, as code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After introducing the AE function, business transformations move into a tested dbt project the AEs own; DEs return to pipelines and infrastructure where their skills fit; analysts stop maintaining production SQL and do analysis on trusted models; and the semantic layer gets a single owner, collapsing three revenue numbers into one contract-enforced definition. The boundary is unambiguous: DE owns raw and below, AE owns transformation and semantics, analyst owns insight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Revenue definitions&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1 (semantic layer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tested transformations&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;high (CI-enforced)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DE time on business logic&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;low (back to platform)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyst time maintaining models&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;low (analysis only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic-layer ownership&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;AE, versioned + tested&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;The missing middle&lt;/strong&gt;&lt;/strong&gt; — the AE fills the transformation + semantic gap that neither DEs (lack domain context) nor analysts (lack engineering rigor) own well. Naming the role assigns the orphaned middle to someone whose job it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Engineering rigor on analytics&lt;/strong&gt;&lt;/strong&gt; — version control, tests, CI, and modularity turn the untested-SQL tangle into a tested dbt project; the tests are the contract that stops silent regressions and the three-revenue-numbers problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;AE-owned semantic layer as code&lt;/strong&gt;&lt;/strong&gt; — defining each metric once, in the repo, under CI, is what makes numbers consistent across every tool; putting it in a BI-tool setting or on the DE recreates the inconsistency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Clean role boundary&lt;/strong&gt;&lt;/strong&gt; — DE stops at raw, AE owns the middle, analyst owns insight; the boundary frees DEs to return to infrastructure and analysts to return to analysis, so every role does what it is best at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — reskilling analysts or hiring AEs, plus standing up a dbt project with CI. The eliminated cost is O(dashboards) re-derived metrics, swamped DEs doing low-context transformation, and the recurring firefighting of inconsistent numbers. Net: transformation cost is paid once per model instead of re-paid per consumer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Dimensional Modeling&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — dimensional-modeling&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Dimensional modeling problems on facts, dims, and marts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/dimensional-modeling" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;
&lt;strong&gt;SQL problems on transformation and semantic-layer logic&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Scaling the org: topologies, data mesh and anti-patterns
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Scale a data org with Team-Topologies mapping and deliberate data-mesh adoption — and fix the anti-patterns before they calcify
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;scaling a &lt;code&gt;data team org&lt;/code&gt; past ~40 people is a topology problem — you map the data function onto the four Team-Topologies team types (stream-aligned, platform, enabling, complicated-subsystem), decide deliberately whether and how far to adopt &lt;code&gt;data mesh&lt;/code&gt; (domain-owned data products with federated governance), size the org with sane headcount ratios (platform : embedded : analytics engineers), and actively remediate the anti-patterns (central bottleneck, data silos, ungoverned mesh, hero dependency) before they harden into permanent structure&lt;/strong&gt;. There is no single right topology — there is the topology that matches your scale, your domain complexity, and your governance maturity, plus a discipline of catching anti-patterns while they are still cheap to fix.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohoabanby8tjspi6nwqd.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohoabanby8tjspi6nwqd.jpeg" alt="Iconographic scaling diagram — Team-Topologies mapping of stream-aligned, platform, enabling, and complicated-subsystem teams onto a data org, with a data-mesh domain grid and an anti-pattern warning strip." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team Topologies mapped onto data.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stream-aligned teams.&lt;/strong&gt; The domain/product squads with embedded DEs and AEs, aligned to a flow of business value (checkout, growth, logistics). They own their data products end to end and are the primary delivery teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform teams.&lt;/strong&gt; The central data platform team providing the paved road as an internal product, so stream-aligned teams self-serve. Reduces the cognitive load on domain teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enabling teams.&lt;/strong&gt; A small, time-boxed team that helps stream-aligned teams adopt new capabilities (a new semantic layer, a migration to dbt), then steps back. Enabling teams &lt;em&gt;coach&lt;/em&gt;; they do not do the work permanently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complicated-subsystem teams.&lt;/strong&gt; A specialist team for a genuinely hard shared subsystem (a real-time streaming platform, an ML feature store, a complex entity-resolution service) that needs deep expertise most domains lack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The interaction modes.&lt;/strong&gt; Team Topologies also defines &lt;em&gt;how&lt;/em&gt; teams interact: collaboration (temporary, close), X-as-a-Service (the platform model — clean interface, low coupling), and facilitating (enabling teams coaching). The platform-to-stream relationship should be X-as-a-Service; anything else means the platform is a bottleneck.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Data mesh — the four principles and the honest trade-offs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain ownership.&lt;/strong&gt; Each domain owns its data as a product, end to end. Powerful at scale; premature below ~40–50 data people because you lack the domains and the platform maturity to support it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data as a product.&lt;/strong&gt; Domains publish discoverable, trustworthy, documented data products with SLAs — not raw dumps. This is the principle even non-mesh orgs should adopt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-serve data platform.&lt;/strong&gt; Domains cannot own products without a strong platform to build on — mesh &lt;em&gt;requires&lt;/em&gt; a mature platform team first. Mesh without a platform is just silos with a rebrand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Federated computational governance.&lt;/strong&gt; Global standards (interoperability, security, canonical entities) enforced computationally (contracts, CI) while domains retain autonomy. This is the hardest principle and the one that separates a real mesh from an ungoverned sprawl.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The honest trade-off.&lt;/strong&gt; Data mesh trades central control for domain autonomy and scalability. It pays off at high scale with many domains and a mature platform; it is an expensive mistake at small scale, where it fragments a team that centralization would serve better.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Headcount ratios — sane starting points, not laws.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform : embedded engineers.&lt;/strong&gt; Roughly 1 platform engineer per 4–6 embedded engineers is a common starting ratio — enough platform to keep the road paved without over-centralizing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DE : AE.&lt;/strong&gt; Often 1:1 to 2:1 embedded DE to AE in analytics-heavy domains; DE-heavy in infrastructure-heavy domains. The AE count grows as the transformation/semantic surface grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineers : analysts.&lt;/strong&gt; Highly variable; a company with many business stakeholders runs more analysts per engineer. The AE layer is what lets a few engineers support many analysts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The caveat.&lt;/strong&gt; Ratios are diagnostics, not targets. A platform team that is 1:20 is under-invested (bottleneck incoming); 1:2 is over-invested (re-centralizing). Use the ratio to spot drift, not to set headcount mechanically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The anti-pattern catalogue — and the fix for each.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Central bottleneck.&lt;/strong&gt; Everything queues behind the central team. Fix: self-serve paved road + embedding. (Section 2 and 3.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data silos.&lt;/strong&gt; Embedded teams diverge, no shared substrate. Fix: platform + guild + contracts. (Section 3.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ungoverned mesh.&lt;/strong&gt; Domain ownership without federated governance → a thousand undiscoverable data products. Fix: federated computational governance (catalog + contracts + interoperability standards) before scaling domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hero dependency.&lt;/strong&gt; One person holds critical undocumented knowledge; the org grinds when they are out. Fix: documentation, on-call rotation, pairing, and bus-factor &amp;gt; 1 as a standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Premature mesh.&lt;/strong&gt; Adopting mesh at 20 people. Fix: centralize or hybrid until you have the domains and platform maturity mesh requires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform ivory tower.&lt;/strong&gt; Platform builds what it finds interesting, not what domains need. Fix: platform-as-product with user research and adoption metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on scaling.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How would you map a data org onto Team Topologies?" — stream-aligned domains, platform for the road, enabling teams to coach, complicated-subsystem for hard specialties.&lt;/li&gt;
&lt;li&gt;"When should we adopt data mesh?" — when you have many domains, a mature platform, and can afford federated governance; not before.&lt;/li&gt;
&lt;li&gt;"What are the ideal data-team ratios?" — ~1 platform per 4–6 embedded; DE:AE 1:1–2:1; use as diagnostics not targets.&lt;/li&gt;
&lt;li&gt;"How do you fix a data team hitting a scaling wall?" — diagnose the anti-pattern, map to topology, remediate structurally.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — mapping a data org onto Team Topologies
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most senior scaling artifact is a Team-Topologies map of the data function: which teams are stream-aligned, which is the platform, where an enabling team is needed, and which specialist subsystem warrants its own team — plus the interaction modes between them. Walk through mapping a 60-person data org.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Teams.&lt;/strong&gt; Six domain squads, one platform team, one streaming specialist team, plus a migration need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Types.&lt;/strong&gt; Assign each to a topology type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactions.&lt;/strong&gt; Define how they interact (X-as-a-Service, collaboration, facilitating).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Map the 60-person data org onto Team Topologies and specify the interaction modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team&lt;/th&gt;
&lt;th&gt;Topology type&lt;/th&gt;
&lt;th&gt;Interaction with others&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6 domain squads (DE+AE embedded)&lt;/td&gt;
&lt;td&gt;stream-aligned&lt;/td&gt;
&lt;td&gt;consume platform X-as-a-Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data platform team&lt;/td&gt;
&lt;td&gt;platform&lt;/td&gt;
&lt;td&gt;X-as-a-Service to domains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming/real-time team&lt;/td&gt;
&lt;td&gt;complicated-subsystem&lt;/td&gt;
&lt;td&gt;X-as-a-Service + collaboration when needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt-migration team (temporary)&lt;/td&gt;
&lt;td&gt;enabling&lt;/td&gt;
&lt;td&gt;facilitating, then dissolves&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Team-Topologies map: 60-person data org
========================================

  [ Enabling: dbt-migration squad ]  (time-boxed)
        : facilitating (coaches domains onto dbt, then dissolves)
        v
  [ Stream-aligned: 6 domain squads ]  &amp;lt;-- primary delivery
     checkout | growth | logistics | payments | support | catalog
        ^                     ^
        | X-as-a-Service      | X-as-a-Service (+collab when hard)
        |                     |
  [ Platform: data platform ] [ Complicated-subsystem: streaming ]
   paved road, catalog,        real-time / feature store —
   contracts, semantic layer   deep expertise domains lack

Interaction rules:
  - Platform -&amp;gt; domains: X-as-a-Service ONLY. If it turns into
    collaboration/ticketing, the platform is becoming a bottleneck.
  - Streaming team -&amp;gt; domains: X-as-a-Service, with short bursts of
    collaboration for a new hard use case.
  - Enabling team: facilitating, explicitly time-boxed, dissolves
    after the migration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The six domain squads are stream-aligned teams — the primary delivery units, each aligned to a flow of business value with embedded DEs and AEs. In Team Topologies, most teams should be stream-aligned; the others exist to support them.&lt;/li&gt;
&lt;li&gt;The data platform team is a platform team whose entire job is to reduce the domains' cognitive load via X-as-a-Service. The critical rule: the interaction mode must be X-as-a-Service (clean self-serve interface), not collaboration. If domains have to collaborate closely with the platform for routine work, the platform has become a bottleneck.&lt;/li&gt;
&lt;li&gt;The streaming/real-time team is a complicated-subsystem team — a genuinely hard specialty (exactly-once streaming, feature stores) that most domains cannot staff. It exists because forcing every domain to master streaming would be wasteful; centralizing the hard part is correct.&lt;/li&gt;
&lt;li&gt;The dbt-migration team is an enabling team, explicitly time-boxed. It coaches domains onto dbt (facilitating interaction) and then &lt;em&gt;dissolves&lt;/em&gt;. The anti-pattern is an enabling team that becomes permanent — that means it started doing the work instead of teaching it.&lt;/li&gt;
&lt;li&gt;The interaction modes are the real content of the map. X-as-a-Service from platform to domains is the target state; collaboration should be temporary and intentional; facilitating is for enabling teams. Getting the interaction mode wrong (e.g. platform stuck in collaboration) is how a correctly-typed team still becomes a bottleneck.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Interaction&lt;/th&gt;
&lt;th&gt;Health check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Domain squads&lt;/td&gt;
&lt;td&gt;stream-aligned&lt;/td&gt;
&lt;td&gt;consume platform as a service&lt;/td&gt;
&lt;td&gt;shipping without waiting?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;platform&lt;/td&gt;
&lt;td&gt;X-as-a-Service&lt;/td&gt;
&lt;td&gt;self-serve rate high?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;complicated-subsystem&lt;/td&gt;
&lt;td&gt;X-as-a-Service + collab&lt;/td&gt;
&lt;td&gt;domains not rebuilding streaming?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt-migration&lt;/td&gt;
&lt;td&gt;enabling&lt;/td&gt;
&lt;td&gt;facilitating (time-boxed)&lt;/td&gt;
&lt;td&gt;on track to dissolve?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Map your data org onto the four Team-Topologies types, but spend your attention on the interaction modes, not the labels. The single most important rule is that platform-to-domain must be X-as-a-Service; the moment it becomes collaboration or ticketing, a correctly-structured platform team has silently become the bottleneck you built it to prevent.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a headcount-ratio staffing model
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; When leadership asks "how many people do we need and in what mix," the answer is a staffing model built from ratios and domain count, not a gut number. It makes the ask defensible and surfaces where the org is under- or over-invested. Walk through building one for a company scaling from 6 to 10 domains.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inputs.&lt;/strong&gt; Number of domains, complexity per domain, target platform ratio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ratios.&lt;/strong&gt; Platform : embedded, DE : AE, engineers : analysts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output.&lt;/strong&gt; A headcount plan with a rationale per line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build a staffing model for a 10-domain org using sane ratios and flag any imbalance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Rule of thumb&lt;/th&gt;
&lt;th&gt;10-domain sizing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Embedded DE&lt;/td&gt;
&lt;td&gt;1–2 per domain&lt;/td&gt;
&lt;td&gt;~12 (heavier domains get 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics engineer&lt;/td&gt;
&lt;td&gt;~1 per domain (more if analytics-heavy)&lt;/td&gt;
&lt;td&gt;~10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform engineer&lt;/td&gt;
&lt;td&gt;1 per 4–6 embedded&lt;/td&gt;
&lt;td&gt;~4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analysts&lt;/td&gt;
&lt;td&gt;domain-driven&lt;/td&gt;
&lt;td&gt;~15 (business-facing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Specialist (streaming/ML)&lt;/td&gt;
&lt;td&gt;1 shared team&lt;/td&gt;
&lt;td&gt;~4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Staffing model from ratios + domain count (defensible headcount ask)
&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;avg_de_per_dom&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;       &lt;span class="c1"&gt;# some domains need 2
&lt;/span&gt;&lt;span class="n"&gt;ae_per_domain&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;span class="n"&gt;platform_ratio&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;         &lt;span class="c1"&gt;# 1 platform eng per 5 embedded eng
&lt;/span&gt;&lt;span class="n"&gt;analysts&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;        &lt;span class="c1"&gt;# business-driven, not ratio-driven
&lt;/span&gt;&lt;span class="n"&gt;specialist_team&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;         &lt;span class="c1"&gt;# streaming + ML feature store
&lt;/span&gt;
&lt;span class="n"&gt;embedded_de&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;avg_de_per_dom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# 12
&lt;/span&gt;&lt;span class="n"&gt;embedded_ae&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ae_per_domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# 10
&lt;/span&gt;&lt;span class="n"&gt;embedded&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedded_de&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;embedded_ae&lt;/span&gt;            &lt;span class="c1"&gt;# 22
&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedded&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;platform_ratio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# ~4
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedded DE : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;embedded_de&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analytics eng: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;embedded_ae&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform    : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;   (ratio 1:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;platform_ratio&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analysts    : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;analysts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;specialists : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;specialist_team&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TOTAL       : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;embedded_de&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;embedded_ae&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;analysts&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;specialist_team&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Imbalance check
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;embedded&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WARN: platform under-invested -&amp;gt; bottleneck risk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;embedded&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WARN: platform over-invested -&amp;gt; re-centralizing risk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The model starts from domain count and complexity, not a target headcount. Ten domains at ~1.2 DEs each gives ~12 embedded DEs; heavier domains (payments, streaming-adjacent) justify two while lighter ones share. Sizing from domains makes the number defensible.&lt;/li&gt;
&lt;li&gt;Analytics engineers are sized ~1 per domain, scaled up where the domain is analytics-heavy (growth, finance). The AE count tracks the transformation and semantic surface area, which is what lets a modest analyst count serve many stakeholders.&lt;/li&gt;
&lt;li&gt;The platform team is derived from the embedded count via the 1:5 ratio — ~4 platform engineers for ~22 embedded. This keeps the road paved without over-centralizing. The ratio is the control that prevents both under- and over-investment.&lt;/li&gt;
&lt;li&gt;Analysts are sized by business demand, not by a ratio to engineers, because analyst need is driven by the number of business questions and stakeholders, not by pipeline count. The AE layer is what keeps this number from exploding.&lt;/li&gt;
&lt;li&gt;The imbalance checks turn the model into a diagnostic. If the platform falls below 1:8 of embedded, the code warns of bottleneck risk; above 1:3, it warns of re-centralizing. This is how the ratio is used correctly — to detect drift, not to mechanically set headcount.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Embedded DE&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;~1.2 per domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics engineer&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;~1 per domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform engineer&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1:5 of embedded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analysts&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;business demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Specialists&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;shared streaming/ML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;defensible from ratios&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Build the staffing ask from domain count and ratios, then use the ratios as a drift alarm, not a target. A platform team below ~1:8 of embedded engineers is a bottleneck forming; above ~1:3 it is re-centralizing. Size analysts from business demand and let the analytics-engineering layer be the multiplier that keeps that number sane.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — anti-pattern remediation
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The final scaling skill is recognizing an anti-pattern from its symptoms and applying the structural fix rather than a band-aid. Each anti-pattern has a signature and a known remedy. Walk through diagnosing and fixing four common ones in a scaling org.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signatures.&lt;/strong&gt; The observable symptoms of each anti-pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause.&lt;/strong&gt; The structural gap behind each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; The remediation, plus what &lt;em&gt;not&lt;/em&gt; to do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Diagnose four anti-patterns from their symptoms and prescribe the structural fix for each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Anti-pattern&lt;/th&gt;
&lt;th&gt;Signature symptom&lt;/th&gt;
&lt;th&gt;Structural fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Central bottleneck&lt;/td&gt;
&lt;td&gt;everything behind one queue&lt;/td&gt;
&lt;td&gt;self-serve road + embedding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data silos&lt;/td&gt;
&lt;td&gt;N stacks, divergent metrics&lt;/td&gt;
&lt;td&gt;platform + guild + contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ungoverned mesh&lt;/td&gt;
&lt;td&gt;1000 undiscoverable products&lt;/td&gt;
&lt;td&gt;federated computational governance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hero dependency&lt;/td&gt;
&lt;td&gt;org stalls when X is on leave&lt;/td&gt;
&lt;td&gt;docs + rotation + bus factor &amp;gt; 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Anti-pattern remediation runbook
================================

1) CENTRAL BOTTLENECK
   Symptom : lead time = weeks; central team backlog grows.
   Root    : services-desk platform, no self-serve.
   Fix     : paved-road self-serve + embed into domains.
   NOT     : add headcount to the queue (queue returns).

2) DATA SILOS
   Symptom : 6 orchestrators, 3 revenue definitions.
   Root    : embedding without platform/guild/contracts.
   Fix     : platform owns tooling; guild owns standards;
             canonical data products with contract-CI.
   NOT     : re-centralize everything (loses domain speed).

3) UNGOVERNED MESH
   Symptom : hundreds of data products, none discoverable,
             no interop, duplicated entities.
   Root    : domain ownership WITHOUT federated governance.
   Fix     : catalog + interop standards + contract-CI +
             canonical shared entities BEFORE more domains.
   NOT     : abandon mesh and re-centralize (throws away scale).

4) HERO DEPENDENCY
   Symptom : critical pipeline only one person understands.
   Root    : no docs, no rotation, bus factor = 1.
   Fix     : document, pair, on-call rotation, ownership in the
             catalog, bus factor &amp;gt; 1 as a standard.
   NOT     : give the hero a retention bonus and hope.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The central bottleneck's signature is week-long lead times and a growing central backlog. The root is a services-desk operating model; the fix is a self-serve paved road plus embedding. The wrong move — adding headcount to the queue — treats the symptom, and the queue regrows to the new capacity.&lt;/li&gt;
&lt;li&gt;Data silos show up as multiple orchestrators and divergent metric definitions. The root is embedding without the coordinating structures. The fix is platform-owned tooling, a guild for standards, and canonical data products with contract CI. The wrong move is full re-centralization, which sacrifices the domain speed embedding bought.&lt;/li&gt;
&lt;li&gt;Ungoverned mesh is the failure specific to premature or under-governed mesh: hundreds of undiscoverable, non-interoperable data products. The root is domain ownership without federated computational governance. The fix is a catalog, interoperability standards, and contract CI &lt;em&gt;before&lt;/em&gt; scaling more domains. The wrong move is abandoning mesh entirely, throwing away the scalability it offers.&lt;/li&gt;
&lt;li&gt;Hero dependency is a bus-factor-of-one problem: a critical pipeline only one person understands. The root is missing documentation, rotation, and shared ownership. The fix is documentation, pairing, on-call rotation, and cataloged ownership so bus factor exceeds one. The wrong move is a retention bonus, which pays to preserve the risk instead of removing it.&lt;/li&gt;
&lt;li&gt;Across all four, the pattern is identical: match the symptom to the anti-pattern, fix the &lt;em&gt;structural&lt;/em&gt; root, and refuse the band-aid that treats the symptom. Anti-patterns caught early are cheap structural adjustments; caught late they are calcified reorgs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Anti-pattern&lt;/th&gt;
&lt;th&gt;Band-aid (wrong)&lt;/th&gt;
&lt;th&gt;Structural fix (right)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Central bottleneck&lt;/td&gt;
&lt;td&gt;more headcount&lt;/td&gt;
&lt;td&gt;self-serve + embed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data silos&lt;/td&gt;
&lt;td&gt;re-centralize&lt;/td&gt;
&lt;td&gt;platform + guild + contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ungoverned mesh&lt;/td&gt;
&lt;td&gt;abandon mesh&lt;/td&gt;
&lt;td&gt;federated governance first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hero dependency&lt;/td&gt;
&lt;td&gt;retention bonus&lt;/td&gt;
&lt;td&gt;docs + rotation + bus factor &amp;gt; 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every data-org anti-pattern has a signature symptom, a structural root, and a band-aid that will tempt you. Diagnose to the root, apply the structural fix, and refuse the band-aid — more headcount for a bottleneck, re-centralization for silos, or a retention bonus for a hero all treat symptoms while the root calcifies. Catch anti-patterns while they are still cheap structural adjustments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on scaling a data team
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your data org has grown to 70 people organized as one big central team plus a few analysts scattered in business units. Delivery has slowed to a crawl, the central team is a bottleneck, business units are hiring shadow analysts and building their own pipelines, and the CTO is asking whether you should 'do data mesh.' Design the target topology, decide how far to go toward data mesh, give a staffing model, and sequence the transition without a big-bang reorg."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a Team-Topologies target, deliberate partial mesh, a ratio-based staffing model, and an incremental transition
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Target topology (Team Topologies)
=================================
  Stream-aligned : domain squads (embedded DE+AE), aligned to
                   business value; own their data products.
  Platform       : central data platform as X-as-a-Service (road).
  Enabling       : time-boxed team to coach domains onto the road.
  Complicated-   : streaming/ML feature-store specialists.
  subsystem

How far toward data mesh:
  - Adopt "data as a product" + "self-serve platform" NOW.
  - Adopt "domain ownership" INCREMENTALLY (domain by domain).
  - Adopt "federated computational governance" IN LOCKSTEP with
    domain ownership (catalog + contracts + interop standards).
  - This is a PARTIAL, deliberate mesh — not a big-bang.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Staffing model (70 people, ~8 domains)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Platform: 8   (1 : ~5 embedded)
&lt;span class="p"&gt;-&lt;/span&gt; Embedded DE: 12, Analytics eng: 10   (~22 embedded)
&lt;span class="p"&gt;-&lt;/span&gt; Specialists (streaming/ML): 5
&lt;span class="p"&gt;-&lt;/span&gt; Analysts: 18 (business-driven, folded IN from shadow hires)
&lt;span class="p"&gt;-&lt;/span&gt; Enabling: 3 (time-boxed) + leadership/leads: rest

&lt;span class="gu"&gt;## Transition (no big-bang; 3 quarters)&lt;/span&gt;
Q1 — Pave + legitimize
&lt;span class="p"&gt;  -&lt;/span&gt; Carve platform team from central; ship self-serve road + catalog.
&lt;span class="p"&gt;  -&lt;/span&gt; Bring shadow analysts into the org (fold, don't fight) with a
    dotted line to the guild.
Q2 — Embed + productize
&lt;span class="p"&gt;  -&lt;/span&gt; Embed DE+AE into top-3 domains; migrate them onto the road.
&lt;span class="p"&gt;  -&lt;/span&gt; Declare canonical entities (customer, revenue) with contract-CI.
&lt;span class="p"&gt;  -&lt;/span&gt; Enabling team coaches each domain, then rotates on.
Q3 — Federate + govern
&lt;span class="p"&gt;  -&lt;/span&gt; Extend domain ownership to remaining domains (partial mesh).
&lt;span class="p"&gt;  -&lt;/span&gt; Turn on federated governance: interop standards, catalog
    coverage SLO, contract-CI everywhere. Enabling team dissolves.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Structure&lt;/td&gt;
&lt;td&gt;one central team + scattered analysts&lt;/td&gt;
&lt;td&gt;stream-aligned domains + platform + enabling + specialists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery&lt;/td&gt;
&lt;td&gt;bottlenecked&lt;/td&gt;
&lt;td&gt;self-serve road + embedded pods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shadow IT&lt;/td&gt;
&lt;td&gt;business units building pipelines&lt;/td&gt;
&lt;td&gt;folded into domains on the road&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mesh stance&lt;/td&gt;
&lt;td&gt;"should we do mesh?"&lt;/td&gt;
&lt;td&gt;deliberate partial mesh, governance in lockstep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;central + overloaded&lt;/td&gt;
&lt;td&gt;federated computational (catalog + contracts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transition&lt;/td&gt;
&lt;td&gt;risk of big-bang reorg&lt;/td&gt;
&lt;td&gt;incremental, 3-quarter sequence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the transition, the central bottleneck is replaced by stream-aligned domain squads building on a platform delivered X-as-a-Service; the shadow analysts are legitimized inside domains rather than fought; and data mesh is adopted deliberately and partially — "data as a product" and "self-serve platform" immediately, "domain ownership" and "federated governance" together, domain by domain, never as a big-bang. Governance scales computationally through the catalog and contract CI rather than through an overloaded central review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delivery lead time&lt;/td&gt;
&lt;td&gt;weeks&lt;/td&gt;
&lt;td&gt;days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shadow pipelines&lt;/td&gt;
&lt;td&gt;proliferating&lt;/td&gt;
&lt;td&gt;folded onto the road&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance model&lt;/td&gt;
&lt;td&gt;central, overloaded&lt;/td&gt;
&lt;td&gt;federated + computational&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mesh adoption&lt;/td&gt;
&lt;td&gt;undecided / risky&lt;/td&gt;
&lt;td&gt;deliberate, incremental, governed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reorg risk&lt;/td&gt;
&lt;td&gt;big-bang&lt;/td&gt;
&lt;td&gt;3-quarter incremental&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Team-Topologies target&lt;/strong&gt;&lt;/strong&gt; — mapping to stream-aligned + platform + enabling + complicated-subsystem gives each team a clear type and interaction mode; platform-as-X-as-a-Service is what breaks the bottleneck structurally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Deliberate partial mesh&lt;/strong&gt;&lt;/strong&gt; — adopting data-as-a-product and self-serve now, but domain ownership and federated governance in lockstep and incrementally, captures mesh's scalability while avoiding the ungoverned-mesh anti-pattern that premature adoption causes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Fold the shadow IT&lt;/strong&gt;&lt;/strong&gt; — legitimizing shadow analysts inside domains on the paved road converts a governance problem into embedded capacity, instead of fighting a losing battle against local demand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Ratio-based staffing&lt;/strong&gt;&lt;/strong&gt; — sizing platform at ~1:5 of embedded and analysts from business demand makes the headcount ask defensible and keeps the platform from becoming either a bottleneck or an ivory tower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Incremental transition&lt;/strong&gt;&lt;/strong&gt; — a 3-quarter, domain-by-domain sequence with a time-boxed enabling team avoids the big-bang reorg that stalls delivery for two quarters; each domain is migrated and governed before the next starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — a platform team, an enabling team (time-boxed), and the governance substrate (catalog + contract CI). The eliminated cost is the compounding O(domains²) reconciliation of an ungoverned mesh, the perpetual central-queue tax, and the shadow-IT duplication. Net: coordination and governance cost grow near-linearly with domains under federated computational governance, instead of exploding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on scaling data platforms and mesh&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;Streaming&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — streaming&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Streaming problems for the complicated-subsystem team&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — data-team-structure recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three archetypes, one decision rule.&lt;/strong&gt; Centralized = strong standards, bottleneck failure mode. Decentralized/embedded = fast + domain-accurate, silo failure mode. Hybrid (platform + embedded) = paved road plus domain speed, fails only if the platform re-centralizes. Diagnose your current failure mode first, then pick the archetype whose failure mode you can afford. Never copy another company's org chart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conway's Law is the mechanism.&lt;/strong&gt; Your pipelines, schemas, and ownership boundaries &lt;em&gt;will&lt;/em&gt; mirror your communication structure. To get one canonical &lt;code&gt;customer&lt;/code&gt; product, create one owning team plus a versioned, test-enforced contract and a real consumer channel. If you cannot name the single owner, you have a fork waiting to happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform-as-product checklist.&lt;/strong&gt; Users are engineers; golden path over bespoke; roadmap not backlog; self-serve is the goal; publish SLAs/SLOs with the query that measures them; run on-call for the substrate; state an explicit anti-goal ("we are NOT a request desk"). Measure success by adoption and self-serve rate, never tickets closed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paved-road capability map.&lt;/strong&gt; For each capability (ingestion, orchestration, catalog, quality, semantic layer) publish the golden path, the self-serve status, and an SLO. No golden path → domains silo; no SLO → domains cannot plan. Deliver governance as guardrails ON the road (auto-PII masking, contract CI), so the compliant path is the easy path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data contract + CI gate.&lt;/strong&gt; Give the platform a data-contract primitive (schema + semantics + quality) enforced by a CI gate that fails breaking-change PRs. This scales governance at the speed of every pipeline instead of a central review queue. Breaking changes ship as a new version with a deprecation window, never in place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding: solid line + dotted line.&lt;/strong&gt; Embedded DEs/AEs report solid line to the domain (delivery, accountability) and dotted line to a guild (standards, craft, career). Solid-line-only → silos; dotted-line-only → outsiders with no domain context. The two RACI rows that matter most: tooling (platform Accountable, stops fragmentation) and career (guild + squad shared, stops attrition).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guild charter, not a status meeting.&lt;/strong&gt; A real guild owns standards, an RFC process, weekly design review, a quarterly standards refresh, and an in-place staff/principal ladder. The career ladder is the retention mechanism; the refresh is the anti-rot mechanism. Never embed a lone engineer into a domain with no paved road to build on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AE = the tested middle.&lt;/strong&gt; The analytics engineer owns transformation (dbt staging → marts) and the semantic layer as versioned, tested code between DE pipelines and analyst dashboards. DE stops at raw; AE owns staging onward; analysts do analysis on AE models, not production SQL. If metrics disagree across dashboards, you have no single semantic owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-boundary matrix.&lt;/strong&gt; One owner per stack layer: DE = ingestion/raw/infra, AE = transformation + semantic layer, DA = dashboards/insight, DS = ML features. Contested layers default to AE (transformation and semantic layer). Smell tests: analysts maintaining prod models = AE under-resourced; DEs writing marts = AE role missing; three revenue numbers = no semantic owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team Topologies mapping.&lt;/strong&gt; Stream-aligned domain squads (primary delivery), platform team (X-as-a-Service road), enabling team (time-boxed coaching, then dissolves), complicated-subsystem team (streaming/ML specialists). Spend your attention on interaction modes: platform → domain must be X-as-a-Service; the moment it becomes collaboration/ticketing, the platform is the bottleneck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data mesh readiness test.&lt;/strong&gt; Adopt "data as a product" and "self-serve platform" early; adopt "domain ownership" and "federated computational governance" together, incrementally, only when you have many domains and a mature platform. Mesh without a platform is silos with a rebrand; domain ownership without federated governance is the ungoverned-mesh anti-pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headcount ratios (diagnostics, not targets).&lt;/strong&gt; ~1 platform engineer per 4–6 embedded; DE:AE roughly 1:1 to 2:1; analysts sized by business demand with the AE layer as multiplier. Below ~1:8 platform-to-embedded = bottleneck forming; above ~1:3 = re-centralizing. Build the ask from domain count and ratios; use the ratio to spot drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-pattern → structural fix.&lt;/strong&gt; Central bottleneck → self-serve + embed (not more headcount). Silos → platform + guild + contracts (not re-centralize). Ungoverned mesh → federated governance first (not abandon mesh). Hero dependency → docs + rotation + bus factor &amp;gt; 1 (not a retention bonus). Premature mesh → hybrid until mature. Always fix the root, refuse the band-aid.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a data platform team?
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;data platform team is&lt;/strong&gt; the central group that owns the shared data substrate — ingestion, orchestration, storage, catalog, governance, and often the semantic layer — and provides it to the rest of the organization as a self-serve internal product rather than as a services desk. Its users are the domain engineers (embedded DEs and analytics engineers), and its job is to build a paved-road golden path so those teams can self-serve common needs (onboarding a source, scheduling a pipeline, discovering a dataset) instead of queuing behind a central team. A well-run platform team publishes SLAs/SLOs, runs on-call for the substrate, and measures itself by adoption and self-serve rate, not tickets closed. The defining test of a healthy &lt;code&gt;platform team&lt;/code&gt; is whether it thinks "product" (build the road once) or "service" (do the work per request) — the latter becomes the org's bottleneck.&lt;/p&gt;

&lt;h3&gt;
  
  
  Central platform vs embedded data engineers — which is better?
&lt;/h3&gt;

&lt;p&gt;Neither is better in isolation; the strongest &lt;code&gt;data platform team structure&lt;/code&gt; at scale is the &lt;strong&gt;hybrid&lt;/strong&gt; where a central platform team owns the shared substrate as a product and embedded data engineers build domain data products on top of it. A purely &lt;strong&gt;central data platform&lt;/strong&gt; gives you strong standards and one warehouse but becomes a bottleneck as demand outpaces headcount and sits far from domain knowledge. Purely &lt;strong&gt;embedded data engineers&lt;/strong&gt; give you fast iteration and deep domain context but drift into silos — divergent tools, duplicated infrastructure, and inconsistent metrics — because nobody owns the shared platform. The hybrid captures both: the platform paves the road (self-serve ingestion, contracts, catalog) and embedded engineers drive on it, reporting solid line to their domain for delivery and dotted line to a guild for standards. The &lt;code&gt;centralized vs decentralized data team&lt;/code&gt; debate is really a question of what stage you are at — centralize early, hybridize as you scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is an analytics engineer?
&lt;/h3&gt;

&lt;p&gt;An &lt;strong&gt;analytics engineer is&lt;/strong&gt; the modern-data-stack role that owns the transformation layer between the data engineer's pipelines and the data analyst's dashboards — turning raw, ingested data into clean, tested, documented, business-ready models (typically in dbt) and often owning the semantic/metrics layer on top. The role emerged because dbt and the ELT pattern moved transformation out of the pipeline and into the warehouse, and someone with software-engineering rigor (version control, testing, CI, modularity) &lt;em&gt;and&lt;/em&gt; business context had to own that middle. In practice the &lt;code&gt;analytics engineer&lt;/code&gt; writes modular SQL models, adds tests (uniqueness, not-null, referential integrity, freshness), documents the models, and defines each metric once in the semantic layer so every downstream tool computes it the same way. The clean boundary: the DE owns raw and below, the AE owns transformation and semantics, and the analyst does analysis on the AE's trusted models.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should we adopt data mesh?
&lt;/h3&gt;

&lt;p&gt;Adopt &lt;strong&gt;data mesh&lt;/strong&gt; when you have many domains, a mature self-serve platform team, and the ability to enforce federated computational governance — typically past ~40–50 data people, not before. Data mesh has four principles: domain ownership of data as a product, data-as-a-product thinking, a self-serve data platform, and federated computational governance. The mistake is adopting "domain ownership" without the other three: mesh without a strong platform is just silos with a rebrand, and domain ownership without federated governance produces the ungoverned-mesh anti-pattern — hundreds of undiscoverable, non-interoperable data products and duplicated entities. The pragmatic path is a &lt;em&gt;partial, deliberate&lt;/em&gt; mesh: adopt data-as-a-product and self-serve platform early, then extend domain ownership incrementally, domain by domain, with federated governance (catalog, interoperability standards, contract CI) turned on in lockstep. &lt;code&gt;Data mesh&lt;/code&gt; trades central control for domain autonomy and scalability, which pays off only when your scale and platform maturity can support it.&lt;/p&gt;

&lt;h3&gt;
  
  
  DE vs AE vs analyst — what's the difference?
&lt;/h3&gt;

&lt;p&gt;The three roles own three consecutive layers of the stack. The &lt;strong&gt;data engineer&lt;/strong&gt; owns ingestion, the pipeline runtime, infrastructure, orchestration, and the raw/source layer landing in the warehouse — their job is trustworthy raw data at scale. The &lt;strong&gt;analytics engineer&lt;/strong&gt; owns the transformation from raw to business-ready (staging → intermediate → marts in dbt), the tests and documentation on those models, and typically the semantic/metrics layer — their job is turning raw data into tested, reusable, single-definition models. The &lt;strong&gt;data analyst&lt;/strong&gt; owns the last mile: exploration, dashboards, ad-hoc analysis, and turning trusted models into insight and recommendations. The clean rule is one owner per layer: DEs stop at raw, AEs own the middle, analysts own insight. Warning signs that the boundary has blurred: DEs writing business marts (AE role missing), analysts maintaining production models (AE layer under-resourced), or three different revenue numbers across dashboards (no single semantic owner).&lt;/p&gt;

&lt;h3&gt;
  
  
  What are the ideal data-team ratios?
&lt;/h3&gt;

&lt;p&gt;There is no universal ratio, but sane starting diagnostics are roughly &lt;strong&gt;1 platform engineer per 4–6 embedded engineers&lt;/strong&gt;, an embedded &lt;strong&gt;DE:AE ratio of about 1:1 to 2:1&lt;/strong&gt; (more AE-heavy in analytics-intensive domains, more DE-heavy in infrastructure-heavy ones), and an &lt;strong&gt;engineers-to-analysts ratio driven by business demand&lt;/strong&gt; rather than by a fixed number — with the analytics-engineering layer acting as the multiplier that lets a modest engineering team support many analysts. Treat these as drift alarms, not targets: a platform team below ~1:8 of embedded engineers is a bottleneck forming, while above ~1:3 it is re-centralizing and over-investing. Build the actual headcount ask from your domain count and complexity — roughly one to two embedded DEs and about one AE per domain, plus a shared specialist team for genuinely hard subsystems (streaming, ML feature stores) — then use the ratios to check the mix is balanced. The right &lt;code&gt;data team org&lt;/code&gt; size is derived from domains and ratios, defended as a staffing model, never guessed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;system design practice library →&lt;/a&gt; for the data-platform, ownership, and org-topology scenarios that leadership and senior interviews love.&lt;/li&gt;
&lt;li&gt;Rehearse the modelling foundations on the &lt;a href="https://pipecode.ai/explore/practice/topic/dimensional-modeling" rel="noopener noreferrer"&gt;dimensional-modeling practice library →&lt;/a&gt; for the facts, dimensions, and marts an analytics engineer owns.&lt;/li&gt;
&lt;li&gt;Sharpen the pipeline axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt; for the ingestion and orchestration paved-road patterns a central platform team provides.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the archetype, platform-as-product, and AE-boundary decisions against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lock in data-org design muscle memory&lt;/h3&gt;

&lt;p&gt;Docs describe org charts. PipeCode drills teach the decision — when centralization becomes a bottleneck, when embedding drifts into silos, where the analytics-engineer boundary sits, and when data mesh earns its place. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — design-first practice tuned for the org and architecture trade-offs senior data engineers and data leaders actually defend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice design problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice SQL problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Data Engineering Manager Interview Prep: People, Roadmaps, Platform vs Product Trade-Offs</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Sat, 01 Aug 2026 13:27:04 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/data-engineering-manager-interview-prep-people-roadmaps-platform-vs-product-trade-offs-29eg</link>
      <guid>https://dev.to/gowthampotureddi/data-engineering-manager-interview-prep-people-roadmaps-platform-vs-product-trade-offs-29eg</guid>
      <description>&lt;p&gt;The &lt;strong&gt;&lt;code&gt;data engineering manager interview&lt;/code&gt;&lt;/strong&gt; is the one loop where your ability to write a flawless window function stops being the thing that gets you hired — and the switch catches almost every strong individual contributor off guard. You have spent years being rewarded for shipping the pipeline, tuning the query, and closing the incident yourself; now a panel wants to know whether you can build a team that ships those things without you, sequence a year of work against a headcount you do not fully control, and defend a platform investment to a VP who only cares about the next product launch. That is a different muscle, and it is trained differently. The interviewer is not testing whether you &lt;em&gt;can&lt;/em&gt; do the work — they assume you can — they are testing whether your judgment scales past your own two hands.&lt;/p&gt;

&lt;p&gt;This guide is the senior walkthrough you wished existed the first time a hiring manager said "tell me about a time you managed someone out," or "you just lost two engineers mid-quarter — what do you cut?", or "why would you spend a whole quarter on an internal platform instead of shipping the feature the business asked for?" It covers the four tracks every panel probes — &lt;strong&gt;people management&lt;/strong&gt; (hiring, growth, performance, conflict), &lt;strong&gt;roadmap planning&lt;/strong&gt; (prioritization, capacity, dependencies), the &lt;strong&gt;platform vs product&lt;/strong&gt; trade-off that defines data-team leadership, and the cross-functional behavioral loop where &lt;strong&gt;team leadership&lt;/strong&gt; is judged through STAR stories and metrics. Each section pairs a teaching block with a Solution-Tail interview answer — a template or script, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkx4ostxdgpg4430o0qb6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkx4ostxdgpg4430o0qb6.jpeg" alt="PipeCode blog header for the data engineering manager interview — bold white headline 'DE Manager Interview' over a hero composition of four glyph medallions (people, roadmap, platform-vs-product scale, behavioral star) arranged on a wheel around a central purple 'judgment' seal, on a dark gradient." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; on the architecture and trade-off muscles the panel will probe, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt;, keep the fundamentals sharp on the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt;, and rehearse the prioritization instinct on the &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;optimization practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why the DE manager interview tests judgment, not code&lt;/li&gt;
&lt;li&gt;People management: hiring, growth, performance, and conflict&lt;/li&gt;
&lt;li&gt;Roadmaps, planning, and prioritization under constraints&lt;/li&gt;
&lt;li&gt;Platform vs product: the central data-team trade-off&lt;/li&gt;
&lt;li&gt;Cross-functional influence, metrics, and the behavioral loop&lt;/li&gt;
&lt;li&gt;Cheat sheet — EM interview recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why the DE manager interview tests judgment, not code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Four interview tracks, one thing being measured — can your judgment scale past your own hands
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;the engineering manager loop is not a harder version of the IC loop — it is a different exam, one where every question is secretly measuring whether you can turn ambiguous business pressure into a sequence of good decisions that other people execute, and where "I would just do it myself" is the fastest way to fail.&lt;/strong&gt; A senior IC is hired for throughput and depth; a manager is hired for &lt;em&gt;leverage&lt;/em&gt; — the multiplier you apply to a team of five or eight or twelve. The panel splits that leverage into four tracks and grades each one, because a manager who is brilliant at roadmaps but cannot have a hard performance conversation will quietly lose their best people, and a manager who is a beloved coach but cannot say no to a stakeholder will burn the team out on a roadmap of everyone-else's-priorities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four tracks every DE manager loop probes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;People.&lt;/strong&gt; Hiring, onboarding, growth, performance management, conflict, retention. This is the track most IC candidates under-prepare and it is usually the highest-weighted. The question behind the question is: "will the team be stronger or weaker a year after you take it over?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project / roadmap.&lt;/strong&gt; Planning a quarter or a year, prioritizing under finite capacity, sequencing dependencies, negotiating scope. The question behind the question is: "when reality changes — headcount, deadlines, an outage — do you re-plan with a clear head or thrash?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical / architecture judgment.&lt;/strong&gt; Not "write this query" but "review this design," "make this build-vs-buy call," "decide when the platform investment is worth it." The question behind the question is: "can you still tell a good decision from a bad one now that you are not the one typing?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-functional.&lt;/strong&gt; Managing up to your director and VP, sideways to product and analytics, and down to your team; running incidents; representing the data org to the rest of the company. The question behind the question is: "can you get things done through influence, not authority?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What actually changes in the IC to EM transition.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your output is the team's output.&lt;/strong&gt; On your best week as a manager you may write zero production code and still have been enormously effective — because you unblocked three people, killed a doomed project, and closed a great hire. Candidates who describe their impact in terms of &lt;em&gt;their own&lt;/em&gt; deliverables have not made the shift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your feedback loop gets slower and noisier.&lt;/strong&gt; A failing test tells you in seconds. A bad hire or a demotivated senior engineer tells you in months. Managers who need fast, clean feedback to feel effective struggle; the job rewards patience and leading indicators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You trade depth for breadth.&lt;/strong&gt; You will know less about every system than the ICs who own them — and your job is to make peace with that, ask sharp questions, and trust the team, while keeping &lt;em&gt;enough&lt;/em&gt; technical depth to smell a bad plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your influence is mostly indirect.&lt;/strong&gt; You rarely get to command. You set context, remove obstacles, coach, and decide the few things only you can decide. "I told them to" is a weak answer; "I gave them the context and the constraints and they made the call" is a strong one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you describe impact as &lt;strong&gt;team outcomes&lt;/strong&gt;, not personal heroics? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you say &lt;strong&gt;"it depends, here are the two or three things I'd need to know"&lt;/strong&gt; before committing to an answer, rather than jumping to a solution? — required behaviour.&lt;/li&gt;
&lt;li&gt;Do you name the &lt;strong&gt;trade-off&lt;/strong&gt; in every decision — what you gave up to get what you got? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you own &lt;strong&gt;failures plainly&lt;/strong&gt; and describe the system change you made after, not just the recovery? — required behaviour.&lt;/li&gt;
&lt;li&gt;Do you distinguish &lt;strong&gt;decisions you'd make yourself&lt;/strong&gt; from &lt;strong&gt;decisions you'd delegate&lt;/strong&gt; and say why? — senior signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the four-track EM interview rubric
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful artifact for an EM loop is a memorised map of what each interviewer on the panel is grading, because the loop is deliberately split so that no single conversation covers everything. If you know which track you are in, you can steer your answer to the signal that interviewer needs. Walk through building the rubric for a typical five-round data-engineering-manager loop.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Round shape.&lt;/strong&gt; Recruiter screen, hiring-manager (people + delivery), a peer manager (cross-functional), a technical/architecture panel, and a skip-level or director (strategy + values).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What varies.&lt;/strong&gt; Each round weights one or two tracks heavily and touches the others lightly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trap.&lt;/strong&gt; Giving the same "here's how I'd architect it" answer in every round — which lands well in the technical round and flat everywhere else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Map the five rounds of a DE-manager loop to the four tracks and the dominant signal each interviewer is trying to extract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Round&lt;/th&gt;
&lt;th&gt;Interviewer&lt;/th&gt;
&lt;th&gt;Dominant track&lt;/th&gt;
&lt;th&gt;Secondary track&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Screen&lt;/td&gt;
&lt;td&gt;Recruiter&lt;/td&gt;
&lt;td&gt;People (motivation)&lt;/td&gt;
&lt;td&gt;Cross-functional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hiring manager&lt;/td&gt;
&lt;td&gt;Your future boss&lt;/td&gt;
&lt;td&gt;People + Project/roadmap&lt;/td&gt;
&lt;td&gt;Judgment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peer manager&lt;/td&gt;
&lt;td&gt;Another EM&lt;/td&gt;
&lt;td&gt;Cross-functional&lt;/td&gt;
&lt;td&gt;Project/roadmap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technical panel&lt;/td&gt;
&lt;td&gt;Senior/Staff ICs&lt;/td&gt;
&lt;td&gt;Judgment (architecture)&lt;/td&gt;
&lt;td&gt;People (mentoring)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skip-level&lt;/td&gt;
&lt;td&gt;Director / VP&lt;/td&gt;
&lt;td&gt;Strategy + values&lt;/td&gt;
&lt;td&gt;People&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EM interview-track rubric — what to lead with per round
=======================================================

Recruiter screen
  Lead with: why management, why this team, one team-outcome story.
  Avoid: deep architecture; save it.

Hiring manager (your future boss)
  Lead with: how you run a team (1:1s, planning), a hard people call,
             how you'd approach the first 90 days here.
  They are imagining working with you daily.

Peer manager (another EM)
  Lead with: a cross-team conflict you resolved, how you negotiate
             scope and dependencies, how you handle a dropped handoff.
  They are checking: are you a good neighbour or a territory-grabber?

Technical / architecture panel
  Lead with: judgment, not recall. Ask clarifying questions, name
             trade-offs, say what you'd delegate vs decide yourself.
  They fear: a manager who has gone stale and rubber-stamps bad designs.

Skip-level (director / VP)
  Lead with: how you connect the data roadmap to business outcomes,
             platform-vs-product thinking, values under pressure.
  They are checking: can you own a mission, not just a backlog?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The recruiter screen is a motivation and communication filter — they are checking that you genuinely want to manage (not that you see it as a promotion you're owed) and that you can tell a crisp story. Leading with architecture here wastes the round.&lt;/li&gt;
&lt;li&gt;The hiring-manager round is the highest-stakes one because that person will live with the decision daily. They weight people and delivery: how you run 1:1s, how you plan, how you handle a struggling engineer. Bring concrete mechanisms, not platitudes.&lt;/li&gt;
&lt;li&gt;The peer-manager round is a "good neighbour" test. Other EMs want to know whether you will fight fair over shared roadmap, own your handoffs, and escalate cleanly. A story about resolving a cross-team dependency conflict is gold here.&lt;/li&gt;
&lt;li&gt;The technical panel is judgment, not a coding gauntlet. Senior ICs are terrified of a manager who has gone stale and approves bad architecture. Demonstrate you can still reason — ask the clarifying questions, name the trade-offs, and be explicit about what you'd trust the team to decide.&lt;/li&gt;
&lt;li&gt;The skip-level round is strategy and values. Directors want a manager who owns a mission and connects the data roadmap to business outcomes. This is where platform-vs-product thinking and "what would you do if we cut your budget 20%" live.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Track&lt;/th&gt;
&lt;th&gt;Rounds that weight it&lt;/th&gt;
&lt;th&gt;If you skip it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;People&lt;/td&gt;
&lt;td&gt;Screen, hiring manager, skip-level&lt;/td&gt;
&lt;td&gt;Read as "still an IC at heart"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project / roadmap&lt;/td&gt;
&lt;td&gt;Hiring manager, peer manager&lt;/td&gt;
&lt;td&gt;Read as "can't run a quarter"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judgment&lt;/td&gt;
&lt;td&gt;Technical panel, hiring manager&lt;/td&gt;
&lt;td&gt;Read as "gone stale technically"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-functional&lt;/td&gt;
&lt;td&gt;Peer manager, screen&lt;/td&gt;
&lt;td&gt;Read as "will create silos"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Before every round, ask yourself "which track is this person grading?" and lead with a story tuned to that signal. The same generic answer in five rounds reads as one-dimensional; five tuned answers read as a rounded manager.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the IC-vs-EM time-allocation shift
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Interviewers frequently probe the transition by asking "how do you spend your week?" or "what did you stop doing when you became a manager?" The strong answer is quantitative and honest: a real week reallocates most hours away from personal delivery and toward people and planning. Walk through the before-and-after allocation and what it reveals.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The IC week.&lt;/strong&gt; Dominated by focused build time — designing, coding, reviewing, debugging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The EM week.&lt;/strong&gt; Dominated by 1:1s, planning, unblocking, hiring, and cross-functional syncs, with a small protected slice for hands-on work to stay credible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure mode.&lt;/strong&gt; A new manager who keeps 60% build time — they are doing two jobs badly and starving the team of attention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Contrast a healthy IC week with a healthy first-line-EM week and identify the biggest reallocation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;IC week (hrs)&lt;/th&gt;
&lt;th&gt;EM week (hrs)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Focused build (code/design/debug)&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code review + design review&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1:1s and coaching&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planning / roadmap / prioritization&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-functional + managing up&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hiring (screens, loops, sourcing)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incident / on-call leadership&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time-allocation answer template
===============================

"As an IC my week was ~70% build time. As a first-line manager,
 build time drops to well under 20% and mostly moves to design
 reviews and small unblocking tasks — I keep just enough hands-on
 work to review architecture credibly and to backfill in an
 emergency, never on the critical path.

 The hours flow into three buckets:
   1. People — 1:1s, coaching, growth, feedback (~8h).
   2. Planning — roadmap, prioritization, capacity (~8h).
   3. Cross-functional + hiring — managing up, partner syncs,
      interview loops (~11h).

 The mistake I avoided was staying on the critical path for
 delivery. The first time a sprint slipped because *I* was the
 bottleneck, I learned to treat my own coding capacity as zero
 for planning purposes."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Naming the roughly-70%-to-under-20% build-time drop signals you understand the job is fundamentally different, not "IC plus some meetings." Interviewers hear this shift as evidence you've actually made the transition rather than aspiring to it.&lt;/li&gt;
&lt;li&gt;Keeping a small, deliberate slice of hands-on work — design reviews, prototypes, glue code off the critical path — is the credible middle position. Claiming zero technical work reads as detached; claiming heavy coding reads as unable to let go.&lt;/li&gt;
&lt;li&gt;The three destination buckets (people, planning, cross-functional) map directly onto the four interview tracks, so this answer doubles as a preview that you know where a manager's leverage comes from.&lt;/li&gt;
&lt;li&gt;Explicitly disowning the critical path — "I treat my coding capacity as zero when planning" — is the senior move. New managers who keep themselves in the delivery plan create a single point of failure and can't do the actual job when a crisis hits.&lt;/li&gt;
&lt;li&gt;Attaching the lesson to a concrete failure ("the first time a sprint slipped because I was the bottleneck") turns an abstract principle into evidence, which is what behavioral interviewers reward.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Weak version&lt;/th&gt;
&lt;th&gt;Senior version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build time&lt;/td&gt;
&lt;td&gt;"I still code a lot"&lt;/td&gt;
&lt;td&gt;"under 20%, off the critical path"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where hours go&lt;/td&gt;
&lt;td&gt;vague "more meetings"&lt;/td&gt;
&lt;td&gt;people / planning / cross-functional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self on the plan&lt;/td&gt;
&lt;td&gt;counts own capacity&lt;/td&gt;
&lt;td&gt;treats own capacity as zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;assertion&lt;/td&gt;
&lt;td&gt;a specific slip they learned from&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Answer "how do you spend your week?" with numbers and a named failure. The reallocation &lt;em&gt;away&lt;/em&gt; from personal build time is the whole point — if your week still looks like an IC's, you haven't made the shift the panel is checking for.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the "why management" answer template
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Almost every EM loop opens with some form of "why do you want to manage?" It is a trap for two common bad answers: the status answer ("it's the next step / more money / a bigger title") and the control answer ("I want to decide how things get built"). The strong answer is about deriving energy from &lt;em&gt;other people's&lt;/em&gt; growth and from multiplying impact. Walk through constructing an honest, non-clichéd version.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Avoid the status framing.&lt;/strong&gt; Management is a change of profession, not a promotion above ICs; senior ICs can out-earn and out-rank managers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid the control framing.&lt;/strong&gt; Wanting to manage so you can dictate design is a red flag for micromanagement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ground it in evidence.&lt;/strong&gt; The best "why management" answers point at things you already did — mentoring, leading without the title, unblocking the team — that you found energizing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draft a "why do you want to be a manager?" answer that is honest, evidence-backed, and free of the two clichéd traps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ingredient&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;Strong answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Motivation&lt;/td&gt;
&lt;td&gt;"next step in my career"&lt;/td&gt;
&lt;td&gt;"I get more energy from the team winning than from my own PR merging"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;"I already mentor two juniors and led the migration without the title"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade-off awareness&lt;/td&gt;
&lt;td&gt;ignores the downside&lt;/td&gt;
&lt;td&gt;"I know I'll code less and my feedback loop gets slower — I've made peace with that"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure honesty&lt;/td&gt;
&lt;td&gt;"I'll be great at it"&lt;/td&gt;
&lt;td&gt;"the part I'll have to work at is patience with slow signals"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Why management" answer template
================================

Hook (motivation, honest):
  "Over the last two years the work I found most energizing wasn't
   my own delivery — it was unblocking the team, mentoring the two
   juniors on my squad, and leading the warehouse migration even
   though I didn't have the title. When my PR merges I feel fine;
   when someone I coached ships something hard and grows from it,
   that's the part I want more of."

Evidence:
  "I already do a lot of the job informally — running planning,
   representing the team in cross-functional syncs, giving feedback."

Trade-off awareness:
  "I've thought about the costs. I'll write far less code, my
   feedback loop gets slower and noisier, and I'll know less about
   each system than the ICs. I've made peace with all three."

The honest gap:
  "The muscle I'll have to build is patience — a bad hire or a
   demotivated engineer takes months to show up, unlike a failing
   test. I'm working on trusting leading indicators."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The hook grounds motivation in a genuine energy source — other people's growth and the team's wins — which is the trait that predicts a happy, durable manager. Interviewers have heard the status answer a thousand times; the growth answer stands out.&lt;/li&gt;
&lt;li&gt;The evidence section proves you're not romanticizing the role: you already do the informal version (mentoring, planning, representing the team) and still want more of it. Wanting to manage &lt;em&gt;after&lt;/em&gt; tasting it is far more credible than wanting it in the abstract.&lt;/li&gt;
&lt;li&gt;Naming the trade-offs — less code, slower feedback, less depth — pre-empts the interviewer's biggest fear that you don't understand what you're signing up for. Volunteering the costs is a senior move.&lt;/li&gt;
&lt;li&gt;Admitting the honest gap (patience with slow signals) shows self-awareness without torpedoing yourself. Choose a real, non-disqualifying growth area — "patience" and "delegation" are safe; "I struggle with conflict" in a people-heavy role is not.&lt;/li&gt;
&lt;li&gt;The whole answer avoids both traps: no "next step / more money" and no "I want to control the design." It reads as someone changing profession with eyes open.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Interviewer reads it as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Growth-energy hook&lt;/td&gt;
&lt;td&gt;show the right motivation&lt;/td&gt;
&lt;td&gt;"will enjoy the actual job"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Informal evidence&lt;/td&gt;
&lt;td&gt;prove it's tested, not romantic&lt;/td&gt;
&lt;td&gt;"already doing the role"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade-off list&lt;/td&gt;
&lt;td&gt;show eyes-open realism&lt;/td&gt;
&lt;td&gt;"understands the costs"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Honest gap&lt;/td&gt;
&lt;td&gt;show self-awareness&lt;/td&gt;
&lt;td&gt;"coachable, self-aware"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Answer "why management?" with an energy source (other people's growth), evidence you already do the job informally, and an explicit list of the trade-offs you've accepted. Never lead with title, money, or control — those three are the fastest disqualifiers in the loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the IC-to-EM transition
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You are a Staff data engineer who has never formally managed. Convince me you are ready to run a team of six, including two engineers more tenured than you. Walk me through how you'd think about the first 90 days, what you'd stop doing, and how you'd earn the trust of people who were your peers last week."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a first-90-days plan built on listening, one team win, and explicit role redefinition
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First-90-days plan for a new DE manager (peer-to-boss transition)
=================================================================

Days 0–30 — LISTEN and stabilize
  - 1:1 with every team member: "what's working, what's broken,
    what do you want from a manager, what should I not break?"
  - Map the systems, the on-call reality, the in-flight roadmap.
  - Change almost nothing. Fix only obvious, low-risk pain
    (a broken alert, a missing runbook) to build credibility.
  - Meet every key stakeholder (product, analytics, infra, my boss).

Days 30–60 — DIAGNOSE and align
  - Synthesize the listening tour into 3 themes (e.g. "on-call is
    burning people out", "roadmap has no clear priority", "two
    seniors feel stalled").
  - Co-write a lightweight team charter / operating model with the
    team, not for them: how we plan, how we do 1:1s, on-call rota.
  - Pick ONE visible team win to land by day 90.

Days 60–90 — DELIVER one win and set the operating rhythm
  - Ship the one win (e.g. cut on-call pages 40% via alert cleanup).
  - Stand up the durable rhythm: weekly 1:1s, biweekly planning,
    quarterly growth conversations.
  - Give the two senior engineers explicit scope/ownership so they
    grow through me, not around me.

Explicit role redefinition (the peer-to-boss part)
  - Name it out loud in the first 1:1s: "our working relationship
    is changing; here's how I'll try to be useful, and I need your
    help and candour."
  - Stop competing on output. My job is now their success.
  - Give the more-tenured engineers MORE autonomy, not less —
    trust is the currency that converts former peers into allies.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Primary activity&lt;/th&gt;
&lt;th&gt;Signal to the team&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Days 0–30&lt;/td&gt;
&lt;td&gt;Listening tour + stabilize&lt;/td&gt;
&lt;td&gt;"listens before acting"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Days 30–60&lt;/td&gt;
&lt;td&gt;Diagnose themes + co-write operating model&lt;/td&gt;
&lt;td&gt;"involves us, has a plan"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Days 60–90&lt;/td&gt;
&lt;td&gt;Land one win + set rhythm&lt;/td&gt;
&lt;td&gt;"delivers, not just talks"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughout&lt;/td&gt;
&lt;td&gt;Redefine the peer relationship explicitly&lt;/td&gt;
&lt;td&gt;"handled the awkward part head-on"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seniors&lt;/td&gt;
&lt;td&gt;Grant scope and autonomy&lt;/td&gt;
&lt;td&gt;"grows us, doesn't threaten us"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The plan resists the new manager's strongest urge — to prove value by immediately reorganizing everything. It front-loads listening, earns credibility with one concrete win, and treats the peer-to-boss awkwardness as something to name directly rather than pretend away. The two tenured engineers are handled by giving them more ownership, converting a potential rivalry into a partnership.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric of a good transition&lt;/th&gt;
&lt;th&gt;90-day target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1:1s established with all reports&lt;/td&gt;
&lt;td&gt;100% by day 14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key stakeholders met&lt;/td&gt;
&lt;td&gt;100% by day 30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visible team win landed&lt;/td&gt;
&lt;td&gt;1 by day 90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team operating model documented&lt;/td&gt;
&lt;td&gt;co-written by day 60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attrition during transition&lt;/td&gt;
&lt;td&gt;0 regretted departures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Listen before you act&lt;/strong&gt;&lt;/strong&gt; — a new manager has the least context they will ever have; changing things in week one on thin information destroys trust and often breaks something that worked. The listening tour buys context and signals respect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;One visible win&lt;/strong&gt;&lt;/strong&gt; — credibility is earned with a concrete outcome (fewer pages, a killed zombie project), not with a reorg. It proves you can deliver &lt;em&gt;through the team&lt;/em&gt; early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Explicit role redefinition&lt;/strong&gt;&lt;/strong&gt; — the peer-to-boss shift is awkward; naming it out loud ("our relationship is changing, here's how I'll be useful") disarms the awkwardness that otherwise festers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;More autonomy for senior peers&lt;/strong&gt;&lt;/strong&gt; — the instinct to assert authority over former peers backfires. Granting ownership converts tenured engineers into allies who grow through you rather than route around you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the plan is deliberately slow on visible change for 30 days, which can feel unproductive and requires discipline to hold. The payoff is durable trust; the alternative — a fast reorg on no context — is O(team) in regretted attrition. Slow-then-steady beats fast-then-firefight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems that build architecture-review judgment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;
&lt;strong&gt;SQL problems to keep your technical credibility sharp&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. People management: hiring, growth, performance, and conflict
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The manager's core loop is hire → onboard → grow → evaluate → retain — and the interview probes every stage
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzs63invd2zz916fwdyks.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzs63invd2zz916fwdyks.jpeg" alt="Iconographic people-management diagram for managing data engineers — a manager's core loop ring (hire, onboard, grow, evaluate, retain) beside a skill-will matrix quadrant and a 1:1 growth-plan card, with a performance-conversation ribbon." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;people management is a loop — you hire the right engineers, onboard them to productivity, grow them toward the next level, evaluate them honestly, and retain the ones you want to keep — and the &lt;code&gt;data engineering manager interview&lt;/code&gt; probes every stage because the through-line, "will the team be stronger a year from now?", is the single strongest predictor of a good manager.&lt;/strong&gt; Every stage has a mechanism a strong manager can describe concretely, and vague answers ("I have an open-door policy," "I give feedback regularly") are the fastest way to sound like someone who has read about management but not done it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hiring — the highest-leverage decision a manager makes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Define the bar before you see resumes.&lt;/strong&gt; Write the role's must-haves and nice-to-haves and the signals you'll test for; otherwise the loop drifts toward "did I like them?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured over vibes.&lt;/strong&gt; Consistent questions, a rubric, and a debrief where each interviewer commits to a rating &lt;em&gt;before&lt;/em&gt; hearing others — this counters groupthink and bias.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hire for the trajectory, not just today.&lt;/strong&gt; A data team needs range: SQL and modeling depth, pipeline/infra skill, and increasingly data-quality and stakeholder instincts. Balance the team, don't clone yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bad hire is more expensive than a slow hire.&lt;/strong&gt; The cost of a mis-hire — ramp time, the mess they leave, the exit process, team morale — dwarfs the cost of a longer search.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Onboarding — the first 90 days set the ceiling.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A ramp plan, not a laptop and good luck.&lt;/strong&gt; A written 30/60/90 with a first small shippable task in week one, a buddy, and clear "what good looks like."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Early wins build confidence.&lt;/strong&gt; Sequence the first tasks from small-and-safe to meaningful, so momentum compounds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Growth — the retention engine.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The skill/will matrix.&lt;/strong&gt; Match your style to the person: high-skill/high-will → delegate; high-skill/low-will → re-engage and excite; low-skill/high-will → coach and teach; low-skill/low-will → direct closely (and consider fit).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Career ladders and growth plans.&lt;/strong&gt; Every engineer should be able to name the two or three things standing between them and the next level, and see you actively creating opportunities to close them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sponsorship, not just mentorship.&lt;/strong&gt; Mentoring is advice; sponsorship is spending your capital to put them on the visible, career-making project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance &amp;amp; conflict — the part IC candidates fear.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feedback early and specifically.&lt;/strong&gt; Situation-behaviour-impact, close to the event, no surprises at review time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The underperformer path.&lt;/strong&gt; Diagnose (skill? will? fit? context?) → set clear, written, time-boxed expectations → support hard → and if it doesn't turn, part ways with dignity. A formal plan is a tool to help someone succeed, not a paperwork prelude to firing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict.&lt;/strong&gt; Get to the interests behind the positions, mediate directly, and don't let two strong engineers' feud quietly tax the whole team.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on people management.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Tell me about a time you managed an underperformer." — the single most common EM behavioral question.&lt;/li&gt;
&lt;li&gt;"How do you grow a senior engineer who's plateaued?" — sponsorship, stretch scope, new domain.&lt;/li&gt;
&lt;li&gt;"How do you handle two engineers in constant conflict?" — interests, mediation, boundaries.&lt;/li&gt;
&lt;li&gt;"How do you know your 1:1s are working?" — leading indicators: candour, they bring problems early, growth is visible.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — a 1:1 and growth-plan doc template
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The 1:1 is the manager's primary instrument, and interviewers probe whether yours are structured (theirs, forward-looking, and captured) or a status meeting in disguise. Pair it with a lightweight living growth plan, and you have the retention engine most teams lack. Walk through a template you can describe verbatim in the loop.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 1:1 is the report's meeting.&lt;/strong&gt; Their agenda first; status belongs in tickets, not here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capture and follow through.&lt;/strong&gt; A shared running doc so commitments don't evaporate and growth is visible over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The growth plan is living.&lt;/strong&gt; Current level, target level, the two or three gaps, and the concrete opportunities you're creating to close them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design a running 1:1 doc and a growth-plan block that a manager and a mid-level data engineer keep together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Their topics&lt;/td&gt;
&lt;td&gt;Report&lt;/td&gt;
&lt;td&gt;Every 1:1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;My topics + feedback&lt;/td&gt;
&lt;td&gt;Manager&lt;/td&gt;
&lt;td&gt;Every 1:1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action items&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;Every 1:1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Growth plan&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;Reviewed monthly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Career target + gaps&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;Reviewed quarterly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# 1:1 — &amp;lt;Report name&amp;gt; / &amp;lt;Manager name&amp;gt;&lt;/span&gt;
&lt;span class="ge"&gt;_Standing doc. Newest notes on top. Their agenda first._&lt;/span&gt;

&lt;span class="gu"&gt;## 2026-08-14&lt;/span&gt;
&lt;span class="gu"&gt;### Their topics (they drive)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Blocked on Airflow upgrade — needs infra to approve the maintenance window
&lt;span class="p"&gt;-&lt;/span&gt; Wants to own the data-quality framework next quarter

&lt;span class="gu"&gt;### My topics / feedback&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Praise: the incident writeup last week was excellent — clear timeline, real root cause
&lt;span class="p"&gt;-&lt;/span&gt; Growth nudge: in design review, state your recommendation first, then the options

&lt;span class="gu"&gt;### Action items&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] (me) escalate the Airflow maintenance window to infra by Fri
&lt;span class="p"&gt;-&lt;/span&gt; [ ] (them) draft a 1-pager on the data-quality framework
&lt;span class="p"&gt;-&lt;/span&gt; [x] (me) confirmed conference budget approved
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## Growth plan (reviewed monthly)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Current level:**&lt;/span&gt; DE II (mid)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Target level:**&lt;/span&gt; Senior DE
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Gaps to close:**&lt;/span&gt;
&lt;span class="p"&gt;  1.&lt;/span&gt; Lead a project end-to-end across ≥2 teams (scope + influence)
&lt;span class="p"&gt;  2.&lt;/span&gt; Raise design-doc quality — drive the decision, not just options
&lt;span class="p"&gt;  3.&lt;/span&gt; Mentor one junior through a full feature
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Opportunities I'm creating:**&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Owns the data-quality framework project (cross-team) starting Q4
&lt;span class="p"&gt;  -&lt;/span&gt; Pairs with junior on the CDC pipeline
&lt;span class="p"&gt;  -&lt;/span&gt; Presents the framework design at the DE guild (visibility)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Putting &lt;em&gt;their&lt;/em&gt; topics first, in a doc they can edit, structurally enforces that the 1:1 is the report's meeting. Managers whose 1:1s are status updates are the ones whose reports feel unheard and eventually leave.&lt;/li&gt;
&lt;li&gt;The feedback block pairs a specific piece of praise with one forward-looking growth nudge every session, so feedback is continuous and low-stakes — there are no surprises at review time because the review just summarizes the doc.&lt;/li&gt;
&lt;li&gt;Action items with owners and checkboxes make the manager accountable too; a report seeing their manager reliably close "escalate the maintenance window" learns the 1:1 is where problems actually get solved.&lt;/li&gt;
&lt;li&gt;The growth plan names the current and target level and — critically — the two or three &lt;em&gt;gaps&lt;/em&gt;, so growth is concrete rather than "keep doing great." An engineer who can name what's between them and the next level is a retained engineer.&lt;/li&gt;
&lt;li&gt;The "opportunities I'm creating" section is the sponsorship half: the manager is putting the person on cross-team, visible work that closes the gaps. This is the difference between telling someone to grow and building the runway for it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;What it prevents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Their-topics-first agenda&lt;/td&gt;
&lt;td&gt;1:1 decaying into status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous SBI feedback&lt;/td&gt;
&lt;td&gt;review-time surprises&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owned action items&lt;/td&gt;
&lt;td&gt;commitments evaporating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Named growth gaps&lt;/td&gt;
&lt;td&gt;"how do I get promoted?" ambiguity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Created opportunities&lt;/td&gt;
&lt;td&gt;plateau and attrition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Run 1:1s from a shared running doc, their agenda first, with one specific piece of feedback every time and a living growth plan that names the gaps and the opportunities you're creating to close them. If you can describe this doc in an interview, you sound like a manager; if you say "open-door policy," you sound like an IC.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a performance-conversation script for an underperformer
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; "Tell me about managing an underperformer" is the most common EM behavioral question, and the strong answer shows a clear, kind, documented process — not avoidance and not a surprise firing. The performance conversation itself has a structure you can rehearse. Walk through a script for a mid-level engineer whose delivery has slipped.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Diagnose first.&lt;/strong&gt; Is it skill, will, fit, or context (a life event, a bad project match)? The fix differs completely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be direct and specific.&lt;/strong&gt; Name the gap with evidence; don't soften it into unrecognizability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Own your part.&lt;/strong&gt; Ask what support has been missing — sometimes the manager is a contributing cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agree on clear, written, time-boxed expectations.&lt;/strong&gt; And on the support you'll provide.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Script the first performance conversation with an engineer whose last two projects slipped and whose design reviews are shallow, structured so it helps them turn it around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Weak approach&lt;/th&gt;
&lt;th&gt;Strong approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Timing&lt;/td&gt;
&lt;td&gt;wait for review cycle&lt;/td&gt;
&lt;td&gt;now, close to the evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framing&lt;/td&gt;
&lt;td&gt;vague ("be better")&lt;/td&gt;
&lt;td&gt;specific behaviours + impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership&lt;/td&gt;
&lt;td&gt;all on them&lt;/td&gt;
&lt;td&gt;ask what support is missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outcome&lt;/td&gt;
&lt;td&gt;unspoken threat&lt;/td&gt;
&lt;td&gt;clear written expectations + a check-in date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tone&lt;/td&gt;
&lt;td&gt;either soft or harsh&lt;/td&gt;
&lt;td&gt;clear AND kind&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Performance conversation script (first, supportive)
===================================================

Open (direct, no ambush):
  "I want to talk about how the last two projects have gone, because
   I think there's a gap and I want to help you close it. This is a
   supportive conversation, not a formal process."

Name it with evidence (SBI):
  "On the ingestion project and the SLA dashboard, both slipped
   more than a sprint past the estimate, and in the last two design
   reviews the docs listed options but didn't land a recommendation.
   The impact is that partners started routing around the team, and
   I had to step in on the review."

Ask (diagnose + own my part):
  "Help me understand what's going on. Is the scope unclear? Are you
   stretched across too much? Is something outside work weighing on
   you? And — is there support I should be giving that I'm not?"

Agree on expectations (clear, written, time-boxed):
  "Here's what 'back on track' looks like over the next 6 weeks:
   design docs that state a recommendation, and the next project
   delivered within its estimate or re-scoped early with a heads-up.
   I'll pair with you on the first design doc and clear two of your
   side tasks so you can focus. Let's check in weekly."

Close (kind, honest):
  "I'm telling you this because I think you can do this level of
   work — that's why the gap is worth closing. I'm on your side."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The open removes the ambush: naming that a gap exists &lt;em&gt;and&lt;/em&gt; that this is supportive (not yet a formal process) lets the person hear the content instead of panicking about their job. Surprise is the enemy of a productive performance conversation.&lt;/li&gt;
&lt;li&gt;Naming the gap with specific situation-behaviour-impact evidence (which projects, which reviews, what the downstream effect was) makes it undeniable and coachable. Vague feedback ("be more proactive") gives them nothing to act on.&lt;/li&gt;
&lt;li&gt;Asking what's going on and owning your part does two things: it diagnoses the real cause — skill and will and context need different responses — and it models that this is a shared problem, which keeps the person engaged rather than defensive.&lt;/li&gt;
&lt;li&gt;The written, time-boxed expectations convert a fuzzy "do better" into observable targets (docs that state a recommendation; next project on estimate or re-scoped early) with a support commitment and a cadence. Clarity is kindness here.&lt;/li&gt;
&lt;li&gt;The close reaffirms belief in the person, which is what makes the whole thing land as help rather than a threat. If a manager doesn't believe the person can succeed, that's a different (exit) conversation — but the first performance conversation should always be a genuine attempt to turn it around.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;If skipped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No-ambush open&lt;/td&gt;
&lt;td&gt;let them hear it&lt;/td&gt;
&lt;td&gt;defensiveness, panic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SBI evidence&lt;/td&gt;
&lt;td&gt;make it coachable&lt;/td&gt;
&lt;td&gt;"vague and unfair"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diagnose + own&lt;/td&gt;
&lt;td&gt;find the real cause&lt;/td&gt;
&lt;td&gt;wrong fix applied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Written expectations&lt;/td&gt;
&lt;td&gt;observable targets&lt;/td&gt;
&lt;td&gt;ambiguity, no accountability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Belief close&lt;/td&gt;
&lt;td&gt;frame as help&lt;/td&gt;
&lt;td&gt;reads as a threat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; A performance conversation is clear &lt;em&gt;and&lt;/em&gt; kind: specific evidence, an honest diagnosis of skill-vs-will-vs-context, written time-boxed expectations, real support, and a genuine statement of belief. If the answer to "manage an underperformer" is a story about avoidance or a surprise firing, it fails; if it's this structure with a real outcome (turned around &lt;em&gt;or&lt;/em&gt; parted ways with dignity), it lands.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the skill/will matrix for tailoring your management style
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A frequent probe is "do you manage everyone the same way?" — and the strong answer is no, you diagnose each person on skill and will and adapt. The skill/will matrix is the crisp mental model. Walk through placing four real reports and choosing a style for each.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skill.&lt;/strong&gt; Do they have the competence for the task at hand?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Will.&lt;/strong&gt; Do they have the motivation and confidence for it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The four quadrants.&lt;/strong&gt; Each needs a different default: delegate, coach, excite/re-engage, or direct.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Place four data engineers on the skill/will matrix and prescribe the management style and the risk for each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engineer&lt;/th&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Will&lt;/th&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A — senior, thriving&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;owns the platform, wants more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B — senior, disengaged&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;great but bored, eyeing the door&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C — junior, eager&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;new grad, hungry, learning fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D — mid, struggling&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;wrong project fit, morale sinking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Skill/Will matrix — style per quadrant
======================================

              HIGH WILL                     LOW WILL
          +--------------------------+--------------------------+
HIGH      | A: DELEGATE              | B: EXCITE / RE-ENGAGE    |
SKILL     | - autonomy + stretch     | - find the motivation gap|
          | - sponsor for promo      | - new domain / bigger    |
          | - risk: under-challenge  |   scope / more autonomy  |
          |                          | - risk: they leave first |
          +--------------------------+--------------------------+
LOW       | C: COACH                 | D: DIRECT (then decide)  |
SKILL     | - teach, pair, clear     | - close direction + fast |
          |   feedback, safe wins    |   feedback loop          |
          | - risk: overwhelm too    | - diagnose: fit? context?|
          |   fast                    | - risk: is it the right  |
          |                          |   seat, or the right bus?|
          +--------------------------+--------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Engineer A (high/high) gets delegation, stretch scope, and active sponsorship for promotion. The trap is neglect — high performers are easy to ignore because they need the least day-to-day attention, and that's exactly how you lose them; they need growth, not supervision.&lt;/li&gt;
&lt;li&gt;Engineer B (high/low) is the most urgent case and the one new managers miss. A bored senior is a flight risk; the job is to find the motivation gap and close it with a new domain, bigger scope, or more autonomy — not more oversight, which accelerates the exit.&lt;/li&gt;
&lt;li&gt;Engineer C (low/high) gets coaching: teaching, pairing, clear feedback, and a sequence of safe wins. The risk is overwhelming their eagerness with too much too fast; protect the early confidence-building wins.&lt;/li&gt;
&lt;li&gt;Engineer D (low/low) needs close direction and a tight feedback loop &lt;em&gt;and&lt;/em&gt; an honest diagnosis: is this a skill gap, a bad project fit, or a context problem? Sometimes it's the wrong seat (fixable by reassignment) and sometimes it's the wrong bus (a fit conversation). Don't confuse the two.&lt;/li&gt;
&lt;li&gt;The meta-point for the interview is that great managers apply &lt;em&gt;different&lt;/em&gt; styles to different people deliberately, and can explain why. "I treat everyone the same / fairly" sounds nice but is actually a red flag — fair means giving each person what &lt;em&gt;they&lt;/em&gt; need, not identical treatment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engineer&lt;/th&gt;
&lt;th&gt;Style&lt;/th&gt;
&lt;th&gt;Biggest risk if mismanaged&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A high/high&lt;/td&gt;
&lt;td&gt;delegate + sponsor&lt;/td&gt;
&lt;td&gt;neglect → attrition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B high/low&lt;/td&gt;
&lt;td&gt;excite / re-engage&lt;/td&gt;
&lt;td&gt;flight risk realized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C low/high&lt;/td&gt;
&lt;td&gt;coach&lt;/td&gt;
&lt;td&gt;overwhelmed, confidence lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D low/low&lt;/td&gt;
&lt;td&gt;direct + diagnose fit&lt;/td&gt;
&lt;td&gt;wrong-seat vs wrong-bus confusion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Diagnose every report on skill and will and adapt: delegate to high/high, re-engage high/low before they leave, coach low/high with safe wins, and direct-then-diagnose low/low. "I manage everyone the same" is the wrong answer; "I give each person what they specifically need" is the right one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on people management
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You inherit a team with a senior data engineer who is technically the strongest person on the team but is toxic in reviews — dismissive, makes juniors afraid to ask questions, and two people have privately said they'll leave if it continues. Walk me through exactly how you'd handle it, including what you'd do if the behaviour doesn't change."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using direct feedback, a clear behaviour bar, and a willingness to lose a top performer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Handling a brilliant-but-toxic senior engineer
==============================================

Step 1 — Gather specifics, not vibes
  - Note concrete incidents: dates, what was said, the effect
    ("in Tue review, said 'this is obvious' to the junior who then
     stopped asking questions"). Feedback needs evidence.

Step 2 — Direct, private, specific feedback (assume good intent first)
  "Your technical judgment is the best on the team and I rely on it.
   But in reviews the delivery is landing as dismissive — here are
   two specific moments — and the effect is that juniors have stopped
   asking questions in front of you. That's costing us more than your
   technical output is adding. I need that to change."

Step 3 — Make the standard explicit and non-negotiable
  - Name the behaviour bar: reviews are for making the work and the
    person better; questions are always welcome; disagreement is
    fine, contempt is not. This applies to everyone, seniors included.

Step 4 — Support the change
  - Offer concrete tools: ask questions instead of declaring, praise
    in public / correct in private, review the code not the coder.
  - Check in; acknowledge improvement genuinely when it comes.

Step 5 — If it doesn't change, act — even at a cost
  - Escalate the consequence honestly: performance rating impact,
    then a formal plan, then exit. Document throughout.
  - Be willing to lose the strongest IC. Culture is set by the worst
    behaviour you tolerate from your best people. Two good engineers
    leaving over one toxic one is a terrible trade.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Underlying principle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Collect specific incidents&lt;/td&gt;
&lt;td&gt;feedback needs evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Direct private feedback, good intent&lt;/td&gt;
&lt;td&gt;respect + clarity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;State the non-negotiable bar&lt;/td&gt;
&lt;td&gt;standards apply to everyone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Support the change&lt;/td&gt;
&lt;td&gt;it's coaching, not punishment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Act if unchanged, accept the cost&lt;/td&gt;
&lt;td&gt;culture &amp;gt; any single IC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The answer refuses the two failure modes: tolerating toxicity because the person is talented (which teaches the team that skill buys a pass on behaviour) and ambushing them with a punishment (which is unfair and usually backfires). It starts with respect and clarity, supports the change, and — crucially — commits to acting even if it means losing the best engineer, because the two departures the toxic behaviour is about to cause are the real business risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Behaviour changes&lt;/td&gt;
&lt;td&gt;keep a strong engineer, team stabilizes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behaviour partially changes&lt;/td&gt;
&lt;td&gt;keep coaching, monitor, protect juniors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behaviour doesn't change&lt;/td&gt;
&lt;td&gt;manage out; protect the two at-risk engineers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manager tolerates it&lt;/td&gt;
&lt;td&gt;lose 2 good engineers, culture rots&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Specific evidence over vibes&lt;/strong&gt;&lt;/strong&gt; — "you're toxic" is unactionable and easy to deny; two dated incidents with their effect on named behaviours are undeniable and coachable. Feedback is only useful when it's specific.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Direct feedback with good intent&lt;/strong&gt;&lt;/strong&gt; — assuming the person doesn't realize the impact, and saying so plainly and privately, gives the best chance of a change while preserving their dignity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;The non-negotiable behaviour bar&lt;/strong&gt;&lt;/strong&gt; — stating that standards apply to everyone, seniors included, prevents the corrosive lesson that talent buys a pass. This is the culture-setting move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Willingness to lose the top performer&lt;/strong&gt;&lt;/strong&gt; — the senior signal. A manager who won't part with a toxic star to save two good engineers is optimizing for short-term output over the team, which is exactly the judgment failure the interview is testing for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — acting may cost your single strongest IC and some short-term delivery; tolerating it costs two good engineers, the trust of the whole team, and your credibility as a manager. The trade favours acting decisively — the cost of inaction compounds across the whole team, not one person.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems to calibrate your team's technical hiring bar&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Design&lt;/span&gt;
&lt;span&gt;Topic — design&lt;/span&gt;
&lt;strong&gt;Design problems for coaching engineers through architecture&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Roadmaps, planning, and prioritization under constraints
&lt;/h2&gt;
&lt;h3&gt;
  
  
  A roadmap is a sequence of bets sized to real capacity — not a wishlist, and the interview tests what you cut
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8j8q0xcubhybhh0piz45.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8j8q0xcubhybhh0piz45.jpeg" alt="Iconographic roadmap-planning diagram — a quarterly gantt board with three swimlanes, a RICE scoring card ranking initiatives, and a capacity-math bar showing headcount converted to story points minus on-call and leave." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;roadmap planning&lt;/code&gt; is the discipline of converting infinite requests into a ranked, capacity-sized, dependency-aware sequence of bets — and because capacity is always finite and reality always changes, the real test of a data engineering manager is not the plan they draw when everything is calm but what they choose to &lt;em&gt;cut&lt;/em&gt; when they lose two engineers mid-quarter or the deadline moves.&lt;/strong&gt; Interviewers probe roadmaps because a manager who can't prioritize will let the loudest stakeholder set the agenda, over-commit the team, and then miss everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prioritization — turning requests into a ranking.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A framework beats a gut feel.&lt;/strong&gt; RICE (Reach × Impact × Confidence ÷ Effort) or a simpler impact-vs-effort 2×2 forces every request onto the same scale so you're comparing apples to apples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score, then sanity-check.&lt;/strong&gt; The number is an input to judgment, not a replacement for it — a strategic bet with a low RICE can still be right, but you should be able to say &lt;em&gt;why&lt;/em&gt; you're overriding the score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Say no explicitly.&lt;/strong&gt; Every yes is a no to something else. A roadmap without a visible "not now / not doing" list is a roadmap that's lying about capacity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Capacity — the math most managers skip.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headcount is not capacity.&lt;/strong&gt; Six engineers is not six engineers of output. Subtract on-call, meetings, interviews, holidays, leave, ramp time for new hires, and a keep-the-lights-on tax (incidents, maintenance, support).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserve for the unplanned.&lt;/strong&gt; Leave 15–25% unallocated; a plan booked to 100% has no slack for the incident that &lt;em&gt;will&lt;/em&gt; happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequence around dependencies.&lt;/strong&gt; If project B needs the platform work from project A, A must land first — a dependency-blind roadmap looks great and delivers nothing on time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The data-team keep-the-lights-on tax.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data teams carry unusual toil.&lt;/strong&gt; Broken upstream schemas, late data, backfills, ad-hoc analyst requests, and on-call for pipeline freshness eat real capacity every week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget it explicitly.&lt;/strong&gt; If KTLO is 30% of the team's time, a plan that assumes 100% availability for new work is fiction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stakeholder trade-offs and communication.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make the trade-off visible.&lt;/strong&gt; "If we take on your request, here's what slips" — decisions made in the open build trust; decisions made silently breed resentment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-plan out loud when reality changes.&lt;/strong&gt; A lost headcount or a moved deadline should trigger a transparent re-rank, not quiet heroics that end in a missed quarter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on roadmaps.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you prioritize when everything is urgent?" — a framework (RICE), capacity math, and an explicit no-list.&lt;/li&gt;
&lt;li&gt;"You lose two engineers mid-quarter — what do you do?" — re-rank, protect the SLA, cut the tail, communicate early.&lt;/li&gt;
&lt;li&gt;"How do you handle a VP who drops a top-priority request mid-quarter?" — surface the trade-off, let them choose what slips.&lt;/li&gt;
&lt;li&gt;"How do you estimate a data project with unknown data quality?" — spike first, estimate ranges, re-plan after discovery.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — a RICE prioritization table with a scoring calc
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; RICE forces competing initiatives onto one comparable scale so prioritization is a defensible ranking rather than a shouting match. It's a favourite interview prop because you can walk it end-to-end. Build the table and the scoring calc for four data-team initiatives.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reach.&lt;/strong&gt; How many people/systems this affects in a period (users, teams, pipelines).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impact.&lt;/strong&gt; How much it moves the needle per unit reached (a 0.25–3 scale).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence.&lt;/strong&gt; How sure you are of Reach and Impact (a percentage).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effort.&lt;/strong&gt; Person-weeks. RICE = (Reach × Impact × Confidence) ÷ Effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Score four initiatives with RICE and produce the ranked roadmap order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Initiative&lt;/th&gt;
&lt;th&gt;Reach&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Confidence&lt;/th&gt;
&lt;th&gt;Effort (pw)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Self-serve metrics layer&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migrate legacy Airflow → managed&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New revenue dashboard (VP ask)&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data-quality alerting framework&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# RICE scoring — (Reach * Impact * Confidence) / Effort
&lt;/span&gt;&lt;span class="n"&gt;initiatives&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# name,                         reach, impact, conf, effort_pw
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Self-serve metrics layer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Airflow -&amp;gt; managed migration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revenue dashboard (VP ask)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data-quality alerting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reach&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;impact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effort&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;reach&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;impact&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;effort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;initiatives&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; RICE=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Data-quality alerting              RICE=14.4
# 2. Self-serve metrics layer           RICE=20.0
# 3. Revenue dashboard (VP ask)         RICE=15.8
# 4. Airflow -&amp;gt; managed migration       RICE=5.6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each initiative is scored on the same four factors, which is the whole point: the self-serve metrics layer (RICE 20.0) beats the VP's revenue dashboard (15.8) despite the dashboard's higher raw impact, because the metrics layer reaches far more people. RICE surfaces that reach-times-breadth comparison that gut feel misses.&lt;/li&gt;
&lt;li&gt;Effort in the denominator rewards cheap high-value work: the revenue dashboard scores respectably (15.8) mostly because it's only 4 person-weeks. Small-and-valuable rises; big-and-speculative sinks.&lt;/li&gt;
&lt;li&gt;The Airflow migration scores lowest (5.6) — it's real work but narrow reach and modest per-unit impact. RICE correctly flags it as "important but not now," which is exactly the kind of item that eats a roadmap if you let infrastructure enthusiasm override the numbers.&lt;/li&gt;
&lt;li&gt;Confidence is the honesty knob: the VP dashboard's 0.7 confidence (are the impact assumptions real?) pulls its score down appropriately. Padding confidence to make a pet project win is the most common way people game RICE — and interviewers will ask how you keep confidence honest.&lt;/li&gt;
&lt;li&gt;The output is a &lt;em&gt;defensible&lt;/em&gt; ranking you can show a stakeholder. When the VP asks why their dashboard is second, you can point at the metrics layer's reach — the conversation is about the inputs, not your taste.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Initiative&lt;/th&gt;
&lt;th&gt;RICE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Self-serve metrics layer&lt;/td&gt;
&lt;td&gt;20.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Revenue dashboard (VP ask)&lt;/td&gt;
&lt;td&gt;15.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Data-quality alerting&lt;/td&gt;
&lt;td&gt;14.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Airflow → managed migration&lt;/td&gt;
&lt;td&gt;5.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Use RICE (or a simpler impact/effort 2×2) to turn prioritization into a defensible ranking, keep the confidence factor honest, and treat the score as an input to judgment — you can override it for a strategic bet, but you must be able to say why. A ranking you can defend with inputs beats an opinion you defend with seniority.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — capacity math and the quarter-plan doc
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The most common planning error is treating headcount as capacity. Real capacity is headcount minus everything that isn't new-feature work, and interviewers love watching you do the subtraction. Walk through the math for a six-person team and turn it into a quarter plan.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with raw weeks.&lt;/strong&gt; 6 engineers × 12 weeks = 72 person-weeks in the quarter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subtract the taxes.&lt;/strong&gt; On-call, meetings/planning, holidays/leave, ramp for a new hire, and KTLO/incidents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserve slack.&lt;/strong&gt; Keep ~20% unallocated for the unplanned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Compute the six-person team's real quarterly capacity and lay out a quarter plan that fits it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deduction&lt;/th&gt;
&lt;th&gt;Person-weeks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw (6 × 12)&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-call rotation&lt;/td&gt;
&lt;td&gt;−6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meetings / planning / 1:1s (~15%)&lt;/td&gt;
&lt;td&gt;−10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Holidays + planned leave&lt;/td&gt;
&lt;td&gt;−6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New-hire ramp (1 hire, half-productive)&lt;/td&gt;
&lt;td&gt;−6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KTLO / incidents / ad-hoc (~20%)&lt;/td&gt;
&lt;td&gt;−14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack reserve (~15% of remainder)&lt;/td&gt;
&lt;td&gt;−5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Q4 2026 Data Platform — Quarter Plan&lt;/span&gt;
&lt;span class="ge"&gt;_Team: 6 engineers. Real capacity after taxes: ~25 person-weeks._&lt;/span&gt;

&lt;span class="gu"&gt;## Capacity&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Raw: 72 pw  |  After on-call/meetings/leave/ramp/KTLO/slack: ~25 pw
&lt;span class="p"&gt;-&lt;/span&gt; Rule: we plan to ~25 pw of NEW work, not 72.

&lt;span class="gu"&gt;## Committed (fits in ~25 pw)&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Self-serve metrics layer — phase 1        (16 pw)  [RICE #1]
&lt;span class="p"&gt;2.&lt;/span&gt; Data-quality alerting — MVP                (8 pw)  [RICE #3]
   → total committed: 24 pw

&lt;span class="gu"&gt;## Stretch (only if slack materializes)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Revenue dashboard (VP ask)                  (4 pw)  [RICE #2]

&lt;span class="gu"&gt;## Not now (explicit no-list)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Airflow -&amp;gt; managed migration               (8 pw)  [RICE #4] — Q1
&lt;span class="p"&gt;-&lt;/span&gt; Real-time CDC pilot                          — needs discovery spike first

&lt;span class="gu"&gt;## Dependencies&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Data-quality alerting depends on the metrics layer's event schema
  → metrics layer phase 1 must land by week 6.

&lt;span class="gu"&gt;## Risks&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; If the new hire ramps slower, drop data-quality alerting to Q1.
&lt;span class="p"&gt;-&lt;/span&gt; KTLO spike (schema breakages) is the top threat to the plan.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The headline number does the teaching: 72 raw person-weeks collapse to about 25 of real new-feature capacity once you subtract on-call, meetings, leave, ramp, KTLO, and slack. A manager who plans to 72 will over-commit by nearly 3× and miss everything.&lt;/li&gt;
&lt;li&gt;The new-hire ramp deduction (−6) captures a subtlety interviewers probe: a new hire is a &lt;em&gt;negative&lt;/em&gt; to short-term capacity (they consume mentoring time) before they're a positive. Adding a head mid-quarter doesn't add a head of output that quarter.&lt;/li&gt;
&lt;li&gt;Committing to 24 of the ~25 available person-weeks leaves the plan realistic; the revenue dashboard is explicitly "stretch," which tells the VP the truth — it happens only if slack materializes — rather than a false promise.&lt;/li&gt;
&lt;li&gt;The explicit no-list ("Airflow migration → Q1," "CDC pilot needs a spike") is the most important section. It makes the trade-offs visible and gives you something concrete to point at when a new request lands: "yes, and here's what moves to make room."&lt;/li&gt;
&lt;li&gt;The dependencies and risks sections turn the plan from a wishlist into a bet with a sequence and a contingency: the metrics layer must land by week 6 because data-quality alerting depends on its schema, and the named fallback (drop alerting to Q1 if ramp is slow) is the pre-agreed cut.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;th&gt;Person-weeks&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Committed&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;fits real capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stretch&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;only if slack appears&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not now&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;td&gt;explicit, dated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack reserve&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;protected for the unplanned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Compute real capacity by subtracting on-call, meetings, leave, new-hire ramp, KTLO, and a ~15–20% slack reserve from raw person-weeks — then plan to that number, not to headcount. A quarter plan with a committed list, a stretch list, and an explicit dated no-list is a plan; a list of everything everyone wants is a wish.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — sequencing a dependency chain
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A roadmap that ignores dependencies looks fully loaded and delivers nothing on time, because half the work is blocked on the other half. Interviewers probe this with "walk me through how you'd sequence these." Walk through ordering a four-project chain where later work depends on earlier platform work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identify the edges.&lt;/strong&gt; Which project needs an output of which other project?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topologically order.&lt;/strong&gt; Blockers first; parallelize what's independent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the critical path.&lt;/strong&gt; The longest dependency chain sets the floor on the timeline, regardless of how many people you add.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Given four projects with dependencies, produce a delivery sequence and identify the critical path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Depends on&lt;/th&gt;
&lt;th&gt;Effort (pw)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A — event schema / contract&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B — self-serve metrics layer&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C — data-quality alerting&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D — revenue dashboard&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dependency graph and sequence
=============================

   A (3) ──► B (16) ──► D (4)
      └────► C (10)

Topological order:  A first, then B and C in parallel, then D.

Critical path (longest chain):  A → B → D = 3 + 16 + 4 = 23 pw
  (C at 10pw runs alongside B; it is NOT on the critical path.)

Sequencing decisions:
  - A is the bottleneck-unlock: nothing starts until the schema
    contract lands. Staff it first, even though it's small.
  - B and C are independent after A — run them in parallel if
    capacity allows; if not, B first (D is blocked on it).
  - Adding people to C does NOT speed up delivery of D, because
    D waits on B. Throwing bodies at the wrong project is the
    classic sequencing mistake.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Project A (the event schema/contract) is small at 3 person-weeks but blocks everything, so it must be staffed first despite its size. New managers often deprioritize small foundational work because it looks low-impact in isolation — sequencing shows why that's wrong.&lt;/li&gt;
&lt;li&gt;Once A lands, B and C are independent and can run in parallel if capacity allows. Recognizing the parallelizable branches is how you compress the timeline without adding scope.&lt;/li&gt;
&lt;li&gt;The critical path is A → B → D = 23 person-weeks. This is the &lt;em&gt;floor&lt;/em&gt; on the timeline: even with infinite engineers you can't finish D before 23 weeks of sequential work complete, because each link waits on the previous.&lt;/li&gt;
&lt;li&gt;The key insight interviewers want: adding people to C does nothing for D's delivery date, because D is blocked on B, not C. Understanding that capacity only helps &lt;em&gt;on the critical path&lt;/em&gt; is the difference between effective and wasted staffing.&lt;/li&gt;
&lt;li&gt;In practice this changes staffing: pour effort into A then B (the critical path), and treat C as fill-in work for whoever isn't needed on the path. The sequence, not the raw effort sum, drives the plan.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Projects active&lt;/th&gt;
&lt;th&gt;Cumulative weeks (critical path)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;B (and C in parallel)&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Sequence by dependency: staff the small blockers first, parallelize independent branches, and find the critical path — the longest chain sets the timeline floor. Adding people off the critical path speeds up nothing; that misallocation is the most common planning error interviewers test for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on planning under constraints
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're three weeks into a quarter with a committed roadmap when two of your six engineers resign, and your VP still expects the revenue dashboard on the original date. Walk me through exactly what you do in the next 48 hours and the next two weeks — the re-plan, what you cut, what you protect, and how you communicate it."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a transparent re-rank that protects the SLA, cuts the tail, and surfaces the trade-off
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Re-plan after losing 2 of 6 engineers mid-quarter&lt;/span&gt;

&lt;span class="gu"&gt;## Next 48 hours — stop the bleeding, get the facts&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Recompute capacity: 6 → 4 engineers, minus the same taxes,
   minus knowledge-transfer time from the two leaving.
   Real new-work capacity roughly HALVES (~25pw → ~12pw).
&lt;span class="p"&gt;2.&lt;/span&gt; Protect keep-the-lights-on FIRST: on-call and pipeline SLAs
   are non-negotiable; a data team that stops delivering fresh
   data loses trust faster than one that ships a feature late.
&lt;span class="p"&gt;3.&lt;/span&gt; Capture the leavers' knowledge: runbooks, ownership handoff,
   pair sessions before they go. Bus-factor is the acute risk.

&lt;span class="gu"&gt;## Next 2 weeks — re-rank and communicate&lt;/span&gt;
&lt;span class="p"&gt;4.&lt;/span&gt; Re-run RICE against the new ~12pw capacity. The committed list
   no longer fits. Cut from the BOTTOM of the ranking, not the top.
&lt;span class="p"&gt;5.&lt;/span&gt; Present the VP a CHOICE, not a no:
   "With 4 engineers I have ~12pw of new work this quarter.
    I can deliver the revenue dashboard on time OR the metrics
    layer, not both. The dashboard is 4pw and high-visibility;
    the metrics layer is our biggest long-term lever. Here's my
    recommendation and the trade-off — which do you want?"
&lt;span class="p"&gt;6.&lt;/span&gt; Cut the tail explicitly and publish the revised plan:
&lt;span class="p"&gt;   -&lt;/span&gt; KEEP: SLA/on-call, revenue dashboard (VP's call), metrics
     layer phase 1 only if it fits.
&lt;span class="p"&gt;   -&lt;/span&gt; CUT to Q1: data-quality alerting, Airflow migration.
&lt;span class="p"&gt;7.&lt;/span&gt; Do NOT solve it with heroics/overtime — that hides the problem,
   burns out the remaining 4, and risks a third resignation.

&lt;span class="gu"&gt;## Ongoing&lt;/span&gt;
&lt;span class="p"&gt;8.&lt;/span&gt; Start backfill hiring immediately; set expectations that new
   hires are net-negative capacity this quarter.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recompute capacity honestly&lt;/td&gt;
&lt;td&gt;4 engineers ≠ 4/6 of output; KT time hurts more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protect SLA/on-call first&lt;/td&gt;
&lt;td&gt;trust dies faster on stale data than a late feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capture leaver knowledge&lt;/td&gt;
&lt;td&gt;bus-factor is the acute risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Re-rank, cut from the bottom&lt;/td&gt;
&lt;td&gt;preserve the highest-value work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Give the VP a choice, not a no&lt;/td&gt;
&lt;td&gt;surface the trade-off; let them own the priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refuse heroics&lt;/td&gt;
&lt;td&gt;overtime hides the problem and risks a 3rd exit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The answer's spine is &lt;em&gt;transparency over heroics&lt;/em&gt;: it recomputes capacity honestly, protects the non-negotiable SLA before feature work, and converts the VP's impossible expectation into an explicit either/or the VP gets to decide. It cuts from the bottom of a re-ranked list so the highest-value work survives, and it explicitly rejects the tempting-but-fatal move of covering the gap with overtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;On-call / pipeline SLA&lt;/td&gt;
&lt;td&gt;protected (non-negotiable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue dashboard (VP ask)&lt;/td&gt;
&lt;td&gt;VP chooses; trade-off surfaced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metrics layer phase 1&lt;/td&gt;
&lt;td&gt;only if it fits ~12pw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data-quality alerting&lt;/td&gt;
&lt;td&gt;cut to Q1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Airflow migration&lt;/td&gt;
&lt;td&gt;cut to Q1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team overtime&lt;/td&gt;
&lt;td&gt;explicitly avoided&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Honest capacity recomputation&lt;/strong&gt;&lt;/strong&gt; — losing 2 of 6 doesn't cut output by a third; knowledge-transfer time and lost redundancy make the real hit closer to half. Managers who do this math avoid re-committing to an impossible plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Protect the SLA first&lt;/strong&gt;&lt;/strong&gt; — a data team's trust is built on reliable, fresh data; letting the pipelines rot to chase a feature is the wrong trade. Keep-the-lights-on comes before new work under constraint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Re-rank and cut from the bottom&lt;/strong&gt;&lt;/strong&gt; — reprioritizing against the new capacity and cutting the lowest-value items preserves the most valuable work, rather than slicing a little off everything (which delivers nothing well).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Give the VP a choice, not a no&lt;/strong&gt;&lt;/strong&gt; — surfacing the explicit trade-off ("dashboard OR metrics layer, your call") respects their authority over priority while being honest about capacity. It moves the decision to the right owner and builds trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the honest path costs you a hard conversation with a VP and a publicly reduced roadmap. The heroics path "costs" nothing visible now but risks burnout, a third resignation, and a bigger miss later. Transparency is cheaper than heroics once you price in the compounding risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Optimization&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Optimization problems that sharpen prioritization instinct&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Design&lt;/span&gt;
&lt;span&gt;Topic — design&lt;/span&gt;
&lt;strong&gt;Design problems for sequencing dependent systems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Platform vs product: the central data-team trade-off
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Platform buys leverage, product buys impact — the interview tests whether you can sequence them, not pick one forever
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F73ga4lglu5e0n1deh5s3.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F73ga4lglu5e0n1deh5s3.jpeg" alt="Iconographic platform-vs-product diagram — a balance scale weighing a platform side (shared pipelines, reusable framework) against a product side (customer-facing data feature), with a build-vs-buy scorecard and a tech-debt budget gauge." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;platform vs product&lt;/code&gt; is the defining tension of data-team leadership — platform work (shared pipelines, reusable frameworks, self-serve tooling) buys &lt;em&gt;leverage&lt;/em&gt; that compounds across many teams over time, while product work (customer-facing data features, dashboards, the thing the business asked for) buys &lt;em&gt;impact&lt;/em&gt; that shows up this quarter — and the senior answer is never "always platform" or "always product" but a defensible judgment about &lt;em&gt;when&lt;/em&gt; each pays, backed by a build-vs-buy discipline and a tech-debt budget.&lt;/strong&gt; Interviewers probe this because it's where data managers most often go wrong: over-investing in a gold-plated platform nobody adopts, or drowning in one-off product asks until the team can't move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When platform investment pays.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The rule of three.&lt;/strong&gt; When the same pain shows up across three or more teams or projects, a shared solution starts to earn its keep. Solving it once for one team is product; solving it once for everyone is platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leverage compounds.&lt;/strong&gt; A self-serve pipeline framework that saves each of ten teams two weeks a quarter returns far more than the feature you'd have shipped instead — but only if it's actually adopted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The adoption trap.&lt;/strong&gt; Platform value is zero until people use it. A beautiful internal tool with no users is worse than no tool — it cost the quarter and delivered nothing. Treat internal platforms like products: find users, solve their real pain, measure adoption.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When product wins.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Impact is now, and now sometimes matters most.&lt;/strong&gt; A revenue-driving dashboard or a launch-blocking data feature can be the right call even when platform work is "more strategic," because credibility and business trust are earned on delivery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Premature platforming is a classic failure.&lt;/strong&gt; Building the general framework before you understand the specific problem produces the wrong abstraction. Ship the specific thing two or three times, &lt;em&gt;then&lt;/em&gt; extract the platform once the pattern is clear.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Build vs buy — the discipline underneath.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Buy the undifferentiated; build the differentiating.&lt;/strong&gt; If a vendor solves it well and it isn't your competitive edge (orchestration, CDC, BI), buy or adopt open-source. Build only what's genuinely specific to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total cost of ownership, not sticker price.&lt;/strong&gt; Build has a large hidden maintenance tail; buy has recurring cost and lock-in. Score both on cost, time-to-value, fit, and long-run maintenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tech-debt budgeting and platform ROI.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Budget debt paydown explicitly.&lt;/strong&gt; Reserve ~15–20% of capacity for reliability, refactoring, and debt, continuously — not "we'll fix it later," which means never.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure platform ROI.&lt;/strong&gt; Adoption (teams/pipelines using it), time saved per team, and reliability delta. If you can't measure it, you can't defend it — and a VP will ask.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on platform vs product.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you decide between building a platform and shipping the feature?" — rule of three, adoption, sequence.&lt;/li&gt;
&lt;li&gt;"Justify a platform investment to a product-focused VP." — frame in their currency: throughput, time saved, risk reduced.&lt;/li&gt;
&lt;li&gt;"When have you built the wrong abstraction?" — premature platforming story with the lesson.&lt;/li&gt;
&lt;li&gt;"How much of your team's time goes to tech debt?" — a deliberate budget, not zero and not ad hoc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the platform-vs-product decision matrix
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The strong answer to "how do you decide?" is a repeatable rubric, not a preference. A simple decision matrix scores a candidate initiative on the factors that actually predict whether platform investment pays. Walk through scoring three candidate initiatives.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repetition.&lt;/strong&gt; How many teams/projects hit this same pain?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compounding.&lt;/strong&gt; Does solving it once keep paying, or is it one-and-done?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adoption certainty.&lt;/strong&gt; Will people actually use it, or is it "build it and hope"?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opportunity cost.&lt;/strong&gt; What product impact are we giving up to build it now?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Score three initiatives on the platform-vs-product matrix and decide platform-now, product-now, or wait.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Initiative&lt;/th&gt;
&lt;th&gt;Teams hit&lt;/th&gt;
&lt;th&gt;Compounding?&lt;/th&gt;
&lt;th&gt;Adoption certainty&lt;/th&gt;
&lt;th&gt;Opportunity cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shared ingestion framework&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;high (teams asking)&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One team's custom export&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic ML feature store&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;low (speculative)&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Platform-vs-product decision matrix
===================================

Score each factor 1-3; platform-now needs repetition AND
compounding AND adoption certainty to clear the bar.

                       Repeat  Compound  Adoption  OppCost   Verdict
Shared ingestion fwk     3        3         3        2      PLATFORM NOW
  → 6 teams, compounds, teams are literally asking. Build it.

One team's custom export  1        1         -        3      PRODUCT NOW
  → single team, no reuse. Just ship it as a feature; don't
    "platformize" a one-off.

Generic ML feature store  2        2         1        1      WAIT
  → only 2 teams, adoption speculative, high opp cost. Ship
    the specific feature 2-3x first, THEN extract the platform
    if the pattern holds. Premature = wrong abstraction.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The shared ingestion framework clears every gate: six teams hit the same pain, the solution compounds, and teams are already asking (adoption is near-certain). This is the textbook platform-now case — you're extracting a pattern that's already proven across the org.&lt;/li&gt;
&lt;li&gt;The one-team custom export scores the opposite: single team, no reuse, one-and-done. The right call is to ship it as a plain feature and resist the urge to "platformize" it — generalizing a one-off produces cost with no leverage.&lt;/li&gt;
&lt;li&gt;The ML feature store is the dangerous middle: it &lt;em&gt;sounds&lt;/em&gt; strategic, but only two teams need it and adoption is speculative. The matrix says wait — ship the specific feature two or three times first, then extract the platform once the real pattern is clear. This is the guard against premature abstraction.&lt;/li&gt;
&lt;li&gt;Adoption certainty is the factor that most often gets ignored and most often kills platforms. A high-compounding idea with low adoption certainty is a bet, not a sure thing; treat it as one and de-risk it before committing a quarter.&lt;/li&gt;
&lt;li&gt;The matrix turns "platform vs product" from a philosophy debate into a per-initiative decision you can defend. In an interview, walking a specific initiative through these gates is far stronger than asserting "I balance both."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Initiative&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shared ingestion framework&lt;/td&gt;
&lt;td&gt;Platform now&lt;/td&gt;
&lt;td&gt;repeats, compounds, adoption certain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One team's custom export&lt;/td&gt;
&lt;td&gt;Product now&lt;/td&gt;
&lt;td&gt;one-off, no reuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic ML feature store&lt;/td&gt;
&lt;td&gt;Wait&lt;/td&gt;
&lt;td&gt;speculative adoption, high opp cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Decide platform-vs-product per initiative on repetition, compounding, and adoption certainty — platform-now requires all three, a genuine one-off ships as product, and a speculative "strategic platform" waits until you've shipped the specific version enough times to know the right abstraction. Premature platforming is the most expensive mistake a data manager can make.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a build-vs-buy scorecard
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; "Build or buy?" is a near-guaranteed probe, and the strong answer scores both on total cost of ownership, not the sticker price. Walk through a scorecard for a CDC/ingestion capability the team needs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Score dimensions.&lt;/strong&gt; Upfront cost, time-to-value, fit to your needs, long-run maintenance, and strategic control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hidden tail.&lt;/strong&gt; Build's real cost is the years of maintenance, on-call, and feature-chasing after v1 ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The heuristic.&lt;/strong&gt; Buy/adopt the undifferentiated heavy-lifting; build only the genuinely differentiating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Score build vs buy vs adopt-open-source for a CDC ingestion capability and make the call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Build in-house&lt;/th&gt;
&lt;th&gt;Buy managed SaaS&lt;/th&gt;
&lt;th&gt;Adopt open-source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to value&lt;/td&gt;
&lt;td&gt;slow (months)&lt;/td&gt;
&lt;td&gt;fast (weeks)&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upfront cost&lt;/td&gt;
&lt;td&gt;high (eng time)&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ongoing cost&lt;/td&gt;
&lt;td&gt;high (maintenance)&lt;/td&gt;
&lt;td&gt;recurring $$&lt;/td&gt;
&lt;td&gt;medium (self-host)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fit to needs&lt;/td&gt;
&lt;td&gt;perfect&lt;/td&gt;
&lt;td&gt;good&lt;/td&gt;
&lt;td&gt;good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance burden&lt;/td&gt;
&lt;td&gt;all on us&lt;/td&gt;
&lt;td&gt;vendor&lt;/td&gt;
&lt;td&gt;shared/community&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strategic control&lt;/td&gt;
&lt;td&gt;full&lt;/td&gt;
&lt;td&gt;low (lock-in)&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build-vs-buy scorecard (CDC ingestion) — score 1-5, higher=better
=================================================================

Dimension          Build   Buy(SaaS)  Adopt(OSS)
------------------------------------------------
Time to value        2         5          4
Upfront cost         2         5          3
Ongoing cost (TCO)   2         3          4
Fit to needs         5         4          4
Maintenance burden   1         5          3
Strategic control    5         2          5
------------------------------------------------
TOTAL               17        24         23

Decision: BUY (managed) or ADOPT (OSS) — NOT build.
  - CDC ingestion is undifferentiated heavy-lifting; it is not our
    competitive edge. Building it means owning a maintenance tail
    for years to reinvent a solved problem.
  - Choose managed SaaS if speed matters most and budget allows.
  - Choose OSS (e.g. a Debezium-based stack) if strategic control
    and TCO matter more than time-to-value.
  - Build ONLY the thin layer that IS differentiating (our
    domain-specific transforms), on top of the bought/adopted core.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scoring both options on total cost of ownership — not the license price — flips the naive intuition that "building is free because we have engineers." Build scores lowest (17) precisely because of the maintenance and time-to-value penalties that don't show up on a spreadsheet but dominate the real cost.&lt;/li&gt;
&lt;li&gt;Buy (SaaS) wins on speed and maintenance but is docked on strategic control and lock-in; OSS nearly ties it by trading a bit of time-to-value for control and better long-run TCO. The scorecard makes the trade-off explicit rather than a matter of taste.&lt;/li&gt;
&lt;li&gt;The core heuristic is the tiebreaker: CDC ingestion is undifferentiated heavy-lifting — a solved problem — so building it reinvents a wheel and saddles the team with a multi-year maintenance tail for no competitive gain.&lt;/li&gt;
&lt;li&gt;The nuanced senior move is the last line: build only the &lt;em&gt;thin differentiating layer&lt;/em&gt; (your domain-specific transforms) on top of a bought or adopted core. This captures the "build the edge, buy the commodity" principle rather than treating it as all-or-nothing.&lt;/li&gt;
&lt;li&gt;The choice between SaaS and OSS then comes down to what the org values more — speed and low maintenance (SaaS) or control and TCO (OSS) — which is a conversation you can have concretely because the scorecard laid out the axes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;When to choose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Buy (managed SaaS)&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;speed + low maintenance priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adopt (OSS)&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;control + TCO priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build in-house&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;only the differentiating thin layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Score build vs buy on total cost of ownership — including build's multi-year maintenance tail — and default to buying or adopting the undifferentiated heavy-lifting while building only the thin layer that's genuinely your competitive edge. "We'll build it, we have engineers" ignores the maintenance tail that sinks the real cost.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — tech-debt budgeting and measuring platform ROI
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Two probes cluster here: "how much time goes to tech debt?" and "how do you prove the platform was worth it?" The strong answers are a deliberate, standing debt budget and a small set of ROI metrics. Walk through both.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The debt budget.&lt;/strong&gt; A fixed, continuous slice of capacity (~15–20%) for reliability, refactoring, and paydown — protected, not raided when deadlines loom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform ROI metrics.&lt;/strong&gt; Adoption (teams/pipelines using it), time saved per team, and reliability delta (incidents, freshness). Measure them or you can't defend the investment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Define a tech-debt budget policy and the ROI metric set for the shared ingestion framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lever&lt;/th&gt;
&lt;th&gt;Policy / metric&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Debt budget&lt;/td&gt;
&lt;td&gt;20% of capacity, standing, protected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adoption metric&lt;/td&gt;
&lt;td&gt;# teams / pipelines migrated onto the framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Efficiency metric&lt;/td&gt;
&lt;td&gt;eng-days saved per new pipeline vs before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability metric&lt;/td&gt;
&lt;td&gt;pipeline incidents + freshness-SLA breaches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review cadence&lt;/td&gt;
&lt;td&gt;quarterly ROI review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tech-debt budget + platform ROI dashboard
=========================================

Tech-debt policy:
  - Reserve 20% of every sprint for reliability + debt paydown.
  - It is PROTECTED: not the first thing cut when a deadline slips.
    (Raiding the debt budget is borrowing at high interest.)
  - Track debt as a visible backlog, ranked, with a paydown rate.

Platform ROI (shared ingestion framework) — quarterly:
  Adoption:     8 / 10 teams migrated   (target 10)   ↑ from 3
  Efficiency:   new pipeline now 2 eng-days vs 9 before  (−78%)
  Reliability:  pipeline incidents 12/qtr → 4/qtr        (−67%)
  Freshness:    SLA breaches 15/mo → 3/mo                (−80%)

  ROI framing for a VP (their currency, not ours):
    "The framework let us ship 40 new pipelines this quarter with
     the same headcount that shipped 9 last year, and cut data
     incidents two-thirds. That's throughput and reliability, not
     an internal science project."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;em&gt;standing&lt;/em&gt; 20% debt budget beats "we'll fix it after the deadline," which is how debt compounds until the team grinds to a halt. Making it a fixed, recurring reserve normalizes paydown as part of the work rather than an apology.&lt;/li&gt;
&lt;li&gt;The protection clause is the whole point: the debt budget must not be the first casualty when a deadline slips. Raiding it is borrowing at high interest — you get a little speed now and pay it back with reliability incidents later.&lt;/li&gt;
&lt;li&gt;Tracking debt as a visible, ranked backlog with a paydown rate makes it manageable and defensible; "we have a lot of tech debt" is a vibe, "we have 40 ranked items and pay down 6 a quarter" is a plan.&lt;/li&gt;
&lt;li&gt;The ROI metrics answer the VP's real question — was the platform worth it? Adoption (8/10 teams), efficiency (2 vs 9 eng-days per pipeline), and reliability (incidents down 67%) are concrete and hard to argue with. Without them, the platform is faith-based.&lt;/li&gt;
&lt;li&gt;The framing line translates engineering wins into the VP's currency — throughput and reliability, not "internal tooling." This is the skill that gets platform investment funded: speaking impact, not architecture.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Teams on framework&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;+5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eng-days per new pipeline&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;−78%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline incidents / qtr&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;−67%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness SLA breaches / mo&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;−80%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Run a standing, protected ~20% tech-debt budget and measure platform ROI in adoption, time saved, and reliability delta — then translate those into the VP's currency (throughput, reliability, risk). A platform you can't measure is a platform you can't defend, and a debt budget you raid under pressure is one you don't really have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on platform vs product
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your product-focused VP wants the whole team on customer-facing data features next quarter. You believe a shared ingestion platform is the higher-leverage investment because six teams keep rebuilding the same brittle pipelines. Convince me — as that VP — to give you a quarter for platform work instead of features."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a business-currency case, a de-risked scope, and a measurable ROI commitment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pitching a platform investment to a product-focused VP
======================================================

Frame in THEIR currency (throughput, revenue, risk — not architecture):
  "Right now six teams each rebuild the same ingestion pipeline.
   We spend ~9 engineer-days per new pipeline and we had 12 data
   incidents last quarter — several delayed the very product
   launches you care about. This isn't an internal science
   project; it's the tax slowing down every feature we ship."

Quantify the return:
  "A shared ingestion framework cuts a new pipeline from ~9 days
   to ~2, and should cut data incidents by more than half. Across
   six teams that's roughly [X] engineer-weeks a quarter back into
   product work — permanently, compounding every quarter after."

De-risk the ask (don't ask for a blank quarter):
  "I'm not asking to disappear for a quarter. I'll take 2 engineers
   for 6 weeks on a phase-1 framework that migrates the 2 teams in
   the most pain, while the rest of the team keeps shipping your
   features. We prove adoption and the time-saved number before we
   scale it."

Commit to a measurable checkpoint:
  "At 6 weeks I'll show you: teams migrated, eng-days saved per
   pipeline, and incident delta. If the numbers aren't there, we
   stop and put everyone back on features. You hold me to that."

Acknowledge the trade-off honestly:
  "The cost is 2 engineers off features for 6 weeks — one feature
   slips a sprint. The return is faster feature delivery for every
   quarter after. I think that trade is worth it; here's the data."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;th&gt;Why it lands with a product VP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frame in throughput/risk, not architecture&lt;/td&gt;
&lt;td&gt;speaks their language, not yours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantify time saved + incidents avoided&lt;/td&gt;
&lt;td&gt;makes leverage concrete and credible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;De-risk: phase-1, 2 engineers, 6 weeks&lt;/td&gt;
&lt;td&gt;not a blank-cheque quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Measurable checkpoint with a kill switch&lt;/td&gt;
&lt;td&gt;shows accountability, lowers their risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Name the trade-off honestly&lt;/td&gt;
&lt;td&gt;builds trust, not a hard sell&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pitch never argues architecture with a VP who doesn't care about it. It reframes the platform as a &lt;em&gt;tax on the product velocity the VP wants&lt;/em&gt;, quantifies the return in their currency, de-risks the ask to a small phased bet with a kill switch, and owns the trade-off. It gives the VP an easy, low-risk yes instead of an all-or-nothing philosophical fight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;The VP hears&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Currency framing&lt;/td&gt;
&lt;td&gt;"this helps my features ship faster"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantified ROI&lt;/td&gt;
&lt;td&gt;"the leverage is real and measured"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phased, de-risked scope&lt;/td&gt;
&lt;td&gt;"low downside, I can say yes"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkpoint + kill switch&lt;/td&gt;
&lt;td&gt;"I stay in control"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Honest trade-off&lt;/td&gt;
&lt;td&gt;"she's leveling with me"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Business-currency framing&lt;/strong&gt;&lt;/strong&gt; — a product VP funds throughput, revenue, and risk reduction, not "good architecture." Translating the platform into faster feature delivery makes it their win, not just yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Quantified, compounding ROI&lt;/strong&gt;&lt;/strong&gt; — concrete numbers (9→2 eng-days, incidents halved, across six teams, every quarter) turn a leverage argument from hand-waving into a defensible business case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;De-risked phased ask&lt;/strong&gt;&lt;/strong&gt; — requesting 2 engineers for 6 weeks on a phase-1 that targets the worst pain is a small bet, not a blank quarter. Small asks with proof-of-value get approved; grand ones get deferred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Measurable checkpoint with a kill switch&lt;/strong&gt;&lt;/strong&gt; — committing to show adoption and time-saved at 6 weeks, and to stop if the numbers aren't there, transfers risk off the VP and demonstrates the accountability a manager is supposed to have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the honest price is one feature slipping a sprint and 2 engineers off product for 6 weeks; the return is compounding velocity for every quarter after. Framed as a phased, measured bet, the expected value is strongly positive and the downside is capped — which is exactly how you win a resource argument.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems for platform-vs-product architecture calls&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on the reusable pipeline patterns platforms extract&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Cross-functional influence, metrics, and the behavioral loop
&lt;/h2&gt;
&lt;h3&gt;
  
  
  You get things done through influence and evidence — the behavioral loop tests both, and the failure story is the pivot
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6o7eex7rck8zgwbz0zrv.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6o7eex7rck8zgwbz0zrv.jpeg" alt="Iconographic behavioral-loop diagram — a STAR story card (situation, task, action, result), a data-team metrics panel with SLO / freshness / DORA gauges, and a managing-up arrow from a data team to stakeholders." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the last track is cross-functional leadership — managing up to your leadership, sideways to product and analytics, and outward to the rest of the company — measured through the behavioral loop, where every answer is really a request for &lt;em&gt;evidence&lt;/em&gt; that you can influence without authority, run a data team by the right metrics, lead an incident calmly, and learn from failure.&lt;/strong&gt; Interviewers weight this heavily for &lt;code&gt;team leadership&lt;/code&gt; because a data manager who can't influence stakeholders or represent the org will have a technically excellent team whose work nobody trusts or uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managing up and across.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Managing up is a skill, not sycophancy.&lt;/strong&gt; Give your leadership no-surprises visibility, bring problems with proposed options (not just problems), and align the team's work to what they're accountable for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Influence without authority.&lt;/strong&gt; You rarely command peers in product or analytics; you persuade with shared goals, data, and reliability. Being the team that ships and keeps its promises is the deepest source of influence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Represent the team outward.&lt;/strong&gt; Shield the team from chaos, translate business needs into technical work and technical constraints into business language, and take the blame publicly while giving credit publicly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Metrics for a data team.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reliability and freshness first.&lt;/strong&gt; Data teams live or die on trust: pipeline uptime, data-freshness SLOs/SLAs, and data-quality (incidents, failed checks) are the core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DORA-style delivery metrics, adapted.&lt;/strong&gt; Deployment frequency, lead time for change, change-failure rate, and mean-time-to-restore translate well to data engineering and signal delivery health without becoming a stack-ranking weapon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics inform, they don't rank.&lt;/strong&gt; Use them to find systemic problems, not to grade individuals — the moment a metric becomes a target for individual performance, it gets gamed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Incident leadership.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Calm, roles, comms.&lt;/strong&gt; As incident commander you assign roles, keep a running timeline, communicate status to stakeholders on a cadence, and protect the responders from the peanut gallery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blameless postmortems.&lt;/strong&gt; The goal is the systemic fix, not a culprit. "What in the system let this happen, and what did we change?" is the only useful question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The behavioral loop and STAR.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;STAR keeps you concrete.&lt;/strong&gt; Situation, Task, Action, Result — with the Result quantified. Rambling context with no outcome is the most common failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure question is the pivot.&lt;/strong&gt; "Tell me about a project that failed" tests ownership and learning. The strong answer owns it plainly, shows what you personally did, and — the key part — names the &lt;em&gt;system change&lt;/em&gt; you made so it can't recur.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on cross-functional leadership.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Tell me about a time you influenced a decision without authority." — shared goals + data.&lt;/li&gt;
&lt;li&gt;"How do you measure a data team's health?" — reliability/freshness SLOs + adapted DORA, not lines of code.&lt;/li&gt;
&lt;li&gt;"Walk me through an incident you led." — commander role, comms cadence, blameless postmortem.&lt;/li&gt;
&lt;li&gt;"Tell me about a project that failed and what you changed." — ownership + system change.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — a data-team metrics and SLO table with a freshness query
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; "How do you measure your team?" is a standard probe, and the strong answer is a small, balanced scorecard that leads with reliability and adapts DORA — plus the willingness to actually instrument it. Walk through the scorecard and a real freshness-SLO query.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reliability tier.&lt;/strong&gt; Pipeline uptime, data-freshness SLO, data-quality incidents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery tier (adapted DORA).&lt;/strong&gt; Deploy frequency, lead time, change-failure rate, MTTR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrail.&lt;/strong&gt; These are team/system health signals, never individual stack-ranking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Define the data-team scorecard and write the SQL that measures the freshness SLO for a critical table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline uptime&lt;/td&gt;
&lt;td&gt;reliability&lt;/td&gt;
&lt;td&gt;≥ 99.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness SLO (critical tables &amp;lt; 1h old)&lt;/td&gt;
&lt;td&gt;reliability&lt;/td&gt;
&lt;td&gt;≥ 99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data-quality incidents&lt;/td&gt;
&lt;td&gt;reliability&lt;/td&gt;
&lt;td&gt;≤ 3 / quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change-failure rate&lt;/td&gt;
&lt;td&gt;delivery (DORA)&lt;/td&gt;
&lt;td&gt;≤ 15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTTR (pipeline)&lt;/td&gt;
&lt;td&gt;delivery (DORA)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Freshness SLO: % of hourly windows where the critical table&lt;/span&gt;
&lt;span class="c1"&gt;-- was updated within its 1-hour freshness target, last 30 days.&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;loads&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loaded_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;load_hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loaded_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_load&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;pipeline_audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load_log&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'analytics.fct_orders'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;loaded_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;windows&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;load_hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;-- "fresh" if the load landed within 60 min of the hour boundary&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_load&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;load_hour&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'60 minutes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;met_sla&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;loads&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                             &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_windows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;met_sla&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;windows_met&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;met_sla&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                 &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;nullif&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;freshness_slo_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- → freshness_slo_pct = 99.4  (meets the &amp;gt;= 99% target)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The scorecard deliberately leads with the reliability tier — uptime, freshness, quality — because a data team's entire value proposition is trustworthy, timely data. A team with dazzling delivery velocity but stale, wrong data has failed at the actual job.&lt;/li&gt;
&lt;li&gt;The delivery tier adapts DORA to data engineering: deploy frequency and lead time show how fast the team ships changes, change-failure rate and MTTR show how safely. These translate cleanly from software delivery and signal health without measuring "output" in a gameable way.&lt;/li&gt;
&lt;li&gt;The freshness query operationalizes the SLO: it buckets loads by hour, marks each window as meeting or missing the 60-minute target, and computes the percentage. This is the difference between &lt;em&gt;claiming&lt;/em&gt; an SLO and &lt;em&gt;measuring&lt;/em&gt; one — interviewers notice when you can write it.&lt;/li&gt;
&lt;li&gt;The guardrail matters as much as the metrics: stating that these are team/system signals and never individual stack-ranking pre-empts the "metrics-driven micromanager" fear. The moment freshness becomes an individual's performance target, someone games the load_log.&lt;/li&gt;
&lt;li&gt;The balanced set resists both extremes — no vanity metrics (lines of code, tickets closed) and no single number that can be gamed. It's a small dashboard that tells you where the &lt;em&gt;system&lt;/em&gt; is unhealthy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Reading&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline uptime&lt;/td&gt;
&lt;td&gt;99.6%&lt;/td&gt;
&lt;td&gt;meets ≥ 99.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness SLO&lt;/td&gt;
&lt;td&gt;99.4%&lt;/td&gt;
&lt;td&gt;meets ≥ 99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data-quality incidents&lt;/td&gt;
&lt;td&gt;2 this qtr&lt;/td&gt;
&lt;td&gt;meets ≤ 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change-failure rate&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;td&gt;meets ≤ 15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTTR&lt;/td&gt;
&lt;td&gt;42 min&lt;/td&gt;
&lt;td&gt;meets &amp;lt; 1h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Measure a data team with a small balanced scorecard — reliability (uptime, freshness SLO, quality) first, adapted DORA (deploy frequency, lead time, change-failure rate, MTTR) second — instrumented for real, and use it to find systemic problems, never to stack-rank individuals. If you can write the freshness query, you sound like a manager who's actually run the dashboard.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a STAR story-bank template
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The behavioral loop is won or lost on preparation: strong candidates keep a bank of 8–12 stories mapped to the themes interviewers probe, each pre-structured as STAR with a quantified result. Walk through building the bank and one fully-worked entry.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cover the themes.&lt;/strong&gt; Conflict, failure, influence-without-authority, hard people call, ambiguity, a delivery win, a hard prioritization call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusable stories.&lt;/strong&gt; A good story often answers several prompts; a bank of 8–12 covers most loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quantify the Result.&lt;/strong&gt; A number or a concrete outcome, plus what you learned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design a STAR story-bank template and fill one entry for a "hard prioritization call" story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Story slot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Conflict&lt;/td&gt;
&lt;td&gt;two engineers feuding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure&lt;/td&gt;
&lt;td&gt;a project that missed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Influence w/o authority&lt;/td&gt;
&lt;td&gt;platform pitch to a VP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard people call&lt;/td&gt;
&lt;td&gt;managed out a toxic senior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ambiguity&lt;/td&gt;
&lt;td&gt;undefined data project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prioritization&lt;/td&gt;
&lt;td&gt;cut a roadmap under headcount loss&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# STAR story bank&lt;/span&gt;

&lt;span class="gu"&gt;## Slot: Hard prioritization call&lt;/span&gt;
&lt;span class="gs"&gt;**Prompts it answers:**&lt;/span&gt; "prioritize under pressure", "say no to a
stakeholder", "make a tough trade-off", "lose headcount".
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Situation:**&lt;/span&gt; 3 weeks into Q3, lost 2 of 6 engineers; VP still
  expected the revenue dashboard on the original date.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Task:**&lt;/span&gt; Re-plan to ~half the new-work capacity without breaking
  the pipeline SLA or burning out the remaining team.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Action:**&lt;/span&gt; Recomputed real capacity, protected on-call/SLA first,
  re-ran RICE, and gave the VP an explicit either/or (dashboard OR
  metrics layer) instead of quiet heroics. Cut two items to Q4,
  captured the leavers' runbooks, refused overtime.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Result:**&lt;/span&gt; Shipped the VP's dashboard on time and the SLA held
  at 99.5%; zero further attrition; the VP later cited the
  transparent trade-off as why they trusted the team's estimates.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Learned:**&lt;/span&gt; Surfacing the trade-off early beats absorbing it
  silently — I now publish a capacity number every planning cycle.

&lt;span class="gu"&gt;## Slot: Failure  → (see next worked example)&lt;/span&gt;
&lt;span class="gu"&gt;## Slot: Conflict → (toxic-senior story from section 2)&lt;/span&gt;
&lt;span class="gu"&gt;## Slot: Influence → (platform pitch from section 4)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Organizing by &lt;em&gt;theme&lt;/em&gt; rather than by story means you walk into the loop with a lookup table: any prompt maps to a prepared slot. This beats improvising, which is where candidates ramble or freeze.&lt;/li&gt;
&lt;li&gt;Listing the prompts each story answers ("prioritize under pressure", "say no", "lose headcount") makes stories reusable — one strong prioritization story covers four common questions, so a bank of 8–12 genuinely covers most loops.&lt;/li&gt;
&lt;li&gt;The STAR structure keeps the answer tight: a one-line Situation and Task, a specific Action (the actual decisions &lt;em&gt;you&lt;/em&gt; made), and a Result. The most common behavioral failure is 90 seconds of Situation and no Result.&lt;/li&gt;
&lt;li&gt;Quantifying the Result (dashboard on time, SLA held at 99.5%, zero attrition) turns a claim into evidence, and adding what you learned shows growth — interviewers explicitly probe for the learning.&lt;/li&gt;
&lt;li&gt;Cross-referencing slots to other real situations (the toxic-senior conflict story, the platform pitch) shows the bank is a system: the same well-run career furnishes evidence across every theme, which is exactly the coherence a panel is looking for.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;STAR element&lt;/th&gt;
&lt;th&gt;Weak version&lt;/th&gt;
&lt;th&gt;Strong version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Situation/Task&lt;/td&gt;
&lt;td&gt;long, meandering&lt;/td&gt;
&lt;td&gt;two crisp sentences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action&lt;/td&gt;
&lt;td&gt;"we decided"&lt;/td&gt;
&lt;td&gt;"&lt;em&gt;I&lt;/em&gt; did X, Y, Z"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;"it went well"&lt;/td&gt;
&lt;td&gt;quantified + what changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning&lt;/td&gt;
&lt;td&gt;omitted&lt;/td&gt;
&lt;td&gt;explicit system change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Walk into the loop with a bank of 8–12 STAR stories mapped to themes (conflict, failure, influence, hard people call, ambiguity, prioritization), each with a quantified Result and a lesson. Keep Situation short and Action first-person; the number in the Result is what makes it evidence instead of an assertion.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — leading an incident as commander
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; "Walk me through an incident you led" tests whether you can create calm and structure under pressure. The strong answer shows the incident-commander mechanics and a blameless follow-up. Walk through the structure with a data-freshness outage.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Declare and assign roles.&lt;/strong&gt; Commander (you, coordinating — not fixing), a lead investigator, a comms owner, a scribe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communicate on a cadence.&lt;/strong&gt; Regular stakeholder updates even when there's nothing new, so the org isn't guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blameless postmortem.&lt;/strong&gt; Timeline, contributing factors, and system-level action items with owners.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Structure your handling of a Sev-2 where the revenue pipeline is 6 hours stale during business hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Incident commander&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;coordinate, decide, shield the team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Investigator&lt;/td&gt;
&lt;td&gt;On-call DE&lt;/td&gt;
&lt;td&gt;find and fix root cause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comms&lt;/td&gt;
&lt;td&gt;You / delegate&lt;/td&gt;
&lt;td&gt;update stakeholders on cadence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scribe&lt;/td&gt;
&lt;td&gt;Anyone free&lt;/td&gt;
&lt;td&gt;keep the timeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incident command — revenue pipeline 6h stale (Sev-2)
====================================================

T+0   Declare Sev-2. Open a channel. Assign roles:
        me = commander (coordinate, do NOT dive into the fix),
        on-call DE = investigator, teammate = scribe.
T+5   First stakeholder comms: "Revenue data delayed, investigating,
        next update in 30 min." Set the cadence up front.
T+15  Investigator finds an upstream schema change broke ingestion.
        Commander decision: roll forward a fix vs backfill? → hotfix
        the parser now, backfill after.
T+30  Comms update on schedule (even though "still working").
T+50  Fix deployed; backfill running. Comms: "Pipeline restored,
        backfilling the 6h gap, ETA 40 min."
T+90  Backfill complete, data verified fresh. Declare resolved.
        Thank the team publicly.

Next day — BLAMELESS postmortem:
  - Timeline (from the scribe's notes).
  - Contributing factors: upstream changed schema without notice;
    we had no schema-contract check to catch it.
  - System action items (with owners + dates):
    1. Add a data-contract/schema-validation gate on ingestion.
    2. Establish a change-notification SLA with the upstream team.
    3. Add a freshness alert at 1h, not 6h.
  - No blame on the on-call engineer. The SYSTEM let it happen.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first move is declaring severity and assigning roles — including the counterintuitive one where the commander coordinates and explicitly does &lt;em&gt;not&lt;/em&gt; dive into the fix. A manager who grabs the keyboard loses the coordination the incident actually needs.&lt;/li&gt;
&lt;li&gt;Setting a comms cadence at T+5 ("next update in 30 min") and holding it even when there's no news is what keeps stakeholders calm and stops the flood of "any update?" pings that distract the responders. Predictable comms &lt;em&gt;is&lt;/em&gt; the leadership.&lt;/li&gt;
&lt;li&gt;The commander makes the call that requires a decision-maker (hotfix-now vs backfill-first) so the investigator can execute without debating trade-offs mid-fire. Clear decision ownership speeds resolution.&lt;/li&gt;
&lt;li&gt;Public thanks at resolution and shielding the on-call engineer set the culture: incidents are team events, not individual failures. This is what makes people willing to be on-call and to surface problems early.&lt;/li&gt;
&lt;li&gt;The blameless postmortem is where the real value is: it converts one outage into permanent system improvements (a schema-contract gate, an upstream change SLA, an earlier freshness alert). "What in the system let this happen?" — not "who broke it?" — is the only question that prevents recurrence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Detection → declaration&lt;/td&gt;
&lt;td&gt;roles assigned, calm established&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;During&lt;/td&gt;
&lt;td&gt;cadence comms, clear decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolution&lt;/td&gt;
&lt;td&gt;data restored + verified, team thanked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postmortem&lt;/td&gt;
&lt;td&gt;3 system fixes, zero blame&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Lead incidents as a commander who coordinates rather than codes: assign roles, communicate on a fixed cadence, own the decisions only you can make, and run a blameless postmortem that ships system-level fixes. The signal interviewers want is calm-plus-structure and "what did the system need," not heroics and not a culprit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the behavioral loop
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Tell me about a project that failed — one you owned. Walk me through what happened, your role in the failure, how you handled it in the moment, and specifically what you changed afterward so it couldn't happen again."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using plain ownership, honest role, and a concrete system change
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failure story — the analytics migration that missed (STAR)
==========================================================

Situation:
  "We committed to migrating the analytics warehouse to a new
   platform in one quarter, with a hard date tied to a finance
   reporting cycle."

Task:
  "I owned the migration end-to-end as the team's manager — scope,
   plan, and delivery."

Action / what went wrong (own MY part plainly):
  "We missed by six weeks. My mistake was concrete: I let the team
   commit to a fixed date before we'd done discovery on data-quality
   in the legacy source. When we started migrating, we found years
   of undocumented edge cases and silent nulls. I'd optimized for a
   confident-sounding plan over an honest one. In the moment I
   escalated early — told finance at week 4, not week 11 — gave them
   a realistic revised date, and stood up a parallel-run so they
   kept their old reports until the new ones were verified."

Result:
  "Finance had their reports (on the old system) with no gap, the
   migration landed 6 weeks late but correct, and no bad numbers
   ever reached a finance report."

What I CHANGED (the pivot — system, not just apology):
  "Three permanent changes: (1) no fixed external date before a
   time-boxed discovery spike — we now estimate ranges and commit
   after discovery; (2) a data-quality assessment is a mandatory
   first phase of any migration; (3) parallel-run is now our default
   migration pattern. The next migration used all three and landed
   on time."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;What it demonstrates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Owns the miss plainly (6 weeks late)&lt;/td&gt;
&lt;td&gt;no deflection, real accountability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names MY specific mistake&lt;/td&gt;
&lt;td&gt;self-awareness, not vague "we"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early escalation (week 4, not 11)&lt;/td&gt;
&lt;td&gt;handled it like a manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel-run protected the stakeholder&lt;/td&gt;
&lt;td&gt;judgment under a bad situation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three concrete system changes&lt;/td&gt;
&lt;td&gt;learning that generalizes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The answer resists the two failure modes: the humblebrag fake-failure ("I work too hard") and the blame-shift ("the upstream team let us down"). It owns a real, consequential miss, names the &lt;em&gt;specific&lt;/em&gt; decision that caused it, shows manager-grade handling in the moment (early escalation, protecting the stakeholder), and lands on concrete, permanent system changes that a later project actually used — which is the whole point of the question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;This answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Failure realness&lt;/td&gt;
&lt;td&gt;genuine, consequential miss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership&lt;/td&gt;
&lt;td&gt;specific personal mistake named&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-the-moment handling&lt;/td&gt;
&lt;td&gt;early escalation + parallel-run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System change&lt;/td&gt;
&lt;td&gt;3 permanent process changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proof it worked&lt;/td&gt;
&lt;td&gt;next migration landed on time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Plain ownership&lt;/strong&gt;&lt;/strong&gt; — naming a real six-week miss and &lt;em&gt;your&lt;/em&gt; specific decision (committing to a date before discovery) signals the accountability the question exists to test. Fake failures and blame-shifting both fail it instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Manager-grade handling&lt;/strong&gt;&lt;/strong&gt; — escalating at week 4 instead of week 11 and standing up a parallel-run shows you handled a bad situation with judgment: honest, early, and protective of the stakeholder rather than hopeful and silent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;The system change is the pivot&lt;/strong&gt;&lt;/strong&gt; — the answer's weight is on the three permanent changes (discovery spike before dates, mandatory data-quality phase, parallel-run default). Learning that changes the &lt;em&gt;system&lt;/em&gt; is what separates a manager from someone who just apologizes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Proof it generalized&lt;/strong&gt;&lt;/strong&gt; — "the next migration used all three and landed on time" closes the loop: the lesson wasn't a nice sentiment, it changed outcomes. That evidence is what makes the story land.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the honest path costs you the discomfort of describing a real failure to a stranger who's judging you. The payoff is credibility: an interviewer trusts a candidate who owns a genuine miss and shows growth far more than one with a suspiciously flawless record. Vulnerability, scoped well, is the strongest move in the behavioral loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems for the freshness, SLO, and metrics queries you'll defend&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;Streaming&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — streaming&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Streaming problems on the real-time pipelines behind incident leadership&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — EM interview recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The one-line frame.&lt;/strong&gt; The &lt;code&gt;data engineering manager interview&lt;/code&gt; grades judgment across four tracks — people, project/roadmap, technical/architecture judgment, and cross-functional — and every answer secretly asks "can your judgment scale past your own hands?" Describe impact as team outcomes, name the trade-off in every decision, and say "it depends, here's what I'd need to know" before committing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IC → EM shift.&lt;/strong&gt; Your output is the team's output; your feedback loop gets slower and noisier; you trade depth for breadth; your influence is indirect. Answer "how do you spend your week?" with numbers: build time drops from ~70% to under 20%, off the critical path, with hours flowing to people, planning, and cross-functional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Why management" answer.&lt;/strong&gt; Lead with an energy source (other people's growth), evidence you already do the job informally, and an explicit list of trade-offs you've accepted. Never lead with title, money, or control — the three fastest disqualifiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First 90 days.&lt;/strong&gt; Listen (30) → diagnose and co-write the operating model (60) → deliver one visible win and set the rhythm (90). For a peer-to-boss move, name the changed relationship out loud and give tenured engineers &lt;em&gt;more&lt;/em&gt; autonomy, not less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;People loop.&lt;/strong&gt; Hire → onboard → grow → evaluate → retain, with a mechanism for each: structured hiring with pre-committed ratings; a 30/60/90 ramp; a skill/will matrix (delegate / re-engage / coach / direct); continuous SBI feedback; and sponsorship for growth. "Open-door policy" is an IC answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1:1 doc.&lt;/strong&gt; Their agenda first, in a shared running doc, one specific piece of feedback every time, owned action items with checkboxes, and a living growth plan that names the two-to-three gaps and the opportunities you're creating to close them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Underperformer / performance conversation.&lt;/strong&gt; Diagnose skill vs will vs fit vs context, then run it clear &lt;em&gt;and&lt;/em&gt; kind: no-ambush open, SBI evidence, own your part, written time-boxed expectations with real support, and a genuine statement of belief. Be willing to lose even a top performer if the behaviour is toxic — culture is set by the worst behaviour you tolerate from your best people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prioritization.&lt;/strong&gt; Use RICE (Reach × Impact × Confidence ÷ Effort) or an impact/effort 2×2 to make ranking defensible; keep the confidence factor honest; treat the score as an input to judgment and say why when you override it. Every roadmap needs a visible, dated no-list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacity math.&lt;/strong&gt; Headcount ≠ capacity. From raw person-weeks subtract on-call, meetings, leave, new-hire ramp (a net negative short-term), and a ~20% KTLO tax, then reserve ~15–20% slack. Plan to that number. Sequence by dependency and staff the critical path — adding people off it speeds up nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lose-headcount answer.&lt;/strong&gt; Recompute capacity honestly (2 of 6 ≈ half the output), protect the SLA/on-call first, re-rank and cut from the bottom, give stakeholders an explicit either/or choice rather than a no, capture leavers' runbooks, and refuse heroics/overtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform vs product.&lt;/strong&gt; Platform buys leverage, product buys impact; decide per initiative on repetition (rule of three), compounding, and adoption certainty. Ship the specific thing 2–3× before extracting a platform — premature platforming is the most expensive data-manager mistake. Buy the undifferentiated, build only your edge; run a standing, protected ~20% tech-debt budget; and measure platform ROI in adoption, time saved, and reliability delta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pitch platform to a product VP.&lt;/strong&gt; Frame in their currency (throughput, revenue, risk, not architecture), quantify the compounding return, de-risk to a small phased bet with a measurable checkpoint and a kill switch, and name the trade-off honestly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-functional + behavioral.&lt;/strong&gt; Manage up with no-surprises visibility and problems-with-options; measure the team with reliability (uptime, freshness SLO, quality) first and adapted DORA second, never to stack-rank; lead incidents as a coordinating commander with cadence comms and a blameless postmortem; and bring a bank of 8–12 STAR stories with quantified Results. For the failure question, own it plainly and land on the concrete system change you made.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What does a data engineering manager actually do?
&lt;/h3&gt;

&lt;p&gt;A data engineering manager &lt;strong&gt;is&lt;/strong&gt; responsible for the &lt;em&gt;output and health of a data team&lt;/em&gt;, not for personally writing the most pipelines — the job is leverage, turning a group of engineers into more than the sum of their commits. Concretely the role spans four areas: people (hiring, growing, evaluating, and retaining engineers), planning (building and prioritizing a &lt;code&gt;data engineering roadmap&lt;/code&gt; against finite capacity), technical judgment (reviewing designs, making build-vs-buy and platform-vs-product calls without being the one who codes them), and cross-functional leadership (managing up to leadership, aligning with product and analytics, and representing the data org). Most managers keep a small slice of hands-on work to stay technically credible, but if their week still looks like an IC's, they haven't made the transition. The through-line is judgment: converting ambiguous business pressure into a sequence of good decisions other people execute.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is the engineering manager interview different from the IC interview?
&lt;/h3&gt;

&lt;p&gt;The IC loop tests whether you &lt;strong&gt;can&lt;/strong&gt; do the work — coding, SQL, system design under a whiteboard clock; the &lt;code&gt;engineering manager interview&lt;/code&gt; assumes you can and tests whether your &lt;em&gt;judgment scales past your own hands&lt;/em&gt;. Instead of "write this query," you get "your team missed a deadline — what did you do," "you lost two engineers mid-quarter — what do you cut," and "justify a platform investment to a skeptical VP." The panel is split into tracks — people, project/roadmap, technical judgment, and cross-functional — and grades behavioral evidence (STAR stories with quantified results) rather than code correctness. The single biggest adjustment for strong ICs is to stop describing impact as personal deliverables and start describing it as team outcomes and decisions. "It depends, here's what I'd need to know" is a strong opening in an EM loop and a weak one in a coding round.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do engineering managers still code?
&lt;/h3&gt;

&lt;p&gt;It depends on the level and the team, but the honest answer for most first-line data engineering managers &lt;strong&gt;is&lt;/strong&gt; "a little, deliberately, and off the critical path." Build time typically drops from roughly 70% as an IC to under 20% as a manager, and that remaining slice goes to design reviews, prototypes, glue code, and emergency backfill — never to owning a critical deliverable, because a manager on the critical path becomes a single point of failure who can't actually manage during a crisis. Some staff/principal-adjacent or very small-team managers code more; some senior managers and directors code essentially zero. In the interview, claim neither extreme: "I code enough to review architecture credibly and help in a pinch, but I plan as if my coding capacity is zero" is the credible, senior position. The goal of &lt;code&gt;EM interview prep&lt;/code&gt; here is to sound like someone who has let go of being the top IC without going stale.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you answer people-conflict and underperformer questions?
&lt;/h3&gt;

&lt;p&gt;Lead with a &lt;strong&gt;process&lt;/strong&gt;, not a vibe, and ground it in a real story. For an underperformer: diagnose whether it's skill, will, fit, or context (each needs a different response), then have a conversation that is clear &lt;em&gt;and&lt;/em&gt; kind — no-ambush opening, specific situation-behaviour-impact evidence, owning your part, written time-boxed expectations with genuine support, and a real statement of belief. For conflict between engineers: get past positions to the underlying interests, mediate directly, and set boundaries so a feud doesn't tax the whole team. The senior signal across both is a willingness to act decisively when things don't improve — including managing out a technically strong but toxic engineer, because &lt;em&gt;people management&lt;/em&gt; means protecting the team's health over any single person's output. Avoidance stories and surprise-firing stories both fail; a story with a clear process and an honest outcome (turned around, or parted ways with dignity) lands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platform vs product — how do you decide?
&lt;/h3&gt;

&lt;p&gt;There &lt;strong&gt;is&lt;/strong&gt; no "always platform" or "always product" answer; the senior move is a per-initiative judgment. Invest in &lt;code&gt;platform&lt;/code&gt; when the same pain repeats across three or more teams, the solution compounds over time, and adoption is genuinely likely (teams are asking) — that's leverage worth a quarter. Ship &lt;code&gt;product&lt;/code&gt; when impact is needed now, when it's a one-off with no real reuse, or when you haven't yet shipped the specific thing enough times to know the right abstraction — building the general framework prematurely produces the wrong one, the single most expensive data-manager mistake. Underneath sits build-vs-buy discipline (buy the undifferentiated heavy-lifting, build only your competitive edge, score on total cost of ownership) and a standing tech-debt budget. And when you pitch platform to a product-focused leader, translate it into their currency — throughput, reliability, risk — and de-risk it to a small phased bet with a measurable checkpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you prepare for the EM interview loop?
&lt;/h3&gt;

&lt;p&gt;Treat &lt;code&gt;EM interview prep&lt;/code&gt; as building evidence, not memorizing answers. First, build a bank of 8–12 STAR stories mapped to the themes every panel probes — conflict, failure, influence without authority, a hard people call, ambiguity, a delivery win, and a hard prioritization call — each with a quantified Result and a lesson. Second, prepare your frameworks so you can walk them live: the skill/will matrix, RICE plus capacity math, the platform-vs-product decision matrix, and a build-vs-buy scorecard. Third, know which track each interviewer is grading and lead with a story tuned to that signal instead of giving the same generic answer five times. Fourth, keep your technical judgment sharp — the architecture round tests whether you've gone stale, so practice reasoning about &lt;code&gt;team leadership&lt;/code&gt;-adjacent design and trade-off problems rather than raw coding speed. Finally, rehearse the failure story until you can own a real miss plainly and land it on a concrete system change — that one question reveals more than almost any other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt; for the architecture-review, build-vs-buy, and platform-vs-product judgment the technical and skip-level rounds probe.&lt;/li&gt;
&lt;li&gt;Keep the fundamentals sharp on the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; so your technical credibility holds up — the freshness/SLO and data-quality queries a manager should still be able to reason about.&lt;/li&gt;
&lt;li&gt;Rehearse the prioritization instinct on the &lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;optimization practice library →&lt;/a&gt; for the trade-off and constraint problems that mirror capacity-limited roadmap planning.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to keep the whole technical foundation current while you build the people, roadmap, and platform-vs-product muscles the interview grades.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lead a data team with judgment you can defend&lt;/h3&gt;

&lt;p&gt;Frameworks explain the theory. PipeCode drills build the judgment — when to invest in platform vs product, how to reason about a design under constraints, and how to keep the technical credibility that a manager who's gone stale quietly loses. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — trade-off-first practice tuned for the decisions data engineering managers actually make.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice design problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice SQL problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Staff &amp; Principal Data Engineer Interviews: Scope, Impact, Cross-Team Architecture Loops</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Sat, 01 Aug 2026 13:21:57 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/staff-principal-data-engineer-interviews-scope-impact-cross-team-architecture-loops-fei</link>
      <guid>https://dev.to/gowthampotureddi/staff-principal-data-engineer-interviews-scope-impact-cross-team-architecture-loops-fei</guid>
      <description>&lt;p&gt;The &lt;strong&gt;&lt;code&gt;staff data engineer interview&lt;/code&gt;&lt;/strong&gt; is the first interview in a data engineer's career where writing correct code is table stakes and no longer the thing being graded — the interviewer already assumes you can model a warehouse, tune a query, and ship a pipeline, so the entire loop is engineered to measure something harder to fake: the &lt;strong&gt;scope and impact&lt;/strong&gt; of the problems you choose, and whether you can move a decision across teams you do not manage. Where the senior loop asks "walk me through how you'd build this," the staff loop asks "three teams disagree on the schema and the deadline is in six weeks — what do you do?" The questions look similar on the surface, but they are testing the opposite muscle. The senior answer optimizes a system; the staff answer optimizes an organization's ability to build many systems.&lt;/p&gt;

&lt;p&gt;That shift catches strong senior engineers off guard, because the very habits that earned the promotion to senior — diving deep, owning the hardest module, out-coding the room — are the habits that cap you at senior if you carry them into a staff loop unchanged. This guide is the walkthrough for the transition: it decodes the leveling rubric that separates senior from staff from &lt;strong&gt;principal data engineer&lt;/strong&gt;, it shows how interviewers probe for &lt;strong&gt;cross-team architecture&lt;/strong&gt; leverage and &lt;strong&gt;technical leadership&lt;/strong&gt; rather than raw throughput, and it gives you the artifacts — the scope narrative, the RFC/ADR skeleton, the design-review script, the promo packet — that turn vague "impact" into a defensible, quantified story. Each section pairs a teaching block with a worked interview answer: a template, a step-by-step trace, an output table, then a concept-by-concept breakdown of why the answer lands.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fepq383kcmu6fcoyupoy1.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fepq383kcmu6fcoyupoy1.jpeg" alt="PipeCode blog header for the staff data engineer interview — bold white headline 'Staff &amp;amp; Principal' over a hero composition of a rising altitude ladder (project, platform, org) beside a cross-team architecture-loop glyph and a purple 'scope &amp;amp; impact' seal, on a dark gradient." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; on the design and coding rounds that still gate the staff loop, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt;, keep the fundamentals sharp on the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt;, and rehearse the platform muscle on the &lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;streaming practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why the staff/principal bar is scope and impact, not more code&lt;/li&gt;
&lt;li&gt;Demonstrating scope: from project to platform to org&lt;/li&gt;
&lt;li&gt;Cross-team architecture loops and the RFC/design-review muscle&lt;/li&gt;
&lt;li&gt;System design at staff/principal altitude: trade-offs and failure narratives&lt;/li&gt;
&lt;li&gt;Influence without authority, promo packets, and the staff interview loop&lt;/li&gt;
&lt;li&gt;Cheat sheet — staff and principal interview recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why the staff/principal bar is scope and impact, not more code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The leveling line is a change of unit — from "owns a system" to "owns a strategy"
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;the promotion from senior to staff is not "more of the same, done better" — it is a change in the unit of work you are measured in, from a system you own end-to-end to a problem space you make the whole org better at, and the &lt;code&gt;staff data engineer interview&lt;/code&gt; is built to detect whether you already operate in the larger unit or merely aspire to&lt;/strong&gt;. A senior engineer is graded on the systems they ship; a staff engineer is graded on the leverage they create so that other people ship better systems; a &lt;strong&gt;principal data engineer&lt;/strong&gt; is graded on the technical strategy that decides which systems the org should build at all. Every question in a staff loop is a probe for which of those three units you naturally reach for when handed an ambiguous prompt.&lt;/p&gt;

&lt;p&gt;This is why "I rewrote the ingestion service and cut latency 40%" — a strong senior answer — is a &lt;em&gt;weak&lt;/em&gt; staff answer if that is where the story ends. The staff version is "I noticed six teams were each building their own ingestion service, so I drove an RFC for a shared platform, got three staff engineers to disagree-and-commit, and now those six teams ship ingestion in a day instead of a quarter." Same person, same technical depth; the difference is the sphere of influence the story operates in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sphere-of-influence ladder — what each level's blast radius looks like.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Senior (self + immediate team).&lt;/strong&gt; Owns a system or a large component end-to-end. Makes the right call under ambiguity &lt;em&gt;within&lt;/em&gt; the team's scope. Mentors juniors. The blast radius of a great senior engineer is their team's roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staff (multiple teams / a platform).&lt;/strong&gt; Owns a problem &lt;em&gt;across&lt;/em&gt; team boundaries. Identifies leverage — the shared platform, the standard, the migration — that makes many teams faster. Drives alignment without managing anyone. The blast radius is an org's ability to execute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Principal (org / company).&lt;/strong&gt; Owns technical &lt;em&gt;strategy&lt;/em&gt; — which bets the org makes, which architectures it standardizes on for the next three years, which it sunsets. Influences the roadmap of roadmaps. The blast radius is the company's technical direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The key discontinuity.&lt;/strong&gt; Between senior and staff, the &lt;em&gt;authority&lt;/em&gt; does not grow — a staff engineer still manages no one — but the &lt;em&gt;scope&lt;/em&gt; does. That gap is the entire skill: producing org-level outcomes with team-level authority. Interviewers hunt for evidence you have closed it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers actually probe in a staff loop.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope selection.&lt;/strong&gt; Given a vague prompt, do you scope &lt;em&gt;up&lt;/em&gt; (find the leveraged version of the problem) or scope &lt;em&gt;down&lt;/em&gt; (retreat to the comfortable component)? Staff engineers instinctively ask "who else has this problem?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguity tolerance.&lt;/strong&gt; Senior questions have a right answer; staff questions have trade-offs and no clean answer. The signal is whether you can make a defensible decision &lt;em&gt;and name what you're trading away&lt;/em&gt; rather than freeze waiting for more requirements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-team leverage.&lt;/strong&gt; Do your stories move other teams, or just your own deliverables? The word "I" versus "we drove" versus "the org adopted" is diagnostic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judgment and taste.&lt;/strong&gt; Do you know when &lt;em&gt;not&lt;/em&gt; to build, when to buy, when to standardize versus let a thousand flowers bloom? Principal-level judgment is knowing which fights are worth having.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The four-signal grading model interviewers score you against.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signal 1 — Scope.&lt;/strong&gt; Was the problem org-sized or task-sized? Did you expand a narrow ask into the real leveraged problem?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal 2 — Impact.&lt;/strong&gt; Was the outcome a multiplier (others got faster) or additive (you personally shipped)? Is it quantified?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal 3 — Leadership.&lt;/strong&gt; Did you drive alignment, mentor, write the doc the org rallied around — or did you just execute a plan someone handed you?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal 4 — Judgment.&lt;/strong&gt; Under trade-offs, did you make a sound call and own its downside — or did you either freeze or hand-wave?&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the Senior → Staff → Principal leveling rubric
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful artifact for a staff interview is a memorized leveling rubric that maps each behavioral dimension across the three levels. Every staff loop implicitly grades you against a rubric like this; having it in your head lets you &lt;em&gt;aim your stories at the level you're interviewing for&lt;/em&gt; instead of telling a great senior story in a staff loop. Walk through building the rubric across the dimensions interviewers actually score.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dimension: scope of ownership.&lt;/strong&gt; From "a component" (senior) to "a platform / cross-team problem" (staff) to "a domain strategy" (principal).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dimension: ambiguity.&lt;/strong&gt; From "solves well-defined problems" to "defines the problem" to "decides which problems the org should solve."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dimension: influence.&lt;/strong&gt; From "influences the team" to "influences multiple teams without authority" to "influences the org / other staff+ engineers."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dimension: what they're a force multiplier for.&lt;/strong&gt; From "junior engineers" to "a whole team / adjacent teams" to "the engineering org's technical direction."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the four-dimension leveling rubric for senior, staff, and principal data engineers, and state the one behavior that most distinguishes each boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Senior&lt;/th&gt;
&lt;th&gt;Staff&lt;/th&gt;
&lt;th&gt;Principal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;one system / component&lt;/td&gt;
&lt;td&gt;a platform / cross-team problem&lt;/td&gt;
&lt;td&gt;a domain / technical strategy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ambiguity&lt;/td&gt;
&lt;td&gt;solves defined problems&lt;/td&gt;
&lt;td&gt;defines the problem&lt;/td&gt;
&lt;td&gt;picks which problems matter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Influence&lt;/td&gt;
&lt;td&gt;the team&lt;/td&gt;
&lt;td&gt;multiple teams, no authority&lt;/td&gt;
&lt;td&gt;the org and other staff+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Force multiplier for&lt;/td&gt;
&lt;td&gt;juniors&lt;/td&gt;
&lt;td&gt;teams&lt;/td&gt;
&lt;td&gt;technical direction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LEVELING RUBRIC — data engineering IC track
===========================================

SENIOR
  Unit of work ...... a system
  Signature move .... "I owned the hardest part and shipped it right."
  Grows ............. depth
  Boundary up ....... starts asking "who else has this problem?"

STAFF
  Unit of work ...... a problem space across teams
  Signature move .... "I drove an RFC 3 teams aligned on; they all got faster."
  Grows ............. leverage (authority stays flat, scope grows)
  Boundary up ....... starts shaping *which* problems the org invests in

PRINCIPAL
  Unit of work ...... a multi-year technical strategy
  Signature move .... "I set the architecture direction the org standardized on."
  Grows ............. judgment / taste at company scale
  Distinguishing .... says 'no' to good ideas to protect the few great ones

THE ONE-LINE TEST PER BOUNDARY
  Senior -&amp;gt; Staff:     do your outcomes require other teams to change?
  Staff  -&amp;gt; Principal: do you decide the roadmap of roadmaps?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the rubric &lt;em&gt;down&lt;/em&gt; a column first to internalize one coherent level, then read &lt;em&gt;across&lt;/em&gt; a row to feel the boundary. The scope row is the spine: everything else (ambiguity, influence, force multiplication) is downstream of how big a unit you operate in.&lt;/li&gt;
&lt;li&gt;The senior→staff boundary is the "who else has this problem?" reflex. A senior fixes their pipeline; a staff engineer notices five teams have the same broken pipeline and builds the shared fix. Same trigger, wider aperture.&lt;/li&gt;
&lt;li&gt;The staff→principal boundary is problem &lt;em&gt;selection at org scale&lt;/em&gt; — a principal decides the org should stop investing in a dying architecture and standardize on another, and says "no" to locally-good ideas that don't fit the strategy. Taste, not throughput.&lt;/li&gt;
&lt;li&gt;Crucially, authority does not appear in the rubric as something that grows — a staff engineer still manages no one. The scope grows while the org-chart authority stays flat, which is exactly why influence-without-authority (section 5) is the load-bearing skill.&lt;/li&gt;
&lt;li&gt;Aim your interview stories at the level you're targeting. Telling a beautifully-executed &lt;em&gt;senior&lt;/em&gt; story (deep, solo, one-team) in a staff loop reads as "not yet ready" — not because the story is bad, but because it's evidence for the wrong level.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Boundary&lt;/th&gt;
&lt;th&gt;The distinguishing behavior&lt;/th&gt;
&lt;th&gt;Interview tell&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Senior → Staff&lt;/td&gt;
&lt;td&gt;outcomes require &lt;em&gt;other teams&lt;/em&gt; to change&lt;/td&gt;
&lt;td&gt;stories say "we drove alignment," not "I built"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staff → Principal&lt;/td&gt;
&lt;td&gt;you decide &lt;em&gt;which&lt;/em&gt; problems the org invests in&lt;/td&gt;
&lt;td&gt;stories include a deliberate "we chose not to"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any level&lt;/td&gt;
&lt;td&gt;quantified, owned trade-offs&lt;/td&gt;
&lt;td&gt;names the downside of the chosen path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Before any staff loop, write your target level's rubric column on one line and test each of your stories against it. If a story doesn't touch a team other than your own, it's a senior story — keep it in reserve for the deep-dive round, and lead the behavioral rounds with the cross-team ones.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — decoding a job ladder / leveling.fyi-style rung
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Job ladders and public leveling references (the leveling.fyi-style rungs, internal engineering ladders) are written in deliberately abstract language — "demonstrates broad technical influence," "operates with significant ambiguity" — and candidates who can &lt;em&gt;translate that abstraction into concrete evidence they'll present&lt;/em&gt; interview far better than those who read it as boilerplate. Walk through decoding a representative staff rung and mapping each phrase to a story you'll tell.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ladder phrase: "significant scope / ambiguity."&lt;/strong&gt; Translate to: I was handed a goal, not a spec, and I defined the problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ladder phrase: "influence beyond your team."&lt;/strong&gt; Translate to: another team changed what they built because of my work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ladder phrase: "technical leadership."&lt;/strong&gt; Translate to: I wrote the doc / drove the decision the group rallied around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ladder phrase: "force multiplier."&lt;/strong&gt; Translate to: my output made N other engineers measurably faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Decode a staff data engineer ladder rung into a prep checklist that maps each abstract requirement to a concrete, quantified story you can defend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ladder phrase (abstract)&lt;/th&gt;
&lt;th&gt;What it really asks&lt;/th&gt;
&lt;th&gt;Evidence to prepare&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;operates with significant ambiguity&lt;/td&gt;
&lt;td&gt;you define problems, not just solve them&lt;/td&gt;
&lt;td&gt;a "goal, no spec" story&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;influence beyond the team&lt;/td&gt;
&lt;td&gt;others changed course because of you&lt;/td&gt;
&lt;td&gt;a cross-team adoption story&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;technical leadership&lt;/td&gt;
&lt;td&gt;you drove the decision&lt;/td&gt;
&lt;td&gt;an RFC / design you authored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;force multiplier&lt;/td&gt;
&lt;td&gt;you made others faster&lt;/td&gt;
&lt;td&gt;a platform / tooling adoption metric&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LADDER-DECODE CHECKLIST — staff data engineer
=============================================

[ ] AMBIGUITY  -&amp;gt;  Story where the ask was "improve data reliability"
                   (a goal) not "add retries to job X" (a task).
                   Prove: I chose the metric, scoped the work, defended it.

[ ] INFLUENCE  -&amp;gt;  Story where Team B adopted my standard / changed
                   their design. Prove with THEIR outcome, not mine.
                   "After the contract, their on-call pages dropped 60%."

[ ] LEADERSHIP -&amp;gt;  A document I authored (RFC/ADR/strategy) that a
                   group of 3+ engineers aligned on. Bring the doc.

[ ] MULTIPLIER -&amp;gt;  A platform/library/pattern N teams reuse.
                   Metric: "12 pipelines migrated; onboarding 2wk -&amp;gt; 1day."

[ ] JUDGMENT   -&amp;gt;  A time I chose NOT to build / chose to deprecate.
                   Prove taste: what I traded away, and why it was right.

RED FLAG SELF-CHECK
  If every checklist item is filled by the SAME project, your scope is
  narrow. Staff evidence should span 2-3 distinct efforts.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The decode step is deliberately literal: for each abstract ladder phrase, write the one-sentence &lt;em&gt;behavior&lt;/em&gt; it is really asking for. Interviewers grade behaviors, not adjectives, so you must arrive with behaviors pre-mapped.&lt;/li&gt;
&lt;li&gt;The ambiguity item forces a "goal, not spec" story. If your best story starts with a well-defined ticket, it's evidence for senior. Reach for the time a director said "our data is unreliable, fix it" and you turned that into a scoped program.&lt;/li&gt;
&lt;li&gt;The influence item must be proven with &lt;em&gt;the other team's outcome&lt;/em&gt;, not yours. "I built a data contract framework" is your output; "their on-call pages dropped 60% after adopting it" is your influence. Interviewers discount the former and reward the latter.&lt;/li&gt;
&lt;li&gt;The multiplier item needs a number that divides work across people: "onboarding a new pipeline went from two weeks to one day, across twelve teams." That is leverage made legible.&lt;/li&gt;
&lt;li&gt;The red-flag self-check catches the most common failure: filling every rung from one heroic project. Real staff scope shows up as a &lt;em&gt;portfolio&lt;/em&gt; of cross-team wins, because a person operating at org scope naturally accumulates several.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checklist item&lt;/th&gt;
&lt;th&gt;Weak evidence (senior)&lt;/th&gt;
&lt;th&gt;Strong evidence (staff)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ambiguity&lt;/td&gt;
&lt;td&gt;"I implemented the spec well"&lt;/td&gt;
&lt;td&gt;"I defined the metric and the scope"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Influence&lt;/td&gt;
&lt;td&gt;"I built a framework"&lt;/td&gt;
&lt;td&gt;"Team B's pages fell 60% after adopting it"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leadership&lt;/td&gt;
&lt;td&gt;"I gave feedback in review"&lt;/td&gt;
&lt;td&gt;"I authored the RFC the org aligned on"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiplier&lt;/td&gt;
&lt;td&gt;"my pipeline is fast"&lt;/td&gt;
&lt;td&gt;"12 teams onboard in 1 day, was 2 weeks"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never present a ladder requirement back in the ladder's own words. Translate every abstract phrase into a concrete behavior with a number attached, and make sure your evidence spans multiple efforts — a single project that ticks every box signals a project win, not a level.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the STAR-for-scope impact story template
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The standard STAR template (Situation, Task, Action, Result) was designed for senior behavioral answers and quietly under-serves staff candidates, because it puts all the weight on &lt;em&gt;Action&lt;/em&gt; (what you personally did) and treats &lt;em&gt;Result&lt;/em&gt; as a single line. Staff stories need the weight on &lt;em&gt;Scope&lt;/em&gt; (how big and ambiguous the problem was) and &lt;em&gt;Leverage&lt;/em&gt; (how the result multiplied across the org). The STAR-for-scope variant re-balances the template for the staff loop. Walk through it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope (replaces a thin Situation).&lt;/strong&gt; How ambiguous and how large was the problem? Who else was affected? Why did it need &lt;em&gt;you&lt;/em&gt; rather than a well-scoped ticket?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tension.&lt;/strong&gt; What made it hard — competing teams, no authority, unclear ownership, a real trade-off?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action, framed as leadership.&lt;/strong&gt; Emphasize the &lt;em&gt;driving&lt;/em&gt; (RFC, alignment, mentoring) over the &lt;em&gt;doing&lt;/em&gt; (the code you wrote).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result, framed as a multiplier.&lt;/strong&gt; Quantify the outcome as leverage: N teams, X% faster, $Y saved, an on-call burden removed — the other party's metric, not yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reflection.&lt;/strong&gt; What you'd do differently and what you learned about operating at scope — the signal of a self-aware operator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Convert a raw senior-flavored accomplishment ("I built a data quality framework") into a STAR-for-scope answer aimed at a staff loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;STAR-for-scope slot&lt;/th&gt;
&lt;th&gt;Prompt to yourself&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;how ambiguous / how many teams?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tension&lt;/td&gt;
&lt;td&gt;why was it hard without authority?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action&lt;/td&gt;
&lt;td&gt;what did I &lt;em&gt;drive&lt;/em&gt;, not just do?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;what multiplier, in the other team's numbers?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reflection&lt;/td&gt;
&lt;td&gt;what did I learn about scope?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STAR-FOR-SCOPE — worked answer
==============================

SCOPE
  "Data quality was everyone's complaint and no one's job. Seven teams
   shipped dashboards off pipelines with no shared reliability bar.
   Leadership's ask was a goal, not a spec: 'stakeholders don't trust
   the numbers.' I scoped it into an org-wide data-contract program."

TENSION
  "I owned none of those seven pipelines and managed none of the teams.
   Two teams thought it was overhead. I had to win it on the merits."

ACTION (as leadership)
  "I wrote an RFC proposing schema + freshness contracts enforced in CI.
   I ran three design reviews, took a dissent from the streaming team
   that improved the freshness spec, and got disagree-and-commit from
   the last holdout. I built the reference implementation, then mentored
   one engineer per team to own their team's contracts."

RESULT (as multiplier)
  "12 pipelines under contract in a quarter. Data incidents down 55%.
   Stakeholder-reported 'bad number' tickets down 70%. New pipeline
   onboarding to the contract: 2 weeks -&amp;gt; 1 day. I wrote none of the
   12 teams' pipelines; they own them now."

REFLECTION
  "I over-invested in the framework before socializing it; next time I'd
   ship the RFC and one design review BEFORE writing a line of code."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Scope slot does the work that STAR's "Situation" usually wastes: it establishes that the problem was &lt;em&gt;org-sized and ambiguous&lt;/em&gt; ("everyone's complaint and no one's job," a goal not a spec). This one paragraph is what makes the whole story a staff story.&lt;/li&gt;
&lt;li&gt;The Tension slot names the absence of authority explicitly — "owned none, managed none." Staff interviewers are listening for whether you can produce outcomes &lt;em&gt;without&lt;/em&gt; positional power; stating the constraint up front frames everything that follows as influence.&lt;/li&gt;
&lt;li&gt;The Action slot deliberately foregrounds &lt;em&gt;driving&lt;/em&gt; verbs (wrote the RFC, ran reviews, took a dissent, got disagree-and-commit, mentored) and backgrounds the coding ("I built the reference implementation" is one clause). This ratio is the tell of a staff operator.&lt;/li&gt;
&lt;li&gt;The Result slot is quantified in &lt;em&gt;other teams'&lt;/em&gt; metrics — incidents down 55%, their onboarding from two weeks to one day — and explicitly disclaims personal execution ("I wrote none of the 12 teams' pipelines"). That is leverage stated as a multiplier, not additive work.&lt;/li&gt;
&lt;li&gt;The Reflection slot signals judgment and growth: admitting you over-built before socializing shows you understand that at staff scope, alignment precedes code. Interviewers read this as a self-correcting operator who will keep leveling.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Slot&lt;/th&gt;
&lt;th&gt;Senior instinct&lt;/th&gt;
&lt;th&gt;Staff-calibrated version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Situation/Scope&lt;/td&gt;
&lt;td&gt;"my team's dashboards were flaky"&lt;/td&gt;
&lt;td&gt;"7 teams, no shared bar, a goal not a spec"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action&lt;/td&gt;
&lt;td&gt;"I coded a QA framework"&lt;/td&gt;
&lt;td&gt;"I drove the RFC + reviews + mentoring"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;"our data got more reliable"&lt;/td&gt;
&lt;td&gt;"12 teams, incidents -55%, onboarding -90%"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reflection&lt;/td&gt;
&lt;td&gt;(often omitted)&lt;/td&gt;
&lt;td&gt;"alignment should precede code"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Weight a staff behavioral answer 40% Scope+Tension, 30% Action-as-leadership, 30% Result-as-multiplier — the inverse of the senior weighting, which is mostly Action. If you find yourself spending the answer describing code you wrote, you are telling a senior story in a staff room.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on scope and leveling
&lt;/h3&gt;

&lt;p&gt;A staff interviewer often opens the behavioral round with: "Tell me about the most impactful thing you've done. Then tell me why it was staff-level and not senior-level work." The trap is that most candidates answer the first half brilliantly — a deep, well-executed technical project — and then cannot articulate the second half, because the project &lt;em&gt;was&lt;/em&gt; senior-level: solo, single-team, additive. The question is really asking you to demonstrate that you understand the leveling line well enough to locate your own work on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using the scope-first reframing of an impact story
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ANSWER SCAFFOLD — "most impactful, and why it's staff"
======================================================

1. LEAD WITH SCOPE, NOT THE ARTIFACT
   Don't open with "I built X." Open with the org-sized problem:
   "Six teams were each rebuilding CDC ingestion; collectively ~4
    engineer-years/yr of duplicated effort and 6 inconsistent
    delete-handling stories feeding the same warehouse."

2. NAME WHY IT NEEDED A STAFF, NOT A TICKET
   "No single team owned 'ingestion' as a platform. It required
    someone to work ACROSS the teams with no authority over any."

3. DRIVE, THEN BUILD
   "I authored the platform RFC, aligned the 6 teams in 3 reviews,
    absorbed the security team's dissent on secrets handling, and
    got disagree-and-commit from the team that preferred its own
    tool. THEN I built the shared connector framework."

4. MULTIPLIER RESULT (other teams' numbers)
   "6 bespoke pipelines -&amp;gt; 1 platform, 18 connectors. New source
    onboarding: 3 weeks -&amp;gt; 2 days. ~3.5 engineer-years/yr reclaimed.
    Delete-handling now consistent across all sources."

5. EXPLICIT LEVELING CLAIM
   "It's staff-level because the unit of impact was the org's
    ingestion capability, not a system I own. The authority was
    influence; the outcome was other teams shipping faster."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Senior framing&lt;/th&gt;
&lt;th&gt;Staff framing (this answer)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opening&lt;/td&gt;
&lt;td&gt;"I built a connector framework"&lt;/td&gt;
&lt;td&gt;"6 teams duplicated ~4 eng-years/yr"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Problem ownership&lt;/td&gt;
&lt;td&gt;"my team needed better ingestion"&lt;/td&gt;
&lt;td&gt;"no one owned ingestion as a platform"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action emphasis&lt;/td&gt;
&lt;td&gt;the code / architecture&lt;/td&gt;
&lt;td&gt;the RFC, alignment, dissent, mentoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result unit&lt;/td&gt;
&lt;td&gt;"our pipeline got faster"&lt;/td&gt;
&lt;td&gt;"onboarding 3wk→2day across 6 teams"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leveling claim&lt;/td&gt;
&lt;td&gt;(absent)&lt;/td&gt;
&lt;td&gt;"the unit of impact was the org's capability"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Walking the scaffold, the candidate first &lt;em&gt;resizes the problem&lt;/em&gt; from a system to an org capability, then makes the absence of authority the central difficulty, spends the bulk of the answer on driving rather than doing, quantifies in the reclaimed engineer-years and the other teams' onboarding time, and finally closes by explicitly locating the work above the leveling line. The interviewer gets both halves of the question answered, and — critically — sees that the candidate can &lt;em&gt;reason about levels&lt;/em&gt;, which is itself staff-level metacognition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;This answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;one system&lt;/td&gt;
&lt;td&gt;the org's ingestion capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impact&lt;/td&gt;
&lt;td&gt;additive (I shipped)&lt;/td&gt;
&lt;td&gt;multiplier (6 teams faster)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leadership&lt;/td&gt;
&lt;td&gt;executed a plan&lt;/td&gt;
&lt;td&gt;drove RFC + alignment + dissent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judgment / metacognition&lt;/td&gt;
&lt;td&gt;can't say why it's staff&lt;/td&gt;
&lt;td&gt;names the leveling line explicitly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantification&lt;/td&gt;
&lt;td&gt;"faster"&lt;/td&gt;
&lt;td&gt;3wk→2day, ~3.5 eng-years/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Scope-first framing&lt;/strong&gt;&lt;/strong&gt; — leading with the org-sized problem instead of the artifact immediately signals the correct unit of work. The interviewer's first-impression grade is set by whether sentence one is about a system or about an org capability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Authority-gap naming&lt;/strong&gt;&lt;/strong&gt; — explicitly stating "no authority over any of the six teams" reframes the entire story as influence-without-authority, which is the defining staff skill. It preempts the interviewer's follow-up ("how did you get them to agree?").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Drive-then-build ratio&lt;/strong&gt;&lt;/strong&gt; — spending four-fifths of the Action on driving (RFC, alignment, dissent, mentoring) and one-fifth on building matches how staff work actually decomposes, and inoculates against the "great senior, not yet staff" verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Multiplier metrics&lt;/strong&gt;&lt;/strong&gt; — quantifying in reclaimed engineer-years and other teams' onboarding time expresses impact as leverage. Additive metrics ("my code is 40% faster") measure a system; multiplier metrics measure an org.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the preparation cost is real: you must have actually done cross-team work to tell this story, and you must have captured the numbers &lt;em&gt;at the time&lt;/em&gt;. The recurring habit that makes this cheap is keeping a running "brag doc" of quantified, cross-team outcomes — O(minutes/week) that compounds into a promo packet and an interview arsenal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;System design problems for the staff loop&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;
&lt;strong&gt;SQL depth problems that still gate staff candidates&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Demonstrating scope: from project to platform to org
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Scope is measured in blast radius, not lines of code — quantify who else got faster
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1sgc5n5r2b7hfdawmft.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1sgc5n5r2b7hfdawmft.jpeg" alt="Iconographic scope-ladder diagram — three widening tiers labelled project, platform, org rising left to right, each annotated with a scope multiplier chip, plus a warning ribbon distinguishing additive from multiplier impact." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;scope and impact&lt;/code&gt; at staff level is measured in blast radius — the number of teams, systems, and future decisions your work touches — and the single most important reframe is from &lt;em&gt;additive&lt;/em&gt; impact (work you personally completed) to &lt;em&gt;multiplier&lt;/em&gt; impact (leverage that makes other people's work faster), because interviewers grade the multiplier and discount the additive&lt;/strong&gt;. Two engineers can spend the same quarter and produce wildly different scope: one ships a beautiful pipeline used by one team; the other ships a slightly-less-beautiful platform twelve teams build on. The second is the staff story, and the difference is entirely blast radius.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The project → platform → org ladder — what each rung looks like in a data org.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project (x1 leverage).&lt;/strong&gt; You build a system your team uses. A revenue-attribution pipeline, a dbt model layer, a specific dashboard's backend. Real value, single-team blast radius. This is excellent &lt;em&gt;senior&lt;/em&gt; work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform (x10 leverage).&lt;/strong&gt; You build something &lt;em&gt;many teams reuse&lt;/em&gt; — a shared ingestion framework, a data-contract library, a self-serve backfill tool, a metrics layer. Your one quarter of work saves each of ten teams a quarter. The multiplier is the point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Org (x100 leverage).&lt;/strong&gt; You change how the &lt;em&gt;whole org&lt;/em&gt; works — a standard everyone adopts, a migration off a dying architecture, a strategy that redirects the data roadmap for years. The blast radius is the org's technical direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ladder is about aperture, not effort.&lt;/strong&gt; Moving up a rung is rarely "work harder." It's "notice the version of this problem that N teams share, and solve &lt;em&gt;that&lt;/em&gt; one." Staff engineers habitually scope up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quantifying scope — turning "impact" into defensible numbers.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Count the teams.&lt;/strong&gt; "Adopted by 12 teams" is scope made legible. Always know the N.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count the reclaimed time.&lt;/strong&gt; "Onboarding a new source: 3 weeks → 2 days" across N teams is engineer-years reclaimed. Time is the universal currency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count the incidents removed.&lt;/strong&gt; "On-call pages down 60%," "data incidents down 55%" — reliability leverage that other teams feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count the dollars where honest.&lt;/strong&gt; Compute cost cut, revenue enabled, headcount avoided. Use real numbers; a fabricated dollar figure is a fast way to fail a staff loop when the interviewer probes the derivation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Multiplier vs additive — the distinction interviewers grade.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Additive impact.&lt;/strong&gt; "I wrote 40 dbt models." "I cut our query time 40%." Value, but it's &lt;em&gt;your&lt;/em&gt; output; it stops when you stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiplier impact.&lt;/strong&gt; "I built the model-testing template 12 teams now use." "I made backfills self-serve so no team files a ticket to me anymore." Value that &lt;em&gt;compounds through other people&lt;/em&gt; and persists without you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tell in a story.&lt;/strong&gt; Additive stories have "I" as the subject of every verb. Multiplier stories have &lt;em&gt;other teams&lt;/em&gt; as the subject of the result verb: "they onboard in a day," "their pages dropped."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trap.&lt;/strong&gt; Heroic additive work can even be &lt;em&gt;negative&lt;/em&gt; multiplier — if you're the only one who understands the critical pipeline, you've created a bus-factor-one liability, which is an anti-signal at staff level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Writing the scope narrative — the doc that makes your impact reviewable.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One page per major effort.&lt;/strong&gt; Problem (org-sized), your role (drove vs did), the multiplier outcome (quantified), the artifacts (RFC, adoption metrics).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead with the number.&lt;/strong&gt; "Reclaimed ~3.5 engineer-years/yr" before the story of how. Reviewers and interviewers skim; the number is the hook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name the counterfactual.&lt;/strong&gt; "Without the platform, each of 6 teams would have kept rebuilding CDC." Counterfactuals make leverage concrete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it running.&lt;/strong&gt; The scope narrative is the raw material for both the promo packet (section 5) and the interview answer; maintain it continuously, not the night before.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the scope-sizing rubric
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; When an interviewer asks about impact, you need to &lt;em&gt;size&lt;/em&gt; a piece of work quickly and honestly on the spot. The scope-sizing rubric is a scoring instrument that converts a vague accomplishment into a defensible level by scoring it on blast radius, leverage type, durability, and difficulty. Walk through the rubric and score two efforts with it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Axis: blast radius.&lt;/strong&gt; 1 = self, 2 = team, 3 = several teams, 4 = org, 5 = company/industry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Axis: leverage type.&lt;/strong&gt; additive (you did it) vs multiplier (others got faster) — a hard gate, not a slider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Axis: durability.&lt;/strong&gt; does the value persist without you (owned by others) or evaporate when you leave?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Axis: difficulty / ambiguity.&lt;/strong&gt; was it a defined task or an ambiguous, contested problem?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Score two efforts — "rewrote our ingestion service, cut latency 40%" and "drove a shared ingestion platform adopted by 6 teams" — and state which one is staff-level evidence and why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;th&gt;Blast radius&lt;/th&gt;
&lt;th&gt;Leverage&lt;/th&gt;
&lt;th&gt;Durability&lt;/th&gt;
&lt;th&gt;Ambiguity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rewrote our ingestion service&lt;/td&gt;
&lt;td&gt;2 (team)&lt;/td&gt;
&lt;td&gt;additive&lt;/td&gt;
&lt;td&gt;medium (I own it)&lt;/td&gt;
&lt;td&gt;low (defined)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drove shared ingestion platform&lt;/td&gt;
&lt;td&gt;3–4 (6 teams / org)&lt;/td&gt;
&lt;td&gt;multiplier&lt;/td&gt;
&lt;td&gt;high (teams own theirs)&lt;/td&gt;
&lt;td&gt;high (contested)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SCOPE-SIZING RUBRIC
===================
Score each effort; a STAFF effort needs multiplier leverage AND
blast radius &amp;gt;= 3 AND durability = high.

              blast   leverage     durability   ambiguity   -&amp;gt; level
              radius
------------------------------------------------------------------------
Ingestion       2      additive      medium        low       SENIOR
rewrite                (I did it)    (bus factor 1)

Ingestion       3-4    MULTIPLIER    high          high      STAFF
platform               (6 teams      (teams own    (contested
                        faster)       their part)   design)

GATES (all must pass for "staff evidence")
  [x] multiplier, not additive
  [x] blast radius &amp;gt;= 3 teams
  [x] value persists without me
  [x] I drove alignment under real ambiguity

VERDICT: lead the loop with the platform story; hold the rewrite for
         the deep-dive round as proof of depth.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Score blast radius first — it's the fastest disqualifier. The ingestion rewrite tops out at "team" (2); the platform reaches "several teams / org" (3–4). Anything scoring 1–2 is senior evidence no matter how technically impressive.&lt;/li&gt;
&lt;li&gt;Apply the leverage gate. This is binary, not a slider: the rewrite is additive (your output), the platform is a multiplier (others' output accelerates). An additive effort cannot be staff evidence regardless of its other scores.&lt;/li&gt;
&lt;li&gt;Check durability. The rewrite has a hidden negative: if you're the sole owner, it's bus-factor-one — a liability. The platform pushes ownership out to each team, so its value persists without you. Durability distinguishes leverage from heroics.&lt;/li&gt;
&lt;li&gt;Weigh ambiguity. The rewrite was a defined task; the platform was a contested, cross-team design with no clear owner. Ambiguity is where staff judgment shows, so it lifts the platform further.&lt;/li&gt;
&lt;li&gt;Convert scores to a strategy, not just a label: lead the behavioral rounds with the platform story (staff evidence) and &lt;em&gt;keep&lt;/em&gt; the rewrite for the deep-dive round, where depth is exactly what's being graded. Both stories are useful; the rubric tells you &lt;em&gt;where&lt;/em&gt; to deploy each.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;th&gt;Rubric verdict&lt;/th&gt;
&lt;th&gt;Where to use it in the loop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ingestion rewrite&lt;/td&gt;
&lt;td&gt;senior evidence (depth)&lt;/td&gt;
&lt;td&gt;deep-dive / coding round&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingestion platform&lt;/td&gt;
&lt;td&gt;staff evidence (leverage)&lt;/td&gt;
&lt;td&gt;behavioral / scope round&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Score every candidate story on blast radius and the additive-vs-multiplier gate before an interview. Lead with multiplier stories that score ≥3 on blast radius and high on durability; hold additive-but-deep stories for the technical rounds where depth is the thing being tested.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the one-page scope narrative doc
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The scope narrative is a one-page document per major effort that you maintain continuously and mine for both promo packets and interviews. It forces you to state the org-sized problem, your driving role, and the quantified multiplier before you need it under pressure. Walk through the template and fill it for a real effort.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header.&lt;/strong&gt; The effort name and the one-line multiplier ("Reclaimed ~3.5 eng-years/yr across 6 teams").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problem.&lt;/strong&gt; The org-sized, ambiguous framing — why it needed a staff, not a ticket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role.&lt;/strong&gt; What you &lt;em&gt;drove&lt;/em&gt; (RFC, alignment, mentoring) vs &lt;em&gt;did&lt;/em&gt; (built the reference impl).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome.&lt;/strong&gt; Quantified in other teams' metrics, with the counterfactual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifacts.&lt;/strong&gt; Links to the RFC, the adoption dashboard, the testimonials.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the one-page scope narrative for the shared ingestion platform effort so it can seed both a promo packet and an interview answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Content to capture&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Header + multiplier&lt;/td&gt;
&lt;td&gt;the headline number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Problem&lt;/td&gt;
&lt;td&gt;org-sized, ambiguous framing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role&lt;/td&gt;
&lt;td&gt;drove vs did&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outcome&lt;/td&gt;
&lt;td&gt;other teams' numbers + counterfactual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifacts&lt;/td&gt;
&lt;td&gt;RFC, adoption metrics, testimonials&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Scope narrative — Shared Ingestion Platform (2026 H1)&lt;/span&gt;

&lt;span class="gs"&gt;**Multiplier:**&lt;/span&gt; ~3.5 engineer-years/yr reclaimed across 6 teams;
new-source onboarding 3 weeks -&amp;gt; 2 days.

&lt;span class="gu"&gt;## Problem (org-sized, ambiguous)&lt;/span&gt;
Six teams each maintained a bespoke CDC ingestion service. Duplicated
~4 eng-years/yr, 6 inconsistent delete-handling stories feeding one
warehouse, and no single owner of "ingestion" as a capability.
Leadership ask was a goal ("ingestion is a mess"), not a spec.

&lt;span class="gu"&gt;## My role (drove &amp;gt; did)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Authored the platform RFC (linked).
&lt;span class="p"&gt;-&lt;/span&gt; Ran 3 cross-team design reviews; 6 teams + security.
&lt;span class="p"&gt;-&lt;/span&gt; Absorbed security's dissent on secrets handling into the design.
&lt;span class="p"&gt;-&lt;/span&gt; Got disagree-and-commit from the team that preferred its own tool.
&lt;span class="p"&gt;-&lt;/span&gt; Built the reference connector framework.
&lt;span class="p"&gt;-&lt;/span&gt; Mentored 1 engineer/team to own their connectors (durability).

&lt;span class="gu"&gt;## Outcome (their numbers + counterfactual)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 6 bespoke pipelines -&amp;gt; 1 platform, 18 connectors.
&lt;span class="p"&gt;-&lt;/span&gt; Onboarding a new source: 3 weeks -&amp;gt; 2 days.
&lt;span class="p"&gt;-&lt;/span&gt; Delete-handling now consistent across all sources.
&lt;span class="p"&gt;-&lt;/span&gt; Counterfactual: without it, 6 teams keep rebuilding CDC yearly.

&lt;span class="gu"&gt;## Artifacts&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; RFC: link  |  Adoption dashboard: link  |  Testimonials: 3 (linked)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The header leads with the multiplier number because every reader — promo committee, interviewer, skip-level — skims. "~3.5 engineer-years/yr across 6 teams" front-loads the leverage so the rest of the page is justification, not suspense.&lt;/li&gt;
&lt;li&gt;The Problem section is written to be &lt;em&gt;unambiguously org-sized&lt;/em&gt;: six teams, one warehouse, no owner, and a goal-not-spec mandate. This framing is what makes the effort promo-worthy and staff-level rather than a nice project.&lt;/li&gt;
&lt;li&gt;The Role section maintains the drive-then-build ratio in bullet form — five driving bullets, one building bullet — and includes the mentoring bullet explicitly, because durability (teams owning their own connectors) is what separates leverage from a bus-factor-one hero project.&lt;/li&gt;
&lt;li&gt;The Outcome section quantifies in the &lt;em&gt;other teams'&lt;/em&gt; currency (onboarding time, consistency) and states the counterfactual, which is what converts "we built a platform" into "we prevented six teams from wasting four engineer-years a year."&lt;/li&gt;
&lt;li&gt;The Artifacts section makes the whole thing &lt;em&gt;auditable&lt;/em&gt;: the RFC proves leadership, the adoption dashboard proves the multiplier, the testimonials prove influence. A scope claim without artifacts is discounted; with artifacts it's evidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consumer of the doc&lt;/th&gt;
&lt;th&gt;What they extract&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Promo committee&lt;/td&gt;
&lt;td&gt;multiplier + artifacts = a leveling case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staff interviewer&lt;/td&gt;
&lt;td&gt;scope-first behavioral story, pre-quantified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your manager&lt;/td&gt;
&lt;td&gt;ammunition for the calibration room&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Future you&lt;/td&gt;
&lt;td&gt;the numbers you'll otherwise forget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Keep one scope-narrative page per major effort and update it &lt;em&gt;the week the outcome lands&lt;/em&gt;, while the numbers are fresh. The single most expensive interview mistake is having done staff work but being unable to quantify it six months later.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — reframing an additive answer into a multiplier answer live
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Interviewers will sometimes catch you giving an additive answer and probe: "That's impressive, but what was the broader impact?" You need to &lt;em&gt;reframe on the fly&lt;/em&gt; from the system you built to the leverage it created. Walk through the live reframing move on a common additive answer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The additive answer.&lt;/strong&gt; "I optimized our nightly batch from 6 hours to 90 minutes."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reframe axes.&lt;/strong&gt; Who else was blocked by the 6-hour window? What did the freed time &lt;em&gt;enable&lt;/em&gt;? Did the technique spread?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The multiplier version.&lt;/strong&gt; The optimization unblocked downstream teams and the &lt;em&gt;technique&lt;/em&gt; became a standard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Reframe "I cut our nightly batch from 6h to 90m" from an additive answer into a multiplier answer without inventing facts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reframe axis&lt;/th&gt;
&lt;th&gt;Additive answer&lt;/th&gt;
&lt;th&gt;Multiplier reframe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who was blocked&lt;/td&gt;
&lt;td&gt;"our job was slow"&lt;/td&gt;
&lt;td&gt;"3 downstream teams waited on it"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What it enabled&lt;/td&gt;
&lt;td&gt;"job finished sooner"&lt;/td&gt;
&lt;td&gt;"morning SLA met; 3 teams' dashboards fresh by 7am"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did it spread&lt;/td&gt;
&lt;td&gt;"I tuned our job"&lt;/td&gt;
&lt;td&gt;"I documented the partition-pruning pattern; 5 teams adopted it"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIVE REFRAME — additive -&amp;gt; multiplier
=====================================

ADDITIVE (what most candidates say)
  "I cut our nightly batch from 6 hours to 90 minutes by fixing
   partition pruning and a skewed join."

  -&amp;gt; grades as: strong senior. Depth, single system.

MULTIPLIER (same facts, reframed for scope)
  "Our 6-hour batch was the critical-path dependency for 3 downstream
   teams whose dashboards missed the 7am SLA. I cut it to 90 minutes
   (partition pruning + skew fix), which unblocked all 3 teams' morning
   SLA. I then wrote up the partition-pruning pattern as an internal
   playbook; 5 other teams applied it to their own jobs. The fix became
   a standard, not a one-off."

  -&amp;gt; grades as: staff-adjacent. Blast radius (3 blocked + 5 adopters),
     multiplier (a spreading technique), durability (a playbook).

RULE: never state a technical win without stating who ELSE it unblocked
      and whether the TECHNIQUE spread.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start from the same true facts — the 6h→90m optimization is real. Reframing is not fabrication; it's surfacing the leverage that was always there but that the additive framing hid.&lt;/li&gt;
&lt;li&gt;Add the &lt;em&gt;who was blocked&lt;/em&gt; axis: the slow batch was a critical-path dependency for three downstream teams missing a 7am SLA. This converts "my job was slow" into "three teams were blocked," expanding the blast radius honestly.&lt;/li&gt;
&lt;li&gt;Add the &lt;em&gt;what it enabled&lt;/em&gt; axis: the fix unblocked all three teams' morning SLA. The outcome is now stated in the other teams' currency (their dashboards, their SLA), not yours (your runtime).&lt;/li&gt;
&lt;li&gt;Add the &lt;em&gt;did it spread&lt;/em&gt; axis: documenting the partition-pruning pattern as a playbook that five teams adopted turns a one-off tuning into a spreading technique — the durability + multiplier signal.&lt;/li&gt;
&lt;li&gt;The result grades a full level higher with zero invented facts. The lesson generalizes into a hard rule: never present a technical win without immediately attaching who else it unblocked and whether the technique propagated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framing&lt;/th&gt;
&lt;th&gt;Blast radius&lt;/th&gt;
&lt;th&gt;Leverage&lt;/th&gt;
&lt;th&gt;Interviewer grade&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Additive ("my job is faster")&lt;/td&gt;
&lt;td&gt;1 team&lt;/td&gt;
&lt;td&gt;additive&lt;/td&gt;
&lt;td&gt;strong senior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiplier (blocked + spread)&lt;/td&gt;
&lt;td&gt;3 + 5 teams&lt;/td&gt;
&lt;td&gt;multiplier + durable&lt;/td&gt;
&lt;td&gt;staff-adjacent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For every technical accomplishment, prepare the two extra sentences — "who else it unblocked" and "whether the technique spread" — before the interview. These two sentences are the difference between a senior grade and a staff grade on the identical underlying work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on demonstrating scope
&lt;/h3&gt;

&lt;p&gt;A staff interviewer might ask: "Tell me about your biggest-impact project. Walk me through how you'd measure that impact if I were skeptical of the number." The second sentence is the real test: it probes whether your impact claim survives &lt;em&gt;adversarial&lt;/em&gt; scrutiny, which separates candidates who genuinely operated at scope from those who inflated a team project into an org story. You must be able to derive the number, name the counterfactual, and concede its limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a defensible, counterfactual-anchored impact derivation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DEFENSIBLE IMPACT DERIVATION — biggest-impact project
=====================================================

THE CLAIM
  "The shared ingestion platform reclaimed ~3.5 engineer-years/yr
   across 6 teams."

THE DERIVATION (show your work)
  - Before: each of 6 teams spent ~0.7 eng-yr/yr building + maintaining
    a bespoke CDC connector set  -&amp;gt;  6 x 0.7 = 4.2 eng-yr/yr baseline.
  - After: the platform team spends ~0.7 eng-yr/yr; the 6 teams spend
    ~0 on ingestion internals (they configure, they don't build).
  - Net reclaimed: 4.2 - 0.7 = ~3.5 eng-yr/yr.

THE COUNTERFACTUAL
  "Without the platform, headcount growth would have forced a 7th and
   8th bespoke connector set this year. We avoided that."

THE HONEST LIMITS
  - "The 0.7/team figure is from 3 teams' time logs, extrapolated to 6.
     It's an estimate, not instrumented; I'd call it +/- 25%."
  - "Some of the reclaimed time went to other work, not pure savings;
     the durable win is CONSISTENCY (one delete-handling story) more
     than the headline hours."

THE MULTIPLIER RESTATEMENT
  "Even at the pessimistic end (~2.6 eng-yr/yr), the value is a
   multiplier: 6 teams ship ingestion in days, not weeks, forever."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What the answer does&lt;/th&gt;
&lt;th&gt;Why it survives scrutiny&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;State the claim&lt;/td&gt;
&lt;td&gt;one crisp number&lt;/td&gt;
&lt;td&gt;gives the interviewer a target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show the derivation&lt;/td&gt;
&lt;td&gt;6 × 0.7 − 0.7 = 3.5&lt;/td&gt;
&lt;td&gt;the number is reconstructable, not asserted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Name the counterfactual&lt;/td&gt;
&lt;td&gt;avoided 7th/8th bespoke set&lt;/td&gt;
&lt;td&gt;leverage is concrete, not hypothetical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concede the limits&lt;/td&gt;
&lt;td&gt;±25%, partly extrapolated&lt;/td&gt;
&lt;td&gt;honesty signals senior+ integrity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restate the multiplier&lt;/td&gt;
&lt;td&gt;even pessimistic case is leverage&lt;/td&gt;
&lt;td&gt;the conclusion is robust to the error bars&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The answer opens with a single crisp claim, then &lt;em&gt;shows its work&lt;/em&gt; — a simple 6 × 0.7 − 0.7 arithmetic any interviewer can follow and challenge. It anchors the value in a counterfactual (the bespoke connectors that were never built), then volunteers the estimate's limits before the interviewer has to extract them, which converts a potential credibility risk into an integrity signal. Finally it restates the conclusion at the &lt;em&gt;pessimistic&lt;/em&gt; end of the error bars to show the multiplier survives even if the headline number is soft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;This answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Number quality&lt;/td&gt;
&lt;td&gt;asserted, round, unfalsifiable&lt;/td&gt;
&lt;td&gt;derived, bounded, falsifiable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Counterfactual&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;explicit (avoided 7th/8th build)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intellectual honesty&lt;/td&gt;
&lt;td&gt;defends the number to the death&lt;/td&gt;
&lt;td&gt;concedes ±25% unprompted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Robustness&lt;/td&gt;
&lt;td&gt;collapses under one probe&lt;/td&gt;
&lt;td&gt;holds even at pessimistic end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real durable win&lt;/td&gt;
&lt;td&gt;conflated with the hours&lt;/td&gt;
&lt;td&gt;named separately (consistency)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Show-your-work derivation&lt;/strong&gt;&lt;/strong&gt; — reconstructing the number from first principles (per-team cost × teams, minus platform cost) makes it &lt;em&gt;falsifiable&lt;/em&gt;, and a falsifiable number reads as true. Round, un-derived numbers ("saved millions") read as inflated and invite a takedown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Counterfactual anchoring&lt;/strong&gt;&lt;/strong&gt; — impact is only meaningful against what would have happened otherwise. Naming the seventh and eighth bespoke connectors that were never built turns an abstract "savings" into a concrete avoided cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Volunteered limits&lt;/strong&gt;&lt;/strong&gt; — conceding the ±25% and the extrapolation &lt;em&gt;before being asked&lt;/em&gt; is the single strongest credibility move. It signals you reason about your own claims adversarially, which is exactly the judgment staff level requires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Multiplier-at-pessimistic-end&lt;/strong&gt;&lt;/strong&gt; — restating the conclusion at the low end of the error bars proves the argument doesn't depend on the optimistic number. A conclusion robust to its own uncertainty is far more persuasive than a precise-but-fragile one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the enabling investment is instrumentation you set up &lt;em&gt;before&lt;/em&gt; the win: time logs, adoption dashboards, before/after baselines. Capturing baselines is O(hours) up front but makes every future impact claim cheap and defensible; skipping it makes every claim a guess.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Platform and scope design problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Optimization&lt;/span&gt;
&lt;span&gt;Topic — optimization&lt;/span&gt;
&lt;strong&gt;Optimization problems behind multiplier impact stories&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/optimization" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Cross-team architecture loops and the RFC/design-review muscle
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Drive the decision in writing, then let the room converge — consensus, not command
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiuo38av449nqjmz04s1p.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiuo38av449nqjmz04s1p.jpeg" alt="Iconographic cross-team architecture-loop diagram — three team nodes connected by curved arrows around a central RFC document, an ADR decision-record card, and a review-gate glyph, plus a disagree-and-commit chip." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the &lt;code&gt;cross-team architecture&lt;/code&gt; loop is the repeatable cycle by which a staff engineer drives a contested decision to closure across teams they don't manage — write the proposal down (RFC), run it through structured design review, absorb and integrate dissent, record the decision (ADR), and let the room converge on a choice it now co-owns — and mastering this loop &lt;em&gt;in writing&lt;/em&gt; is the core &lt;code&gt;technical leadership&lt;/code&gt; skill the staff interview probes, because it is how you produce org-level alignment with team-level authority&lt;/strong&gt;. The junior instinct is to win the argument in the meeting; the staff instinct is to make the argument &lt;em&gt;reviewable&lt;/em&gt; so the decision outlives the meeting and the room feels ownership of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The architecture-decision RACI — who does what in a cross-team decision.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Responsible.&lt;/strong&gt; The staff engineer driving the RFC — writes it, runs the reviews, integrates feedback. This is you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accountable.&lt;/strong&gt; Usually a single decision-maker (a principal, a director, or an explicitly-named DRI) who owns the final call if consensus stalls. Name this person &lt;em&gt;before&lt;/em&gt; the review, not after it deadlocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consulted.&lt;/strong&gt; The teams affected — they provide input, dissent, constraints. Their sign-off is what makes the decision stick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Informed.&lt;/strong&gt; Downstream teams and leadership who need to know but don't shape the call. Over-consulting the "informed" group is a classic way to stall a decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The RFC / design-doc loop — the artifact that carries the decision.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why writing wins.&lt;/strong&gt; A meeting persuades the people in the room once; a document persuades everyone, asynchronously, forever, and forces &lt;em&gt;you&lt;/em&gt; to find the holes before the room does. Amazon's "narrative over slides" culture exists for exactly this reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The RFC shape.&lt;/strong&gt; Context and problem → goals and non-goals → the options considered → the recommendation → the trade-offs → the rollout and rollback. Non-goals are as important as goals; they prevent scope creep during review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The review cadence.&lt;/strong&gt; Circulate async for comments, then hold a focused review meeting for the &lt;em&gt;unresolved&lt;/em&gt; disagreements only — never read the doc aloud. The meeting is for dissent, not narration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ADR.&lt;/strong&gt; After the decision, record it as an Architecture Decision Record: the decision, the context, the alternatives rejected, and the consequences. The ADR is what stops the org re-litigating the same choice in six months.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Handling dissent — the disagree-and-commit machinery.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Steel-man the dissent.&lt;/strong&gt; Restate the objecting team's position better than they did. This earns the right to be disagreed with, and often improves your design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrate what's right.&lt;/strong&gt; Real dissent usually contains a real constraint you missed. Fold it into the RFC and credit the objector — this converts an adversary into a co-owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name one-way vs two-way doors.&lt;/strong&gt; Reversible decisions (two-way doors) can be made fast and revisited; irreversible ones (one-way doors) deserve more consensus. Say which kind you're facing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalate cleanly when stuck.&lt;/strong&gt; If consensus genuinely fails, take the &lt;em&gt;narrowed&lt;/em&gt; disagreement to the Accountable DRI with a recommendation, not a shrug. Disagree-and-commit means the room commits to the DRI's call even if some still disagree.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The tech radar and decision records — making decisions durable.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tech radar.&lt;/strong&gt; An adopt / trial / assess / hold quadrant for technologies. It turns "should we use X?" from a per-team debate into an org-level standard, which is leverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision records over tribal knowledge.&lt;/strong&gt; Every consequential choice gets an ADR. New engineers read the ADRs instead of re-discovering the reasoning; the org stops paying the "why did we do this?" tax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-way vs two-way in the radar.&lt;/strong&gt; "Hold" is a one-way-door signal ("don't adopt this without a strong case"); "trial" is a two-way door ("try it, we can back out").&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — an RFC / ADR skeleton
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The RFC is the load-bearing artifact of cross-team leadership, and having a skeleton you can instantiate fast is what lets you &lt;em&gt;lead in writing&lt;/em&gt; under time pressure. Walk through a skeleton that covers context, goals/non-goals, options, recommendation, trade-offs, and rollout, plus the companion ADR that records the outcome.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RFC front matter.&lt;/strong&gt; Title, author, reviewers (by team), status, one-way-or-two-way-door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body.&lt;/strong&gt; Problem → goals/non-goals → options (with trade-offs) → recommendation → rollout/rollback → open questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ADR companion.&lt;/strong&gt; The decision, the alternatives rejected, the consequences — written &lt;em&gt;after&lt;/em&gt; the review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The discipline.&lt;/strong&gt; Non-goals and rejected-alternatives are mandatory; they're what stop re-litigation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Provide an RFC skeleton for a contested cross-team schema decision and the ADR that records the outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RFC&lt;/td&gt;
&lt;td&gt;drive the decision, async + review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Options section&lt;/td&gt;
&lt;td&gt;make trade-offs explicit and comparable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-goals&lt;/td&gt;
&lt;td&gt;prevent scope creep in review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADR&lt;/td&gt;
&lt;td&gt;record the decision so it isn't re-litigated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# RFC-014: Canonical event schema for the orders domain&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Author: &lt;span class="nt"&gt;&amp;lt;you&amp;gt;&lt;/span&gt;            Status: In review (target: 2026-08-15)
&lt;span class="p"&gt;-&lt;/span&gt; Reviewers: Orders (DRI: Priya), Payments, Search, Data Platform, Security
&lt;span class="p"&gt;-&lt;/span&gt; Door: TWO-WAY for field names (renamable via alias); ONE-WAY for the
        partition key (rewrites history) -&amp;gt; partition key needs consensus.

&lt;span class="gu"&gt;## 1. Problem&lt;/span&gt;
Three teams emit an "order" event with incompatible shapes. The warehouse
runs 3 reconciliation jobs to unify them; Search re-derives fields the
others already have. No canonical schema owner exists.

&lt;span class="gu"&gt;## 2. Goals / Non-goals&lt;/span&gt;
Goals:     one canonical orders event; backward-compatible evolution;
           a single delete-handling story.
Non-goals: unifying the CUSTOMER domain (separate RFC); changing the
           event bus; real-time (this is about schema, not transport).

&lt;span class="gu"&gt;## 3. Options considered&lt;/span&gt;
| Option | Pros | Cons |
|--------|------|------|
| A. Superset schema (all fields) | no team loses data | wide, sparse, unclear ownership |
| B. Core + extensions per team | clean core; teams extend | extension governance needed |
| C. Status quo + reconciliation | no migration | 3 recon jobs forever; drift |

&lt;span class="gu"&gt;## 4. Recommendation&lt;/span&gt;
Option B. Core owned by Orders; teams add namespaced extensions;
freshness + schema enforced in CI via data contracts.

&lt;span class="gu"&gt;## 5. Trade-offs&lt;/span&gt;
We trade some upfront governance (an extension review) for eliminating
3 reconciliation jobs and permanent schema drift.

&lt;span class="gu"&gt;## 6. Rollout / rollback&lt;/span&gt;
Dual-write core+legacy for 1 sprint; migrate consumers; deprecate legacy.
Rollback = keep reading legacy (dual-write makes this reversible).

&lt;span class="gu"&gt;## 7. Open questions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Who approves new extensions? (proposed: Orders DRI + Data Platform)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# ADR-014: Adopt core+extensions schema for the orders domain&lt;/span&gt;

Status: Accepted (2026-08-15). Supersedes ad-hoc per-team schemas.

Decision: Option B (core + namespaced extensions), per RFC-014.

Context: 3 incompatible order events; 3 reconciliation jobs; no owner.

Alternatives rejected:
&lt;span class="p"&gt;-&lt;/span&gt; Superset (A): unbounded width, no clear ownership.
&lt;span class="p"&gt;-&lt;/span&gt; Status quo (C): permanent reconciliation cost and drift.

Consequences:
&lt;span class="p"&gt;-&lt;/span&gt; (+) 3 reconciliation jobs removed; one delete-handling story.
&lt;span class="p"&gt;-&lt;/span&gt; (+) Orders owns the core; extension review governs additions.
&lt;span class="p"&gt;-&lt;/span&gt; (-) New process: extension approvals (Orders DRI + Data Platform).
&lt;span class="p"&gt;-&lt;/span&gt; Dissent integrated: Search's latency concern -&amp;gt; extensions are
  read-time optional, not required joins. (credit: Marco, Search)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The front matter names the reviewers &lt;em&gt;by team&lt;/em&gt; and the DRI up front, and classifies the decision's reversibility per-field. Declaring "partition key is a one-way door, needs consensus" tells the room where to spend its scrutiny and where to move fast.&lt;/li&gt;
&lt;li&gt;Goals/non-goals do the scope-control work. By explicitly ruling the customer domain and the event bus &lt;em&gt;out of scope&lt;/em&gt;, the RFC prevents the review from ballooning into an everything-schema debate — the most common way cross-team RFCs die.&lt;/li&gt;
&lt;li&gt;The options table makes trade-offs &lt;em&gt;comparable&lt;/em&gt; rather than advocating a single answer. Presenting A/B/C with honest cons (including for the recommended option) signals you've genuinely considered alternatives, which earns the room's trust and preempts "did you think about X?"&lt;/li&gt;
&lt;li&gt;The recommendation is decisive but grounded in the trade-off it accepts (upfront governance in exchange for killing three reconciliation jobs). Naming what you trade &lt;em&gt;away&lt;/em&gt; is the staff move; a recommendation with no stated cost reads as naive.&lt;/li&gt;
&lt;li&gt;The ADR, written after the review, records the decision &lt;em&gt;and the integrated dissent&lt;/em&gt; — Search's latency concern became "extensions are read-time optional," credited to the objector. This is the disgree-and-commit machinery made durable: the objection is honored in writing, and the org never re-litigates it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RFC element&lt;/th&gt;
&lt;th&gt;What it prevents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Non-goals&lt;/td&gt;
&lt;td&gt;scope creep during review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Options table with honest cons&lt;/td&gt;
&lt;td&gt;"did you consider X?" ambush&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Door classification&lt;/td&gt;
&lt;td&gt;over-consensus on reversible calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADR + integrated dissent&lt;/td&gt;
&lt;td&gt;re-litigating the decision later&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never drive a cross-team decision in a meeting alone — write the RFC first, make non-goals and rejected alternatives mandatory sections, classify each sub-decision as a one-way or two-way door, and close with an ADR that credits the dissent you integrated. The document is the leadership; the meeting is just where the last disagreements get resolved.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a design-review facilitation script
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Running the review meeting well is a distinct skill from writing the RFC. A staff engineer facilitates so that the &lt;em&gt;decision&lt;/em&gt; gets made, dissent is heard, and the room converges — without the meeting devolving into narration or a rehash of resolved comments. Walk through a facilitation script for a cross-team design review.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-read enforcement.&lt;/strong&gt; The doc was circulated async; the meeting assumes it's read. Reserve the first few minutes of silent reading only if you must.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agenda by disagreement.&lt;/strong&gt; Spend time only on the unresolved comments and the open questions, not on the parts everyone agreed with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeboxing and parking.&lt;/strong&gt; Each contested point gets a timebox; unresolvable-in-the-room items get parked with an owner and a date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit decision + disagree-and-commit.&lt;/strong&gt; End by stating the decision, confirming commit even from dissenters, and naming the ADR owner.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the facilitation script a staff engineer uses to run the RFC-014 schema review to a decision in 45 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frame&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;restate decision + door type + DRI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dissent&lt;/td&gt;
&lt;td&gt;25 min&lt;/td&gt;
&lt;td&gt;work only the unresolved comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Converge&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;td&gt;decide or escalate; disagree-and-commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Close&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;ADR owner, follow-ups, dates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DESIGN-REVIEW FACILITATION SCRIPT — RFC-014 (45 min)
====================================================

[0:00] FRAME (5 min)
  "This review decides the orders event schema. Door: partition key is
   one-way (needs consensus today); field names are two-way. DRI if we
   don't converge: Priya. The doc was pre-read; we won't narrate it.
   Agenda is the 4 unresolved comments + 1 open question."

[0:05] WORK THE DISSENT (25 min) — one contested item at a time
  For each item:
   1. "Marco (Search), restate your concern in one sentence."
   2. Steel-man it back: "So extensions as required joins add read
      latency for Search. Is that right?"
   3. Options on the table, timebox 6 min.
   4. Decide or park: "Resolved: extensions are read-time optional.
      Marco, does that address it?" -&amp;gt; integrate into doc live.

[0:30] CONVERGE (10 min)
  "Recommendation stands: Option B, core owned by Orders. Any blocking
   objection to the ONE-WAY item (partition key = order_id)?"
   - If none: decided.
   - If blocked and unresolved: "We disagree-and-commit to Priya's call:
     partition key = order_id. Noted dissent: Payments prefers composite."

[0:40] CLOSE (5 min)
  "Decision: Option B, partition key order_id. ADR owner: me, by Fri.
   Follow-ups: extension-approval process (owner: Priya + Data Platform,
   next week). Thank you — dissent from Payments is recorded, not erased."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Frame phase sets the &lt;em&gt;decision to be made&lt;/em&gt;, the door types, and the escalation DRI in the first five minutes. Rooms drift when they don't know whether they're deciding or discussing; naming the DRI up front means a stall has a pre-agreed resolution rather than an awkward silence.&lt;/li&gt;
&lt;li&gt;The "work the dissent" phase is explicitly &lt;em&gt;not&lt;/em&gt; a walkthrough — it iterates the unresolved comments only. For each, the facilitator makes the objector restate the concern, then &lt;em&gt;steel-mans it back&lt;/em&gt;, which both confirms understanding and signals respect, defusing defensiveness.&lt;/li&gt;
&lt;li&gt;Integrating resolutions &lt;em&gt;live&lt;/em&gt; into the doc ("extensions are read-time optional") means the artifact leaves the meeting already updated, and the objector sees their concern honored in real time — the fastest path to genuine commit rather than grudging silence.&lt;/li&gt;
&lt;li&gt;The Converge phase isolates the one-way-door decision (the partition key) for explicit consensus, and if consensus fails, invokes disagree-and-commit to the named DRI &lt;em&gt;with the dissent recorded&lt;/em&gt;. This is how a decision gets made without steamrolling the minority.&lt;/li&gt;
&lt;li&gt;The Close phase assigns the ADR owner and the follow-up owners with dates, and explicitly states that dissent is "recorded, not erased." Leaving the room with owners and honored dissent is what makes the decision stick and keeps the dissenters willing to engage next time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Facilitation move&lt;/th&gt;
&lt;th&gt;Failure it prevents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frame the decision + DRI&lt;/td&gt;
&lt;td&gt;endless discussion with no decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agenda = unresolved only&lt;/td&gt;
&lt;td&gt;re-reading the doc aloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steel-man each dissent&lt;/td&gt;
&lt;td&gt;objectors feeling unheard → later sabotage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disagree-and-commit + recorded dissent&lt;/td&gt;
&lt;td&gt;steamrolling or false consensus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADR owner + dated follow-ups&lt;/td&gt;
&lt;td&gt;the decision quietly not happening&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Facilitate design reviews by decision, not by document: frame the call and the DRI in the first five minutes, spend the middle only on unresolved dissent (steel-manned and integrated live), and close with an explicit decision, disagree-and-commit, a recorded dissent, and dated owners. A review that ends without a decision and an owner was a status meeting, not a design review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on cross-team alignment
&lt;/h3&gt;

&lt;p&gt;A staff interviewer might ask: "Three teams need to agree on a shared schema. Two of them have already shipped incompatible versions and each thinks theirs should win. You have no authority over any of them and a six-week deadline. Walk me through exactly how you'd get to a decision everyone actually commits to." This is the signature staff scenario: contested, cross-team, deadline-bound, zero authority. The interviewer is grading your alignment &lt;em&gt;process&lt;/em&gt;, not your schema opinion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using the write-review-integrate-record alignment loop
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALIGNMENT LOOP — 3 teams, incompatible schemas, no authority, 6 weeks
=====================================================================

WEEK 1 — UNDERSTAND BEFORE PROPOSING
  - 1:1 with each team lead. Goal: understand WHY each shipped what they
    did. Capture the real constraint behind each version (Search needs
    field X denormalized; Payments needs a composite key for idempotency).
  - Name the Accountable DRI now (align with the 3 managers on one).

WEEK 2 — WRITE THE RFC
  - Options table: Team A's shape, Team B's shape, a synthesized Option C.
  - Steel-man A and B in the doc so both feel represented.
  - Classify doors: field names two-way; partition key one-way.
  - Circulate async; collect comments in the doc.

WEEK 3 — DESIGN REVIEW #1
  - Work only the unresolved comments. Integrate Payments' idempotency
    constraint into Option C live. Park the one truly-contested item.

WEEK 4 — RESOLVE THE PARKED ITEM
  - The one-way-door partition key. If A and B still disagree, take the
    NARROWED question + a recommendation to the DRI. Disagree-and-commit.

WEEK 5 — ADR + MIGRATION PLAN
  - Record the decision + rejected alternatives + integrated dissent.
  - Dual-write plan so BOTH existing versions have a reversible path in.

WEEK 6 — LAND + FOLLOW-UP OWNERS
  - Reference implementation + one owner per team mentored.
  - Extension-governance follow-up assigned with a date.

WHY THEY COMMIT
  Each team's real constraint is visibly in the final design; the DRI
  broke the one genuine tie; dissent is recorded, not erased.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Week&lt;/th&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;th&gt;Alignment mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1:1s to find each team's real constraint&lt;/td&gt;
&lt;td&gt;influence starts with understanding, not proposing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;name the DRI&lt;/td&gt;
&lt;td&gt;a pre-agreed tiebreaker prevents deadlock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;RFC with steel-manned options&lt;/td&gt;
&lt;td&gt;both teams feel represented in writing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;review works unresolved items; integrate live&lt;/td&gt;
&lt;td&gt;dissent becomes co-ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;escalate the narrowed one-way-door item&lt;/td&gt;
&lt;td&gt;disagree-and-commit on the one real tie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5–6&lt;/td&gt;
&lt;td&gt;ADR + dual-write + mentored owners&lt;/td&gt;
&lt;td&gt;reversible migration + durability&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The loop front-loads &lt;em&gt;understanding&lt;/em&gt; — a full week of 1:1s to surface why each team built what it did — because you cannot align teams whose real constraints you don't know. It names the DRI early so the one genuinely-contested decision has a pre-agreed resolution. The RFC steel-mans both existing versions so neither team feels erased, the review integrates the discovered constraints live, and only the single one-way-door decision (the partition key) is escalated — narrowed to one question with a recommendation. Dual-write gives both incumbent versions a reversible on-ramp, and mentoring one owner per team makes the outcome durable. Teams commit because each sees its real constraint in the final design and because the one true tie was broken by a legitimate DRI, not by whoever argued loudest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;This answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Process&lt;/td&gt;
&lt;td&gt;"I'd get everyone in a room"&lt;/td&gt;
&lt;td&gt;a 6-week write-review-integrate-record loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Understanding&lt;/td&gt;
&lt;td&gt;jumps to proposing a schema&lt;/td&gt;
&lt;td&gt;week of 1:1s to find real constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authority handling&lt;/td&gt;
&lt;td&gt;"I'd convince them"&lt;/td&gt;
&lt;td&gt;named DRI + disagree-and-commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dissent&lt;/td&gt;
&lt;td&gt;"I'd get buy-in"&lt;/td&gt;
&lt;td&gt;steel-manned, integrated, recorded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durability&lt;/td&gt;
&lt;td&gt;"then we ship"&lt;/td&gt;
&lt;td&gt;dual-write + mentored per-team owners&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Understand-before-propose&lt;/strong&gt;&lt;/strong&gt; — starting with 1:1s to surface each team's real constraint is what makes alignment possible; a proposal written before you understand the constraints will be rejected on contact. Influence without authority is built on being the person who understood everyone's problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Named DRI + disagree-and-commit&lt;/strong&gt;&lt;/strong&gt; — pre-agreeing on who breaks a tie converts a potential deadlock into a bounded decision. Disagree-and-commit lets the minority stay engaged: they commit to execution without pretending to agree, and their dissent is recorded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Steel-manned options in writing&lt;/strong&gt;&lt;/strong&gt; — representing both incumbent schemas fairly in the RFC means neither team enters the review feeling it has already lost, which is the precondition for genuine (not grudging) commitment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Doors classification&lt;/strong&gt;&lt;/strong&gt; — spending consensus only on the one irreversible decision (the partition key) and moving fast on the reversible ones (field names, aliasable) is how you hit a six-week deadline without either steamrolling or stalling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the loop costs ~6 weeks of calendar and a week of pure listening before any code — expensive in the short run, but it's O(1) in re-litigation: the ADR and integrated dissent mean the decision is made &lt;em&gt;once&lt;/em&gt;. The alternative (winning the meeting, skipping the doc) is cheap up front and O(N) forever as the choice gets re-argued every quarter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Cross-team architecture and schema design problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Streaming&lt;/span&gt;
&lt;span&gt;Topic — streaming&lt;/span&gt;
&lt;strong&gt;Streaming and event-schema alignment problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. System design at staff/principal altitude: trade-offs and failure narratives
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Requirements first, trade-offs second, tech names last — the altitude of a staff answer
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv8ltbw1osaxavg9vaj07.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv8ltbw1osaxavg9vaj07.jpeg" alt="Iconographic staff system-design diagram — a lakehouse/streaming data platform with ingestion, storage and serving tiers, annotated with a cost/latency/reliability trade-off dial and an SLA freshness gauge, plus a failure-mode blast-radius chip." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a staff/principal system-design answer operates at a higher &lt;em&gt;altitude&lt;/em&gt; than a senior one — it starts from requirements and non-functional requirements, reasons explicitly about the &lt;code&gt;architecture review&lt;/code&gt; trade-offs between cost, latency, and reliability, walks a concrete design, and then narrates the &lt;em&gt;failure modes and their blast radius&lt;/em&gt; — whereas a weaker answer jumps straight to naming technologies ("I'd use Kafka and Snowflake") before establishing what problem the design must actually solve&lt;/strong&gt;. The senior answer builds a system that works; the staff answer defends a system against the ways it will break and the budget it will consume, and names what it deliberately does &lt;em&gt;not&lt;/em&gt; optimize.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The altitude ladder — where a staff answer spends its time.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requirements first.&lt;/strong&gt; Before any box on the whiteboard: who are the consumers, what's the data volume and velocity, what's the freshness SLA, what's the consistency requirement, what's the budget? A staff candidate spends the first several minutes here and &lt;em&gt;drives&lt;/em&gt; the requirement-gathering rather than waiting to be told.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-functional requirements (NFRs) second.&lt;/strong&gt; Latency, throughput, availability, cost, security, multi-tenancy, evolvability. NFRs are where the design's hard trade-offs live; naming them up front is the altitude signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design third.&lt;/strong&gt; Now draw the boxes — ingestion, storage, transformation, serving — chosen to satisfy the NFRs, not because they're trendy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure narratives last (and most important).&lt;/strong&gt; What happens when a source is down, a partition is hot, the warehouse is unreachable, a schema changes? Name the blast radius of each failure and the mitigation. This is what separates a staff answer from a diagram.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The cost / latency / reliability trilemma — you cannot max all three.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The dial.&lt;/strong&gt; Every data-platform decision trades among cost, latency, and reliability. Real-time exactly-once is expensive; cheap batch is high-latency; ultra-reliable multi-region doubles cost. Naming the dial and where you set it is the trade-off muscle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency vs cost.&lt;/strong&gt; Streaming (seconds) costs more in always-on compute than batch (hours). Pick latency to the &lt;em&gt;requirement&lt;/em&gt;, not the maximum — "the BI consumers need hourly, the ML feature store needs sub-minute" justifies a hybrid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability vs cost.&lt;/strong&gt; Multi-region replication, exactly-once, and long retention all cost money. Match the reliability tier to the data's criticality; not every dataset deserves five-nines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The staff move.&lt;/strong&gt; State the trilemma explicitly, set the dial per-consumer, and justify the setting from the requirements. Never silently max one axis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Non-functional requirements for a data platform — the checklist.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Freshness / latency SLA.&lt;/strong&gt; Per consumer. BI hourly, ML feature store sub-minute, audit daily. Different SLAs justify different architectures for different lanes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput and scale.&lt;/strong&gt; Events/sec, GB/day, peak vs average. The peak drives the design; the average drives the cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenancy and isolation.&lt;/strong&gt; Can one tenant's load or bad data affect another? Isolation is a first-class NFR for platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evolvability.&lt;/strong&gt; Schema change, new consumers, new sources — the platform must accommodate change without a rewrite. Staff designs optimize for the changes that &lt;em&gt;will&lt;/em&gt; come.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Failure narratives — the part juniors skip and staff lead with.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source failure.&lt;/strong&gt; A source DB is down or lagging. Blast radius: which downstream consumers miss SLA? Mitigation: backfill, watermark, graceful degradation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hot partition / skew.&lt;/strong&gt; One key dominates. Blast radius: a single lane backs up. Mitigation: salting, repartitioning, isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downstream unavailability.&lt;/strong&gt; The warehouse or bus is unreachable. Blast radius: buffering limits, data loss risk. Mitigation: durable buffering, backpressure, retention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bad data / poison record.&lt;/strong&gt; A malformed record breaks the pipeline. Blast radius: one lane vs the whole platform. Mitigation: dead-letter queue, schema/contract enforcement, quarantine.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the NFR trade-off matrix
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The NFR trade-off matrix is the artifact that turns "I'd use X" into "here's what each option costs on each axis that matters." Building it live in a design interview is the clearest possible altitude signal. Walk through constructing the matrix for the batch-vs-streaming-vs-hybrid decision on a data platform.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The axes.&lt;/strong&gt; Freshness, cost, operational complexity, reliability/exactly-once, evolvability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The options.&lt;/strong&gt; Batch (hourly), streaming (seconds), hybrid (streaming hot path + batch reconciliation).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scoring.&lt;/strong&gt; Honest per-axis; the "best" option depends on the requirement, not on a universal winner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The conclusion.&lt;/strong&gt; Pick per-consumer, justify from the SLA.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the NFR trade-off matrix for batch vs streaming vs hybrid ingestion, and recommend a setting for a platform serving both hourly BI and sub-minute ML features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;th&gt;Freshness need&lt;/th&gt;
&lt;th&gt;Criticality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BI dashboards&lt;/td&gt;
&lt;td&gt;hourly&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML feature store&lt;/td&gt;
&lt;td&gt;sub-minute&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit / compliance&lt;/td&gt;
&lt;td&gt;daily&lt;/td&gt;
&lt;td&gt;high (durability)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NFR TRADE-OFF MATRIX — ingestion architecture
=============================================
                     BATCH        STREAMING       HYBRID
                     (hourly)     (seconds)       (stream hot + batch recon)
------------------------------------------------------------------------
Freshness            hourly       sub-second      sub-min hot / hourly cold
Cost                 $            $$$             $$  (pay stream only for
                                                      the hot lane)
Ops complexity       low          high            medium-high
Reliability /        easy         hard (dedup,    hot=at-least-once +
exactly-once         (idempotent  watermarks,     batch recon fixes dupes
                     reload)      state)          -&amp;gt; effectively exactly-once
Evolvability         high         medium          high (batch is the
                                                      source of truth)
------------------------------------------------------------------------
RECOMMENDATION
  Hybrid. Streaming hot path feeds the ML feature store (sub-minute,
  high criticality). Batch reconciliation feeds BI (hourly) AND repairs
  the hot path's duplicates -&amp;gt; exactly-once semantics without paying
  streaming's exactly-once complexity everywhere.
  Audit reads the batch (durable, daily) tier.

WHY NOT max freshness everywhere: streaming exactly-once for BI would
  triple cost for a consumer that only needs hourly. Set the dial per
  consumer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The matrix scores each option on the axes that the &lt;em&gt;requirements&lt;/em&gt; made relevant — freshness, cost, ops, reliability, evolvability — rather than on generic virtues. The axes come from the NFRs, which is why requirement-gathering has to happen first.&lt;/li&gt;
&lt;li&gt;The cost row is where the trilemma bites: streaming is $$$ because always-on compute and exactly-once state are expensive. Making cost an explicit row prevents the common failure of maxing freshness while ignoring the bill.&lt;/li&gt;
&lt;li&gt;The reliability row exposes the subtle staff insight: pure streaming exactly-once is hard, but a &lt;em&gt;hybrid&lt;/em&gt; gets effectively-exactly-once cheaply by letting periodic batch reconciliation repair the hot path's duplicates. This is a trade-off design, not a technology name.&lt;/li&gt;
&lt;li&gt;The recommendation sets the dial &lt;em&gt;per consumer&lt;/em&gt;: streaming only for the sub-minute high-criticality ML lane, batch for hourly BI and daily audit. This directly answers the requirement instead of applying one architecture everywhere.&lt;/li&gt;
&lt;li&gt;The explicit "why not max freshness everywhere" is the altitude clincher — it names the rejected alternative (streaming for BI) and its cost (3× for no benefit), demonstrating that the design is a &lt;em&gt;deliberate&lt;/em&gt; trade-off, not a default.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;th&gt;Chosen lane&lt;/th&gt;
&lt;th&gt;Justification from requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ML feature store&lt;/td&gt;
&lt;td&gt;streaming hot path&lt;/td&gt;
&lt;td&gt;sub-minute + high criticality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BI dashboards&lt;/td&gt;
&lt;td&gt;batch tier&lt;/td&gt;
&lt;td&gt;hourly is enough; save the cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit&lt;/td&gt;
&lt;td&gt;batch (durable)&lt;/td&gt;
&lt;td&gt;daily + durability, not freshness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dedup / correctness&lt;/td&gt;
&lt;td&gt;batch reconciliation&lt;/td&gt;
&lt;td&gt;exactly-once without streaming's cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; In any staff design round, draw the NFR trade-off matrix before drawing the architecture. Set the cost/latency/reliability dial &lt;em&gt;per consumer&lt;/em&gt; from the SLA, and explicitly name the alternative you rejected and why. A design that maxes one axis silently is a senior answer; a design that sets the dial per requirement is a staff answer.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a lakehouse/streaming design walkthrough with an SLA freshness check
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; With requirements and NFRs established, the staff candidate walks a concrete design and — critically — shows how the platform &lt;em&gt;measures its own SLA&lt;/em&gt;. Walk through a medallion lakehouse with a streaming hot path, and include a real freshness-monitoring query, because "how do you know the SLA is met?" is a guaranteed follow-up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion.&lt;/strong&gt; Streaming hot path (CDC → bus) for the ML lane; batch loads for cold sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage.&lt;/strong&gt; Medallion lakehouse: bronze (raw) → silver (cleaned/conformed) → gold (serving marts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serving.&lt;/strong&gt; ML feature store off silver/gold streaming; BI off gold batch; audit off bronze/silver durable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability.&lt;/strong&gt; A freshness SLA query per gold table, alerted when freshness exceeds the per-consumer budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Walk the lakehouse design and provide the freshness-SLA monitoring query that proves the platform meets its per-table freshness budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bronze&lt;/td&gt;
&lt;td&gt;raw CDC + batch&lt;/td&gt;
&lt;td&gt;audit, reprocessing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;silver&lt;/td&gt;
&lt;td&gt;cleaned, conformed&lt;/td&gt;
&lt;td&gt;ML features, quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gold&lt;/td&gt;
&lt;td&gt;serving marts&lt;/td&gt;
&lt;td&gt;BI, ML feature store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Freshness SLA check: is each gold table within its per-consumer budget?&lt;/span&gt;
&lt;span class="c1"&gt;-- gold_sla_config(table_name, freshness_budget_minutes) is the contract.&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;freshness&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freshness_budget_minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPOCH&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loaded_at&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;minutes_since_load&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;        &lt;span class="n"&gt;gold_sla_config&lt;/span&gt;       &lt;span class="n"&gt;t&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt;        &lt;span class="n"&gt;gold_load_audit&lt;/span&gt;       &lt;span class="k"&gt;g&lt;/span&gt;  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freshness_budget_minutes&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes_since_load&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;age_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;freshness_budget_minutes&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;budget_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;minutes_since_load&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;freshness_budget_minutes&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'OK'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;minutes_since_load&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;freshness_budget_minutes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'WARN'&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'BREACH'&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;                                             &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sla_status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;freshness&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;minutes_since_load&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;freshness_budget_minutes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- worst first&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DESIGN WALKTHROUGH — narration to pair with the diagram
=======================================================
1. Ingestion: CDC hot path -&amp;gt; bus -&amp;gt; streaming into bronze (sub-min) for
   the ML lane. Batch loads for cold/low-priority sources into bronze.
2. bronze -&amp;gt; silver: streaming + micro-batch cleaning/conforming. Data
   contracts enforced here (schema + freshness) -&amp;gt; bad data quarantined.
3. silver -&amp;gt; gold: serving marts. ML feature store reads streaming gold
   (sub-min budget); BI reads batch gold (60-min budget).
4. Observability: the freshness query above runs every 5 min; BREACH
   pages the owning team. Each gold table has a budget in gold_sla_config.
5. Failure mode: if the hot path stalls, silver freshness breaches its
   budget, the query flags BREACH, and BI silently falls back to the last
   good batch gold (degraded, not broken). Blast radius = ML lane only.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The design assigns each consumer to a lane whose freshness budget matches its SLA — sub-minute streaming gold for ML, 60-minute batch gold for BI — implementing the per-consumer dial from the NFR matrix as concrete boxes.&lt;/li&gt;
&lt;li&gt;Data contracts are enforced at the bronze→silver boundary, so a bad record is quarantined &lt;em&gt;before&lt;/em&gt; it reaches serving. This bounds the blast radius of a poison record to one lane rather than the whole platform — a failure-narrative decision baked into the architecture.&lt;/li&gt;
&lt;li&gt;The freshness query is the observability backbone: it joins each gold table's actual age against its contracted budget in &lt;code&gt;gold_sla_config&lt;/code&gt; and classifies OK/WARN/BREACH. Ordering by the &lt;em&gt;ratio&lt;/em&gt; of age to budget surfaces the most-at-risk table first, which is what an on-call engineer needs.&lt;/li&gt;
&lt;li&gt;The query answers the guaranteed interview follow-up — "how do you know the SLA is met?" — with a running, per-table, contract-driven check rather than a hand-wave. Showing the measurement is a stronger altitude signal than showing the pipeline.&lt;/li&gt;
&lt;li&gt;The failure narration closes the loop: a stalled hot path breaches silver freshness, the query flags it, BI degrades gracefully to the last good batch, and the blast radius is confined to the ML lane. Naming the degraded-but-not-broken behavior and the blast radius is exactly the staff move juniors omit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;table_name&lt;/th&gt;
&lt;th&gt;age_min&lt;/th&gt;
&lt;th&gt;budget_min&lt;/th&gt;
&lt;th&gt;sla_status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ml_features_gold&lt;/td&gt;
&lt;td&gt;4.2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;orders_gold_bi&lt;/td&gt;
&lt;td&gt;44.0&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sessions_gold&lt;/td&gt;
&lt;td&gt;78.0&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;WARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;payments_gold&lt;/td&gt;
&lt;td&gt;133.0&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;BREACH&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Always pair a data-platform design with the query or metric that &lt;em&gt;proves&lt;/em&gt; its SLA, and always narrate at least one failure mode with its blast radius and graceful-degradation path. A design you can't measure and can't defend against failure is a diagram; a design with an SLA check and a failure narrative is an architecture.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — reasoning about a one-way vs two-way door platform decision
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Staff/principal design is as much about &lt;em&gt;which decisions to sweat&lt;/em&gt; as about the decisions themselves. The one-way vs two-way door framing (from the disagree-and-commit toolkit) applies directly: spend design rigor on the irreversible choices and move fast on the reversible ones. Walk through classifying the major decisions in the platform design.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-way doors (sweat these).&lt;/strong&gt; Storage format/partitioning (rewriting history is expensive), the canonical schema/keys, the tenancy isolation model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-way doors (move fast).&lt;/strong&gt; Which BI tool, the exact streaming framework, retention windows (tunable), the number of medallion layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The interview signal.&lt;/strong&gt; Correctly classifying decisions by reversibility shows judgment about where to invest scarce consensus and design time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trap.&lt;/strong&gt; Over-designing a two-way door (endless debate on the BI tool) while under-designing a one-way door (picking a partition key casually) is the classic reversed-priority mistake.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Classify the platform's major decisions as one-way or two-way doors and state where design rigor should concentrate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Reversible?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Partition key / storage layout&lt;/td&gt;
&lt;td&gt;hard to reverse (rewrites history)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical schema + keys&lt;/td&gt;
&lt;td&gt;hard to reverse (all consumers depend)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tenancy isolation model&lt;/td&gt;
&lt;td&gt;hard to reverse (security boundary)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BI tool choice&lt;/td&gt;
&lt;td&gt;easy (swap the reader)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming framework&lt;/td&gt;
&lt;td&gt;medium (rewire the hot path)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention windows&lt;/td&gt;
&lt;td&gt;easy (config change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DOOR CLASSIFICATION — where to spend design rigor
=================================================
ONE-WAY DOORS  (invest consensus + design time; hard/expensive to undo)
  * Partition key + storage layout   -&amp;gt; rewrites history to change
  * Canonical schema + primary keys   -&amp;gt; every consumer couples to it
  * Multi-tenancy isolation model     -&amp;gt; security + blast-radius boundary
  RULE: RFC + broad review + ADR. Get these right the first time.

TWO-WAY DOORS (decide fast; revisit cheaply)
  * BI tool                           -&amp;gt; swap the reader, data unchanged
  * Streaming framework               -&amp;gt; rewire hot path, schema unchanged
  * Retention windows                 -&amp;gt; config change
  RULE: pick a reasonable default, timebox the debate, move on.

REVERSED-PRIORITY ANTI-PATTERN (the tell of weak judgment)
  Spending 3 meetings on the BI tool (two-way) while choosing the
  partition key (one-way) in a hallway conversation. Invert this.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Classify each decision by the &lt;em&gt;cost to reverse it&lt;/em&gt;, not by how much people want to argue about it. Storage layout and partition keys are one-way doors because changing them means rewriting history — expensive and risky at scale.&lt;/li&gt;
&lt;li&gt;The canonical schema and keys are one-way because every consumer couples to them; a breaking change ripples across the org. These deserve the full RFC + review + ADR treatment from section 3.&lt;/li&gt;
&lt;li&gt;The tenancy isolation model is one-way because it's a security and blast-radius boundary — getting it wrong later means a painful, risky migration under compliance pressure. Sweat it up front.&lt;/li&gt;
&lt;li&gt;The two-way doors (BI tool, streaming framework, retention) get a reasonable default and a timeboxed debate, because the cost of being wrong is a bounded, reversible swap. Spending scarce consensus here is waste.&lt;/li&gt;
&lt;li&gt;The reversed-priority anti-pattern — three meetings on the BI tool, a hallway decision on the partition key — is a direct tell of weak staff judgment. Naming and inverting it in a design round signals you know where rigor pays off.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Door&lt;/th&gt;
&lt;th&gt;Design investment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Partition key / layout&lt;/td&gt;
&lt;td&gt;one-way&lt;/td&gt;
&lt;td&gt;RFC + broad review + ADR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical schema + keys&lt;/td&gt;
&lt;td&gt;one-way&lt;/td&gt;
&lt;td&gt;RFC + broad review + ADR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tenancy isolation&lt;/td&gt;
&lt;td&gt;one-way&lt;/td&gt;
&lt;td&gt;RFC + security review + ADR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BI tool&lt;/td&gt;
&lt;td&gt;two-way&lt;/td&gt;
&lt;td&gt;default + timeboxed debate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming framework&lt;/td&gt;
&lt;td&gt;two-way&lt;/td&gt;
&lt;td&gt;default + timeboxed debate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention windows&lt;/td&gt;
&lt;td&gt;two-way&lt;/td&gt;
&lt;td&gt;config, revisit as needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Classify every major platform decision as a one-way or two-way door and concentrate your RFCs, reviews, and consensus-building on the one-way doors. Deciding reversible choices quickly and irreversible choices carefully is the essence of staff-level design judgment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on staff-altitude system design
&lt;/h3&gt;

&lt;p&gt;A staff interviewer might ask: "Design a multi-tenant data ingestion platform that hundreds of internal teams onboard sources to themselves. Cover the requirements you'd gather, the NFRs, the tenancy isolation model, the cost/latency/reliability trade-offs, at least two failure modes with their blast radius, and how you'd measure whether the platform meets its SLA." This is a full staff design prompt — deliberately broad, self-serve, multi-tenant — and it rewards altitude (requirements and trade-offs) over technology name-dropping.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a requirements-first, isolation-aware multi-tenant platform design
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MULTI-TENANT INGESTION PLATFORM — staff design answer
=====================================================

1. REQUIREMENTS (drive these; don't wait to be told)
   - Consumers: 100s of internal teams, self-serve onboarding.
   - Volume: mixed; some tenants 10 GB/day, some 10 TB/day (skew!).
   - Freshness: per-tenant SLA (sub-min to daily).
   - Non-goal: not a public/external product; not a query engine.

2. NFRs
   - Multi-tenancy ISOLATION is the headline NFR: one noisy/bad tenant
     must not degrade others (blast-radius containment).
   - Self-serve evolvability: onboard a source without platform-team work.
   - Cost attribution: per-tenant so heavy tenants pay their share.

3. TENANCY ISOLATION MODEL (a one-way door -&amp;gt; sweat it)
   - Logical isolation: per-tenant namespaces + resource quotas on the
     shared bus and compute (rate limits, partition quotas).
   - Bad-data isolation: per-tenant dead-letter queues; a poison record
     quarantines to the tenant's DLQ, never blocks the shared pipeline.
   - Noisy-neighbor: per-tenant throughput quotas + priority classes.

4. TRADE-OFFS (set the dial)
   - Shared infra (cost-efficient) vs per-tenant infra (isolated but $$$).
     Choose SHARED with hard quotas -&amp;gt; cost efficiency + bounded blast
     radius. Offer dedicated tier for the few 10-TB/day tenants.

5. SELF-SERVE ONBOARDING
   - Declarative source config (a contract file); CI validates schema +
     freshness budget; platform provisions the lane. No tickets to us.

6. FAILURE MODES + BLAST RADIUS
   - Poison record: quarantined to tenant DLQ. Blast radius = 1 tenant.
   - Hot partition (10-TB tenant skew): per-tenant quota throttles it;
     others unaffected. Blast radius = the offending tenant's lane.
   - Bus outage: durable buffering + backpressure; freshness degrades
     platform-wide but no data loss. Blast radius = freshness SLA, not data.

7. SLA MEASUREMENT
   - Per-tenant freshness query (see gold_sla_config pattern); BREACH
     pages the TENANT'S owner, not the platform team -&amp;gt; ownership at scale.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design decision&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;th&gt;Blast-radius / trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requirements + non-goals first&lt;/td&gt;
&lt;td&gt;scope the prompt before drawing&lt;/td&gt;
&lt;td&gt;avoids designing the wrong platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation = headline NFR&lt;/td&gt;
&lt;td&gt;multi-tenant means noisy neighbors&lt;/td&gt;
&lt;td&gt;contain failures to one tenant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared infra + hard quotas&lt;/td&gt;
&lt;td&gt;cost efficiency without shared fate&lt;/td&gt;
&lt;td&gt;dedicated tier for the few giants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-tenant DLQ&lt;/td&gt;
&lt;td&gt;poison record containment&lt;/td&gt;
&lt;td&gt;1 tenant, not the platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-tenant quotas&lt;/td&gt;
&lt;td&gt;skew/noisy-neighbor containment&lt;/td&gt;
&lt;td&gt;offending lane only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-tenant SLA + owner paging&lt;/td&gt;
&lt;td&gt;ownership scales, platform team doesn't&lt;/td&gt;
&lt;td&gt;tenant owns its freshness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The answer opens by &lt;em&gt;driving&lt;/em&gt; the requirements and stating non-goals, so the design targets the real problem (self-serve, multi-tenant, skewed volume) rather than a generic pipeline. It elevates isolation to the headline NFR because in a multi-tenant platform the dominant risk is shared fate, then chooses shared infrastructure with hard per-tenant quotas — the cost-efficient option that still contains blast radius — while offering a dedicated tier for the handful of giant tenants. Every failure mode is answered with its containment boundary (one tenant's DLQ, one tenant's lane, freshness-not-data), and the SLA is measured per-tenant with paging routed to the &lt;em&gt;tenant's&lt;/em&gt; owner, which is how platform ownership scales to hundreds of teams without the platform team becoming the bottleneck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;This answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Altitude&lt;/td&gt;
&lt;td&gt;"Kafka + Spark + Snowflake"&lt;/td&gt;
&lt;td&gt;requirements → NFRs → isolation → trade-offs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-tenancy&lt;/td&gt;
&lt;td&gt;ignored or hand-waved&lt;/td&gt;
&lt;td&gt;isolation as the headline NFR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade-offs&lt;/td&gt;
&lt;td&gt;maxes throughput&lt;/td&gt;
&lt;td&gt;shared + quotas, dedicated tier for giants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure modes&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;3 modes, each with a named blast radius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLA / ownership&lt;/td&gt;
&lt;td&gt;"we'd monitor it"&lt;/td&gt;
&lt;td&gt;per-tenant SLA, tenant-owner paging&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Requirements-first altitude&lt;/strong&gt;&lt;/strong&gt; — driving the requirements and non-goals before drawing boxes ensures the design solves the actual prompt (self-serve, multi-tenant, skewed) instead of a generic pipeline. The first five minutes on requirements is the clearest staff-vs-senior tell in a design round.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Isolation as the headline NFR&lt;/strong&gt;&lt;/strong&gt; — recognizing that multi-tenancy's dominant risk is shared fate, and elevating isolation above throughput, is the judgment that defines the design. Everything downstream (DLQs, quotas) flows from this one correctly-prioritized NFR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Shared-plus-quotas trade-off&lt;/strong&gt;&lt;/strong&gt; — choosing shared infrastructure with hard per-tenant quotas captures cost efficiency while bounding blast radius, and offering a dedicated tier for the few giant tenants sets the dial per-tenant instead of one-size-fits-all. It's an explicit, defended trade-off, not a default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Blast-radius-bounded failure modes&lt;/strong&gt;&lt;/strong&gt; — answering each failure with its containment boundary (one tenant, one lane, freshness-not-data) is the failure-narrative muscle that separates a staff design from a diagram. Naming the blast radius is more important than naming the tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — the design's ongoing cost is the quota/isolation machinery and per-tenant observability, plus a dedicated tier for giants; the payoff is that the platform team's operational load stays O(1) as tenants grow to O(hundreds), because ownership (onboarding, SLA, paging) is pushed to each tenant. A platform without pushed-down ownership scales its own team's toil linearly with adoption — the anti-pattern this design avoids.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Multi-tenant platform and data-system design problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL and ingestion-pipeline design problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Influence without authority, promo packets, and the staff interview loop
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Move the org without a manager title — glue work, force multiplication, and the promo case
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnloqt7ze4cdzee9k79o0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnloqt7ze4cdzee9k79o0.jpeg" alt="Iconographic influence and promo-packet diagram — a central engineer-node radiating force-multiplier arrows to mentee and team nodes, beside a promo-packet document with scope/impact/leadership sections and a staff interview-loop track of five stages." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;influence without authority is the defining staff/principal skill — producing org-level outcomes with zero positional power over the people whose behavior must change — and it is expressed through &lt;em&gt;force multiplication&lt;/em&gt; (mentoring, tooling, standards that make others faster), &lt;em&gt;glue work&lt;/em&gt; (the unglamorous coordination that holds cross-team efforts together), and the ability to &lt;em&gt;drive an initiative you don't own&lt;/em&gt;; the promo packet is simply the written, quantified evidence of this influence, and the staff interview loop is a structured attempt to verify it&lt;/strong&gt;. A manager influences through reporting lines; a staff engineer influences through being the person others &lt;em&gt;choose&lt;/em&gt; to align with because their judgment, writing, and reliability have earned it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sources of influence when you have no authority.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Earned technical credibility.&lt;/strong&gt; Being right, repeatedly and visibly, in high-stakes decisions. Credibility is the currency you spend on influence; you earn it by shipping and by good calls under ambiguity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing that others rally around.&lt;/strong&gt; The RFC, the strategy doc, the ADR. A document that clarifies a messy problem &lt;em&gt;becomes&lt;/em&gt; the org's plan, and its author becomes the de-facto leader of the effort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships and trust.&lt;/strong&gt; The 1:1s, the reliability, the reputation for making others successful rather than hoarding credit. Influence flows through trust, and trust compounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Making others successful.&lt;/strong&gt; The fastest way to gain influence is to make other people and teams win — mentoring, unblocking, amplifying their work. Influence is a byproduct of generosity, not extraction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Force multiplication — the mechanism of staff impact.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mentoring at scale.&lt;/strong&gt; Not just pairing with one junior — creating the guild, the onboarding path, the design-review culture that levels up many engineers. A force multiplier raises the whole team's ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooling and platforms.&lt;/strong&gt; The self-serve tool that removes you as a bottleneck (section 4) is force multiplication — every team it unblocks is leverage that persists without you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standards and patterns.&lt;/strong&gt; The playbook, the reference implementation, the "how we do X here." A pattern adopted by ten teams multiplies one person's judgment across the org.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The anti-pattern.&lt;/strong&gt; Being the indispensable hero who personally fixes everything is &lt;em&gt;negative&lt;/em&gt; force multiplication — it creates a bottleneck and a bus-factor-one risk. Staff engineers work themselves &lt;em&gt;out&lt;/em&gt; of being the bottleneck.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Glue work — the coordination that holds cross-team efforts together.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it is.&lt;/strong&gt; The unowned, often-invisible work of coordination: writing the doc, running the sync, chasing the dependency, noticing the gap between teams. It's what makes multi-team efforts actually ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The recognition trap.&lt;/strong&gt; Glue work is essential but under-credited, and disproportionately falls on some engineers. The staff move is to do the &lt;em&gt;high-leverage&lt;/em&gt; glue (driving the RFC, owning the cross-team plan) and to make it &lt;em&gt;visible and quantified&lt;/em&gt;, not to drown in low-leverage glue (endless status-chasing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Making it count.&lt;/strong&gt; Frame glue work as leadership in your scope narrative: "I owned the cross-team migration plan" is leadership; "I sent a lot of reminder messages" is toil. Same activity, different framing and different leverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to decline.&lt;/strong&gt; Part of staff judgment is declining low-leverage glue so you can do high-leverage glue. Saying no to organizing the offsite to say yes to driving the architecture is a leveling-appropriate choice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The promo packet and the staff interview loop.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The promo packet.&lt;/strong&gt; A written case that you &lt;em&gt;already operate&lt;/em&gt; at the target level: scope (the org-sized problems), impact (quantified multipliers), leadership (the RFCs, the mentoring), with peer and cross-team testimonials as evidence. It's the scope narratives (section 2) assembled into a leveling argument.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The staff interview loop.&lt;/strong&gt; Typically: a system-design round (altitude, section 4), a technical deep-dive (proving depth is still there), one or more cross-team/behavioral rounds (scope, influence, dissent), a leadership round, and a values round. The behavioral rounds carry more weight than in a senior loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staff-project selection.&lt;/strong&gt; The single most-cited promo blocker is the absence of a demonstrably staff-scoped project. Choosing (and creating) a project with org-level blast radius is a deliberate career move, not luck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The through-line.&lt;/strong&gt; Promo packet and interview answers draw from the same well: quantified, cross-team, multiplier stories with artifacts. Build the well once (the running brag doc), draw from it for both.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the promo-packet outline
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The promo packet is the written argument that you already operate at staff level; a well-structured one makes the committee's decision easy by mapping your evidence directly onto the leveling rubric. Walk through an outline that a committee (or a hiring loop) can grade against the rubric in minutes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summary.&lt;/strong&gt; The one-paragraph leveling claim with the headline multipliers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope + impact.&lt;/strong&gt; 2–3 org-sized efforts, each quantified in other teams' metrics, mapped to the rubric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leadership + influence.&lt;/strong&gt; The RFCs authored, the alignment driven, the engineers mentored — with artifacts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence.&lt;/strong&gt; Peer and cross-team testimonials, adoption dashboards, the ADRs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the promo-packet outline for a senior→staff data engineer case, structured so a committee can map it onto the leveling rubric.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Maps to rubric dimension&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Summary&lt;/td&gt;
&lt;td&gt;overall level claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope + impact&lt;/td&gt;
&lt;td&gt;scope, impact (multiplier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leadership&lt;/td&gt;
&lt;td&gt;leadership, influence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judgment&lt;/td&gt;
&lt;td&gt;judgment / taste&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;verification (testimonials, artifacts)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Promo case: Senior -&amp;gt; Staff Data Engineer — &amp;lt;name&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Summary (the claim)&lt;/span&gt;
Operating at staff scope for ~3 quarters: drove 2 org-level platform
efforts reclaiming ~5 engineer-years/yr across 9 teams, authored the
data-contract standard now org policy, and mentored 4 engineers to
own cross-team surfaces. Requesting Staff.

&lt;span class="gu"&gt;## 1. Scope &amp;amp; Impact (rubric: scope, multiplier impact)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Shared ingestion platform: 6 teams, onboarding 3wk-&amp;gt;2day,
  ~3.5 eng-yr/yr reclaimed. [scope narrative + adoption dashboard]
&lt;span class="p"&gt;-&lt;/span&gt; Data-contract standard: 9 teams, data incidents -55%,
  "bad number" tickets -70%. [RFC-014, ADR-014]
&lt;span class="p"&gt;-&lt;/span&gt; (Each maps to blast radius &amp;gt;= 3 teams, multiplier, durable.)

&lt;span class="gu"&gt;## 2. Leadership &amp;amp; Influence (rubric: leadership, no-authority influence)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Authored RFC-014 + 3 others; ran 11 cross-team design reviews.
&lt;span class="p"&gt;-&lt;/span&gt; Integrated dissent from Security + Search into shipped designs.
&lt;span class="p"&gt;-&lt;/span&gt; Mentored 4 engineers; 2 now drive their teams' contracts.
&lt;span class="p"&gt;-&lt;/span&gt; Established the design-review guild (force multiplier).

&lt;span class="gu"&gt;## 3. Judgment &amp;amp; Taste (rubric: judgment)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Chose NOT to build a bespoke streaming engine; standardized on the
  managed option -&amp;gt; saved ~1 eng-yr, avoided a maintenance trap.
&lt;span class="p"&gt;-&lt;/span&gt; Deprecated 2 legacy reconciliation jobs after the contract landed.

&lt;span class="gu"&gt;## 4. Evidence&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Testimonials: 5 (3 cross-team leads, 1 partner PM, 1 mentee).
&lt;span class="p"&gt;-&lt;/span&gt; Artifacts: RFC-014, ADR-014, adoption dashboards, guild charter.
&lt;span class="p"&gt;-&lt;/span&gt; Manager + skip-level endorsement.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Summary states the leveling claim in one paragraph with the headline multipliers up front, so a committee member forms the "this is a staff case" impression in the first ten seconds — the same lead-with-the-number discipline as the scope narrative.&lt;/li&gt;
&lt;li&gt;The Scope &amp;amp; Impact section presents 2–3 &lt;em&gt;distinct&lt;/em&gt; org-sized efforts (not one project stretched), each quantified in other teams' metrics and each explicitly clearing the rubric gates (blast radius ≥3, multiplier, durable). The portfolio breadth is what proves sustained staff operation.&lt;/li&gt;
&lt;li&gt;The Leadership &amp;amp; Influence section maps directly onto the no-authority-influence rubric dimension: RFCs authored, reviews run, dissent integrated, engineers mentored, a guild established. Each is a leadership &lt;em&gt;behavior&lt;/em&gt; with an artifact, not an adjective.&lt;/li&gt;
&lt;li&gt;The Judgment section supplies the hardest-to-fake signal: deliberate &lt;em&gt;non-building&lt;/em&gt; and deprecation decisions. "Chose not to build a bespoke engine" and "deprecated two legacy jobs" demonstrate taste, which is the dimension that most distinguishes staff+ from a productive senior.&lt;/li&gt;
&lt;li&gt;The Evidence section makes the whole case &lt;em&gt;verifiable&lt;/em&gt; — cross-team testimonials (not just the manager), artifacts, and skip-level endorsement. A promo case without external verification is self-assessment; with it, it's evidence the committee can trust.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Packet section&lt;/th&gt;
&lt;th&gt;Rubric dimension&lt;/th&gt;
&lt;th&gt;Strongest evidence type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Summary&lt;/td&gt;
&lt;td&gt;overall claim&lt;/td&gt;
&lt;td&gt;headline multipliers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope &amp;amp; impact&lt;/td&gt;
&lt;td&gt;scope + multiplier&lt;/td&gt;
&lt;td&gt;other-team metrics + artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leadership&lt;/td&gt;
&lt;td&gt;influence, no authority&lt;/td&gt;
&lt;td&gt;RFCs + integrated dissent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judgment&lt;/td&gt;
&lt;td&gt;taste&lt;/td&gt;
&lt;td&gt;deliberate non-building / deprecation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;verification&lt;/td&gt;
&lt;td&gt;cross-team testimonials&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Structure a promo packet to map one-to-one onto the leveling rubric, lead every section with a quantified multiplier, include at least one deliberate &lt;em&gt;non-building&lt;/em&gt; decision as judgment evidence, and back every claim with a cross-team testimonial or artifact. A committee grades against the rubric; make their job trivial by pre-mapping your evidence to it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the influence / glue-work leverage rubric
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Not all influence work is equal — some glue work is high-leverage leadership and some is low-leverage toil, and staff engineers deliberately choose the former and decline the latter. A leverage rubric for glue work helps you (and an interviewer) distinguish "drove the cross-team migration" from "sent status reminders." Walk through the rubric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Axis: leverage.&lt;/strong&gt; Does this glue work unblock many people (high) or is it one-off coordination (low)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Axis: visibility / creditability.&lt;/strong&gt; Is it framed as leadership with artifacts, or invisible toil?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Axis: durability.&lt;/strong&gt; Does it create a lasting mechanism (a process, a doc, an owner) or evaporate?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The decision.&lt;/strong&gt; Do the high-leverage, durable, visible glue; decline or delegate the low-leverage toil.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Score three glue-work activities on the leverage rubric and decide which to own, which to delegate, and which to decline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Glue activity&lt;/th&gt;
&lt;th&gt;Leverage&lt;/th&gt;
&lt;th&gt;Durability&lt;/th&gt;
&lt;th&gt;Visibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Drive the cross-team migration plan&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;high (a plan + owners)&lt;/td&gt;
&lt;td&gt;high (you author it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mentor 1 engineer per team to own contracts&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;high (durable owners)&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send weekly status-chase reminders&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GLUE-WORK LEVERAGE RUBRIC
=========================
                          leverage  durability  visibility  -&amp;gt; action
--------------------------------------------------------------------------
Drive migration plan        HIGH      HIGH        HIGH        OWN IT
  (the doc becomes the org's plan; you become the de-facto lead)

Mentor 1 owner/team         HIGH      HIGH        MED         OWN IT
  (creates durable cross-team owners; force multiplication)
  -&amp;gt; raise visibility: name the mentees' wins in your scope narrative.

Weekly status reminders     LOW       NONE        LOW         DELEGATE
  (necessary but not leadership; rotate it or automate a dashboard)

STAFF JUDGMENT
  Say YES to high-leverage glue (it IS the leadership).
  Say NO / delegate low-leverage glue so it doesn't crowd out the
  high-leverage work. Doing all glue indiscriminately is a career trap:
  it's essential, under-credited, and infinite.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Score each activity on leverage first — how many people it unblocks. Driving the migration plan and mentoring per-team owners are high-leverage; status-chasing is low-leverage even though it feels urgent.&lt;/li&gt;
&lt;li&gt;Driving the migration plan scores high on all three axes: it unblocks everyone, it's durable (a plan with owners), and it's visible (you author it and become the de-facto lead). This is glue work that &lt;em&gt;is&lt;/em&gt; leadership — own it unconditionally.&lt;/li&gt;
&lt;li&gt;Mentoring one owner per team is high-leverage and durable (it creates lasting cross-team owners — force multiplication) but only medium-visibility, so the action includes &lt;em&gt;raising its visibility&lt;/em&gt; by naming the mentees' wins in your scope narrative. Otherwise this leadership work goes uncredited.&lt;/li&gt;
&lt;li&gt;Weekly status reminders are necessary but low on every axis — they don't scale your judgment, create nothing durable, and read as toil. The action is delegate or automate (a dashboard), freeing your time for the high-leverage glue.&lt;/li&gt;
&lt;li&gt;The staff judgment is the meta-point: glue work is essential, under-credited, and &lt;em&gt;infinite&lt;/em&gt;, so doing all of it indiscriminately is a career trap. Choosing the high-leverage glue and declining the rest is itself a leveling-appropriate act of judgment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Glue activity&lt;/th&gt;
&lt;th&gt;Rubric verdict&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Drive migration plan&lt;/td&gt;
&lt;td&gt;high/high/high&lt;/td&gt;
&lt;td&gt;own it (it's leadership)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mentor per-team owners&lt;/td&gt;
&lt;td&gt;high/high/med&lt;/td&gt;
&lt;td&gt;own it + raise visibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status reminders&lt;/td&gt;
&lt;td&gt;low/none/low&lt;/td&gt;
&lt;td&gt;delegate or automate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Triage glue work on leverage, durability, and visibility before agreeing to it. Own the high-leverage, durable glue and frame it as the leadership it is; delegate, automate, or decline the low-leverage toil. Indiscriminately absorbing all glue work is the most common way strong engineers stall below staff.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — a "drive an initiative you don't own" script
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The purest test of influence without authority is driving an initiative that isn't yours — one that crosses teams, has no clear owner, and requires people who don't report to you to change what they do. Walk through the script a staff engineer uses to take an unowned org problem and move it to done.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claim the problem, not the authority.&lt;/strong&gt; Write the problem down clearly; becoming the person who articulated it makes you its de-facto owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the coalition.&lt;/strong&gt; Recruit the affected teams by connecting the initiative to &lt;em&gt;their&lt;/em&gt; goals, not yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create the plan and the artifacts.&lt;/strong&gt; The RFC, the roadmap, the owners — make the effort legible and trackable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sustain momentum.&lt;/strong&gt; Run the cadence, unblock, and keep it visible to leadership so it doesn't quietly die.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the script for driving an unowned, cross-team initiative (e.g., "our data lineage is a black box") to completion without authority.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frame&lt;/td&gt;
&lt;td&gt;write the problem; become its articulator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coalition&lt;/td&gt;
&lt;td&gt;recruit teams via their goals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan&lt;/td&gt;
&lt;td&gt;RFC + roadmap + named owners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Momentum&lt;/td&gt;
&lt;td&gt;cadence + leadership visibility&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DRIVE-WHAT-YOU-DON'T-OWN — script
=================================

1. FRAME THE PROBLEM (claim it by articulating it)
   Write a 1-page problem statement: "No one can answer 'where did this
   number come from?' Lineage gaps cause X incidents/quarter and Y hours
   of debugging across 4 teams." Circulate it. You are now the owner in
   everyone's mind, without a title.

2. BUILD THE COALITION (their goals, not yours)
   - Data Platform: "lineage reduces your on-call load."
   - Analytics: "lineage lets you trust the numbers you present."
   - Each team gets a reason rooted in THEIR pain, not your initiative.
   - Secure an executive sponsor for air cover (informed, not deciding).

3. PLAN + ARTIFACTS (make it legible)
   - RFC for the lineage approach; ADR for the decision.
   - Roadmap with a named owner per milestone (NOT all you).
   - A visible dashboard: % of pipelines with lineage coverage.

4. SUSTAIN MOMENTUM (the glue that ships it)
   - Biweekly 30-min cross-team sync; you facilitate, others report.
   - Unblock actively; escalate blockers to the sponsor with a rec.
   - Monthly update to leadership -&amp;gt; keeps it funded and alive.

5. HAND OFF (durability = you're not the bottleneck)
   - Each team owns its lineage; you own the standard, not the work.
   - Success = the initiative runs without you in the room.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The initiative is claimed by &lt;em&gt;articulating&lt;/em&gt; it, not by being assigned it — writing the crisp, quantified problem statement makes you the person everyone associates with the problem, which is de-facto ownership without any title change.&lt;/li&gt;
&lt;li&gt;The coalition is built by connecting the initiative to each team's &lt;em&gt;own&lt;/em&gt; goals — lineage reduces Data Platform's on-call load, lets Analytics trust its numbers. Influence without authority works by making your problem the solution to their problem.&lt;/li&gt;
&lt;li&gt;An executive sponsor is recruited for &lt;em&gt;air cover&lt;/em&gt; (informed, not deciding) so the initiative has legitimacy and funding, but the day-to-day driving stays with you — this is the difference between borrowing authority and being handed it.&lt;/li&gt;
&lt;li&gt;The plan distributes ownership (a named owner per milestone, not all you) and makes progress &lt;em&gt;legible&lt;/em&gt; via a coverage dashboard, so the effort is trackable and the multiplier is visible. Distributed ownership is what makes it durable and prevents bus-factor-one.&lt;/li&gt;
&lt;li&gt;The final phase is the hand-off: success is explicitly defined as the initiative running &lt;em&gt;without you in the room&lt;/em&gt;. Working yourself out of the bottleneck is the force-multiplication signal and the durability signal simultaneously — the opposite of the indispensable-hero anti-pattern.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Influence mechanism&lt;/th&gt;
&lt;th&gt;Durability outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frame&lt;/td&gt;
&lt;td&gt;articulate → de-facto owner&lt;/td&gt;
&lt;td&gt;the problem has a face&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coalition&lt;/td&gt;
&lt;td&gt;their goals + sponsor air cover&lt;/td&gt;
&lt;td&gt;teams opt in willingly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan&lt;/td&gt;
&lt;td&gt;RFC + per-milestone owners&lt;/td&gt;
&lt;td&gt;distributed ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Momentum&lt;/td&gt;
&lt;td&gt;cadence + leadership visibility&lt;/td&gt;
&lt;td&gt;stays funded and alive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-off&lt;/td&gt;
&lt;td&gt;teams own it, you own the standard&lt;/td&gt;
&lt;td&gt;runs without you&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; To drive an initiative you don't own: claim it by writing the problem down, recruit each team through its own goals, secure a sponsor for air cover, distribute milestone ownership, and define success as the effort running without you. Influence without authority is engineered, not innate — it's the reproducible loop of articulate → coalition → plan → momentum → hand-off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on influence without authority
&lt;/h3&gt;

&lt;p&gt;A staff interviewer might ask: "Tell me about a time you drove an important initiative that wasn't yours — where you had no authority over the people you needed, and someone could reasonably have said 'that's not your job.' How did you get it done, and how did you handle the people who resisted?" This is the definitive influence-without-authority probe. The interviewer wants the mechanism (how you created movement without power) and the conflict (how you converted resistance into commitment), not a happy-path story where everyone cooperated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using the articulate-coalition-integrate-handoff influence loop
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFLUENCE-WITHOUT-AUTHORITY — worked behavioral answer
======================================================

SCOPE (the unowned, org-sized problem)
  "Data lineage was a black box. Four teams collectively lost ~Y
   hours/quarter debugging 'where did this number come from,' and no
   team owned lineage. It was explicitly no one's job — including mine."

ARTICULATE (claim by writing)
  "I wrote a 1-page problem statement quantifying the debugging cost and
   circulated it. Within a week I was the person people pinged about
   lineage, with no title change."

COALITION (their goals + air cover)
  "I pitched each team on its own pain: Platform's on-call load,
   Analytics' trust in numbers. I got a director as sponsor for air
   cover and monthly visibility."

THE RESISTANCE (the real test)
  "One team lead pushed back hard: 'this is scope creep, we don't have
   the cycles.' I steel-manned it — he was right that a heavy lineage
   tool would burden his team. So I integrated his constraint: lineage
   capture was auto-derived from existing dbt metadata, near-zero effort
   for his team. He went from blocker to advocate."

RESULT (multiplier + durability)
  "80% lineage coverage in 2 quarters across 4 teams; debugging time for
   'where did this come from' down ~65%. Each team owns its lineage; I
   own the standard. It runs without me now."

REFLECTION
  "The resistance improved the design. I now seek out the strongest
   objector early — their constraint is usually the one I missed."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;th&gt;Why it converts resistance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;quantify the unowned problem&lt;/td&gt;
&lt;td&gt;makes the case on merit, not mandate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Articulate&lt;/td&gt;
&lt;td&gt;write it → de-facto owner&lt;/td&gt;
&lt;td&gt;ownership without a title&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coalition&lt;/td&gt;
&lt;td&gt;pitch each team's own pain&lt;/td&gt;
&lt;td&gt;opt-in, not compliance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resistance&lt;/td&gt;
&lt;td&gt;steel-man + integrate the objection&lt;/td&gt;
&lt;td&gt;blocker → co-owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;coverage + durability&lt;/td&gt;
&lt;td&gt;multiplier that runs without you&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The answer leads with the &lt;em&gt;unowned, quantified&lt;/em&gt; problem — establishing that it was explicitly no one's job, which is what makes it an influence-without-authority story rather than an assigned-project story. It shows ownership being &lt;em&gt;claimed by articulation&lt;/em&gt;, the coalition being built on each team's own incentives, and — the crux — it does not dodge the resistance. The objecting lead is steel-manned, his real constraint (tooling burden) is discovered and &lt;em&gt;integrated&lt;/em&gt; (auto-derived lineage from existing dbt metadata), and that integration converts him from blocker to advocate. The result is a durable multiplier that runs without the candidate, and the reflection generalizes the lesson (seek the strongest objector early), signaling a self-improving operator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;This answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No-authority framing&lt;/td&gt;
&lt;td&gt;"I was asked to lead it"&lt;/td&gt;
&lt;td&gt;"it was explicitly no one's job"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mechanism of influence&lt;/td&gt;
&lt;td&gt;"I convinced everyone"&lt;/td&gt;
&lt;td&gt;articulate → coalition → air cover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handling resistance&lt;/td&gt;
&lt;td&gt;"eventually they agreed"&lt;/td&gt;
&lt;td&gt;steel-manned + integrated the constraint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflict → commitment&lt;/td&gt;
&lt;td&gt;glossed over&lt;/td&gt;
&lt;td&gt;blocker became an advocate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durability&lt;/td&gt;
&lt;td&gt;"we shipped it"&lt;/td&gt;
&lt;td&gt;runs without me; teams own it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Unowned-problem framing&lt;/strong&gt;&lt;/strong&gt; — establishing that the initiative was explicitly no one's job (including yours) is what makes it a genuine influence-without-authority story. An assigned project tests execution; an unowned one tests whether you can &lt;em&gt;manufacture&lt;/em&gt; org movement from nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Claim-by-articulation&lt;/strong&gt;&lt;/strong&gt; — writing and circulating the quantified problem statement is the mechanism by which authority-free ownership is created. The author of the clearest problem statement becomes the de-facto owner, every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Steel-man-and-integrate&lt;/strong&gt;&lt;/strong&gt; — the resistance is the graded part. Steel-manning the objector's concern, discovering the real constraint (tooling burden), and integrating a fix (auto-derived lineage) converts a blocker into an advocate. This is the single most persuasive thing you can show: conflict turned into commitment via design, not politics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Durability / hand-off&lt;/strong&gt;&lt;/strong&gt; — ending with "it runs without me; teams own it" proves force multiplication rather than heroics. A story where you remain the indispensable operator is a &lt;em&gt;negative&lt;/em&gt; staff signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — influence-without-authority is expensive in &lt;em&gt;relationship and writing time&lt;/em&gt; — the 1:1s, the problem statement, the coalition-building, the steel-manning — all before any technical work. But it's the only mechanism that scales impact past your own two hands, and the coalition and standard it builds are O(1) to maintain once established, versus the O(N) cost of personally coordinating every cross-team effort forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Cross-team leadership and design-review problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems that keep your depth interview-sharp&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — staff and principal interview recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leveling in one line.&lt;/strong&gt; Senior owns a &lt;em&gt;system&lt;/em&gt;, staff owns a &lt;em&gt;problem space across teams&lt;/em&gt;, principal owns a &lt;em&gt;technical strategy&lt;/em&gt;. The senior→staff boundary is the "who else has this problem?" reflex; authority stays flat while scope grows. Aim each interview story at the level's unit of work — a solo, single-team story is senior evidence even when technically brilliant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The four graded signals.&lt;/strong&gt; Every staff loop scores Scope (org-sized vs task-sized), Impact (multiplier vs additive), Leadership (drove vs executed), and Judgment (owned the trade-off vs froze/hand-waved). Prepare at least one story that scores high on all four, and make sure your evidence spans 2–3 efforts, not one heroic project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additive vs multiplier — the reframe.&lt;/strong&gt; Additive impact is &lt;em&gt;your&lt;/em&gt; output ("I wrote 40 models," "cut my job 40%"); multiplier impact makes &lt;em&gt;others&lt;/em&gt; faster ("12 teams onboard in a day"). Never state a technical win without the two extra sentences: who else it unblocked, and whether the technique spread. Multiplier framing raises the identical work a full level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope-sizing gates.&lt;/strong&gt; For staff evidence, an effort must pass all three: multiplier (not additive) AND blast radius ≥3 teams AND durability high (value persists without you). Score each story before the loop; lead behavioral rounds with multiplier stories, hold additive-but-deep stories for the deep-dive round.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope narrative template.&lt;/strong&gt; One page per major effort: header with the headline multiplier, org-sized problem framing, drove-vs-did role, outcome in &lt;em&gt;other teams'&lt;/em&gt; metrics plus the counterfactual, and links to artifacts (RFC, adoption dashboard, testimonials). Update it the week the outcome lands, while the numbers are fresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defensible impact numbers.&lt;/strong&gt; Show your work (per-team cost × teams − platform cost), anchor to a counterfactual (the bespoke systems never built), volunteer the error bars (±25%, extrapolated) before being asked, and restate the conclusion at the pessimistic end. A derived, falsifiable number reads as true; a round, un-derived one invites a takedown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RFC hygiene.&lt;/strong&gt; Drive decisions in writing: problem → goals/&lt;strong&gt;non-goals&lt;/strong&gt; → options (with honest cons for &lt;em&gt;every&lt;/em&gt; option, including yours) → recommendation → trade-offs → rollout/rollback → open questions. Non-goals and rejected-alternatives are mandatory — they stop scope creep and re-litigation. Classify each sub-decision as a one-way or two-way door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design-review facilitation.&lt;/strong&gt; Frame the decision + door type + DRI in the first five minutes; spend the middle only on unresolved dissent (steel-manned and integrated live); close with an explicit decision, disagree-and-commit, &lt;em&gt;recorded&lt;/em&gt; dissent, and dated owners. A review that ends without a decision and an owner was a status meeting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-way vs two-way doors.&lt;/strong&gt; Sweat the irreversible decisions (partition key/storage layout, canonical schema/keys, tenancy isolation) with full RFC + review + ADR; move fast on the reversible ones (BI tool, streaming framework, retention). The reversed-priority anti-pattern — three meetings on the BI tool, a hallway call on the partition key — is the tell of weak judgment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System-design altitude.&lt;/strong&gt; Requirements first, NFRs second, design third, failure narratives last (and most important). Draw the NFR trade-off matrix before the architecture; set the cost/latency/reliability dial &lt;em&gt;per consumer&lt;/em&gt; from the SLA; name at least one failure mode with its &lt;strong&gt;blast radius&lt;/strong&gt; and graceful-degradation path; and pair every design with the query/metric that proves its SLA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force multiplication vs heroics.&lt;/strong&gt; Mentoring, tooling, and standards that make others faster are positive leverage; being the indispensable hero who personally fixes everything is &lt;em&gt;negative&lt;/em&gt; leverage (bottleneck + bus-factor-one). Staff engineers work themselves &lt;em&gt;out&lt;/em&gt; of being the bottleneck; "it runs without me" is the durability signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glue-work triage.&lt;/strong&gt; Triage glue work on leverage × durability × visibility before agreeing to it. Own the high-leverage, durable glue (drive the RFC, own the cross-team plan) and frame it as leadership; delegate or automate the low-leverage toil (status-chasing). Indiscriminately absorbing all glue is the most common sub-staff stall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promo packet + staff loop.&lt;/strong&gt; Structure the packet to map one-to-one onto the leveling rubric (summary, scope+impact, leadership, judgment, evidence), lead each section with a quantified multiplier, include at least one deliberate &lt;em&gt;non-building&lt;/em&gt; decision as judgment evidence, and back every claim with a cross-team testimonial or artifact. The loop weights behavioral/cross-team rounds more than a senior loop; the promo packet and interview answers draw from the same running brag doc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a staff data engineer?
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;staff data engineer&lt;/strong&gt; is a senior individual-contributor role — typically one level above senior on the technical (non-management) ladder — where the unit of impact shifts from a system you own to a problem space you make an entire org better at. A staff data engineer still writes code and designs systems, but the defining work is &lt;em&gt;leverage&lt;/em&gt;: building the shared platform many teams reuse, authoring the standard the org adopts, and driving cross-team architecture decisions without managing anyone. The role is measured in &lt;strong&gt;scope and impact&lt;/strong&gt; (how many teams and future decisions your work touches) rather than in throughput, and its signature skill is producing org-level outcomes with only team-level authority. In the &lt;code&gt;staff data engineer interview&lt;/code&gt;, this is why the questions probe scope selection, cross-team leverage, and judgment far more than they probe your ability to write correct pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Staff vs principal data engineer — what's the difference?
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;staff vs principal&lt;/strong&gt; distinction is another change in the unit of ownership. A staff data engineer owns a &lt;em&gt;problem space across teams&lt;/em&gt; — a platform, a standard, a migration — and makes many teams faster. A &lt;strong&gt;principal data engineer&lt;/strong&gt; owns &lt;em&gt;technical strategy&lt;/em&gt;: which architectures the org standardizes on for the next several years, which bets it makes, which systems it sunsets, and which locally-good ideas to say "no" to in service of a coherent direction. Put simply, staff is measured in cross-team leverage and principal is measured in org- or company-level judgment and taste. Both are individual-contributor roles with little to no positional authority, so both depend on influence without authority; the difference is that a staff engineer drives &lt;em&gt;decisions&lt;/em&gt; while a principal shapes the &lt;em&gt;space of decisions&lt;/em&gt; the org even considers.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is the staff interview different from the senior interview?
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;senior data engineer interview questions&lt;/strong&gt; center on whether you can build correct, performant, well-designed systems: model this warehouse, tune this query, design this pipeline, debug this failure. The &lt;code&gt;staff engineer interview&lt;/code&gt; assumes all of that and instead grades scope, cross-team impact, leadership, and judgment. Concretely: the system-design round rises in altitude (requirements and trade-offs and failure narratives over technology names), the behavioral rounds carry more weight and probe influence-without-authority and dissent-handling, and there is usually a dedicated cross-team or leadership round with no analog in a senior loop. The depth rounds don't disappear — you still have to prove you can code and design — but they become a &lt;em&gt;gate&lt;/em&gt; rather than the &lt;em&gt;decision&lt;/em&gt;. The decision is made in the rounds that test whether you operate at org scope.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I show impact without a title or authority?
&lt;/h3&gt;

&lt;p&gt;You show impact without authority through &lt;strong&gt;technical leadership&lt;/strong&gt; expressed in artifacts and outcomes rather than positional power. Author the RFC that the org rallies around — the person who writes the clearest problem statement and proposal becomes the de-facto owner, no title required. Build the coalition by connecting your initiative to each team's &lt;em&gt;own&lt;/em&gt; goals so they opt in rather than comply, and secure an executive sponsor for air cover. Handle resistance by steel-manning objections and integrating the real constraint behind them, which converts blockers into co-owners. Then quantify the result in &lt;em&gt;other teams'&lt;/em&gt; metrics (their onboarding time, their reduced incidents) and make it durable by distributing ownership so the effort runs without you. Influence without authority is a reproducible loop — articulate, coalition, integrate, hand off — not an innate trait.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do staff data engineers still code?
&lt;/h3&gt;

&lt;p&gt;Yes — staff and even principal data engineers still write code, and the depth rounds of the interview verify it. What changes is the &lt;em&gt;ratio&lt;/em&gt; and the &lt;em&gt;purpose&lt;/em&gt;. A staff engineer codes less of the routine feature work and more of the high-leverage kind: the reference implementation of a platform, the tricky proof-of-concept that de-risks an architecture decision, the tooling that removes a bottleneck. The failure mode is a staff engineer who has stopped coding entirely and become a pure coordinator — that person loses the technical credibility that their influence depends on. So the honest answer is that staff DEs code &lt;em&gt;strategically&lt;/em&gt;: enough to stay credible and to build the leveraged artifacts, but not so much that they become the indispensable bottleneck the role exists to eliminate. The interview reflects this by keeping a real deep-dive/coding round alongside the scope and leadership rounds.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I prepare for the staff interview loop?
&lt;/h3&gt;

&lt;p&gt;Prepare on two tracks. First, keep the depth sharp — the staff loop still has a system-design round and a technical deep-dive, so drill design, SQL, and platform fundamentals so the gate rounds are non-events. Second, and more decisively, build a portfolio of quantified, cross-team, multiplier stories: maintain a running "brag doc" of scope narratives (org-sized problem, drove-vs-did, other-teams' metrics, artifacts), score each against the four graded signals and the scope-sizing gates, and rehearse them in the STAR-for-scope structure that weights scope and multiplier over the code you wrote. Prepare the specific staff scenarios — align three teams on a schema, drive an initiative you don't own, design a multi-tenant platform — because they recur. Finally, prepare to &lt;em&gt;reason about levels&lt;/em&gt; out loud: being able to say why a given piece of work is staff-level rather than senior-level is itself the metacognition the loop is testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt; for the system-design and cross-team architecture rounds that anchor every staff and principal loop.&lt;/li&gt;
&lt;li&gt;Keep your depth interview-sharp on the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; so the deep-dive gate rounds are non-events while you focus on scope and leadership.&lt;/li&gt;
&lt;li&gt;Rehearse the platform muscle on the &lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;streaming practice library →&lt;/a&gt; for the real-time, multi-tenant, and event-schema scenarios staff designs are built on.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to keep your fundamentals graded and sharp while you build the cross-team, multiplier stories the staff loop actually decides on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Level up from senior to staff&lt;/h3&gt;

&lt;p&gt;Docs describe the staff bar; PipeCode drills the skills the staff loop actually gates — the system-design altitude, the SQL and platform depth that has to be a non-event, and the trade-off judgment behind every cross-team decision. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — trade-off-first practice tuned for the scope, impact, and architecture-leadership questions senior and staff data engineers actually face.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice design problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice streaming problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Chaos Engineering for Data Pipelines: Fault Injection with LitmusChaos &amp; Gremlin</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 31 Jul 2026 14:43:48 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/chaos-engineering-for-data-pipelines-fault-injection-with-litmuschaos-gremlin-336n</link>
      <guid>https://dev.to/gowthampotureddi/chaos-engineering-for-data-pipelines-fault-injection-with-litmuschaos-gremlin-336n</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;chaos engineering data&lt;/code&gt;&lt;/strong&gt; is the discipline of deliberately injecting failures — pod kills, network partitions, broker crashes, disk fills — into a &lt;em&gt;running&lt;/em&gt; data pipeline to prove the pipeline stays inside its freshness, correctness, and lag budgets when the underlying infrastructure misbehaves. It is not a stunt, it is not load testing, and it is not "let's yank a cable and see what happens." It is a hypothesis-driven engineering practice with a written steady state, a stated hypothesis, a blast-radius-controlled experiment, and a verification step that either confirms the pipeline's resilience or produces a concrete backlog item to fix it — and in 2026 it has moved from "SRE toolbox for stateless microservices" to a table-stakes practice for any data platform team that owns a real freshness SLO.&lt;/p&gt;

&lt;p&gt;This guide is the senior-data-platform-engineer walkthrough you wished existed the first time you had to answer "how do we prove our Kafka + Airflow + Spark stack survives a broker crash without our warehouse silently losing an hour of events?" It walks through why &lt;code&gt;chaos engineering pipelines&lt;/code&gt; is qualitatively different from chaos on stateless services (state, ordering, exactly-once contracts change everything), how &lt;code&gt;litmuschaos&lt;/code&gt; on Kubernetes uses a &lt;code&gt;ChaosEngine&lt;/code&gt; custom resource + probe guardrails to run pod-delete / pod-network-loss / disk-fill experiments with auto-abort, how &lt;code&gt;gremlin&lt;/code&gt; layers scenarios + a blast-radius dial + halt-on-breach health checks on top of raw attack primitives, and how to compose &lt;code&gt;data fault injection&lt;/code&gt; — &lt;code&gt;kafka chaos&lt;/code&gt; (broker kill + ISR shrink), &lt;code&gt;airflow chaos&lt;/code&gt; (scheduler kill mid-DAG), &lt;code&gt;spark chaos&lt;/code&gt; (executor loss) — into a repeatable game-day loop. Every section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp2gyunqw7jgypqeplig7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp2gyunqw7jgypqeplig7.jpeg" alt="PipeCode blog header for Chaos Engineering for Data Pipelines — bold white headline 'CHAOS ENGINEERING' over a hero composition of four small glyph medallions (pod-kill lightning, network-partition scissors, node-crash bolt, blast-radius circle) arranged on a wheel around a central purple 'CHAOS' seal, on a dark gradient." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;streaming practice library →&lt;/a&gt;, rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt;, and sharpen the architecture axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why chaos engineering is different for data pipelines&lt;/li&gt;
&lt;li&gt;LitmusChaos on Kubernetes — pod / network / node kill experiments&lt;/li&gt;
&lt;li&gt;Gremlin — scenarios, blast-radius controls, workflows&lt;/li&gt;
&lt;li&gt;Data-plane fault injection — Kafka broker kill, Airflow scheduler kill, Spark executor loss&lt;/li&gt;
&lt;li&gt;Hypothesis-driven chaos loop — steady state → hypothesis → blast → verify&lt;/li&gt;
&lt;li&gt;Cheat sheet — chaos engineering data recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why chaos engineering is different for data pipelines
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stateful streams, exactly-once contracts, and freshness SLOs — data chaos plays by different rules
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;&lt;code&gt;chaos engineering data&lt;/code&gt; is the practice of deliberately injecting infrastructure failures into a running data pipeline while a codified data SLO (freshness, lag, correctness) acts as the steady state that decides whether the experiment passes or halts — and every design choice in the experiment (blast radius, probe cadence, rollback path) has to account for the fact that streams are ordered, tables are stateful, and downstream consumers hold exactly-once contracts that a naive chaos experiment can violate&lt;/strong&gt;. A chaos experiment against a stateless HTTP microservice that returns "hello world" is easy: kill a pod, watch p99 latency, done. A chaos experiment against a Kafka broker that owns 300 partition leaders with &lt;code&gt;min.insync.replicas=2&lt;/code&gt; is a different physics problem — one wrong move and you have unshipped events sitting in producer buffers, ISR shrinks that trigger leader-election storms, or a downstream Spark job that fails mid-shuffle and re-reads gigabytes it already processed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four "must-answer" axes for a data chaos experiment.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State.&lt;/strong&gt; Where does the pipeline hold state, and what happens if that state is unavailable for N seconds? Kafka partitions have leader + ISR replicas, Airflow schedulers own the metadata DB, Spark drivers own the DAG lineage, Iceberg tables own atomic commits. Every one of these is a distinct state-loss failure mode with distinct recovery semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ordering.&lt;/strong&gt; Does the pipeline require in-order processing, and does the chaos experiment preserve or violate that? A network partition between two Kafka brokers can cause events to arrive at consumers out of order relative to source-time. A Spark executor loss forces stage retry, which re-emits shuffle blocks that downstream sinks may have already committed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exactly-once.&lt;/strong&gt; Does the chaos experiment risk breaking the exactly-once contract? Kafka + idempotent producers + transactional consumers hold EOS via a coordinator; if you kill the coordinator broker at exactly the wrong moment during a commit, some records commit twice on retry. The chaos experiment must either prove EOS holds or clearly report where it broke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness SLO.&lt;/strong&gt; Is the steady-state SLI a &lt;em&gt;data&lt;/em&gt; metric (freshness p99, end-to-end lag, DAG success rate, tombstone count) rather than an &lt;em&gt;infra&lt;/em&gt; metric (CPU, memory, pod-ready count)? Data chaos experiments verify against data SLIs; infra chaos experiments verify against infra SLIs. Confuse the two and you'll ship "the pod restarted but the pipeline is fine" when actually the pipeline is 40 minutes behind.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The 2026 reality — chaos has crossed from stateless SRE into data platform SRE.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LitmusChaos&lt;/strong&gt; is the CNCF sandbox → incubating open-source chaos framework that runs entirely on Kubernetes. It exposes chaos experiments as &lt;code&gt;ChaosEngine&lt;/code&gt; custom resources with a library of ready-made faults (&lt;code&gt;pod-delete&lt;/code&gt;, &lt;code&gt;pod-network-loss&lt;/code&gt;, &lt;code&gt;node-taint&lt;/code&gt;, &lt;code&gt;disk-fill&lt;/code&gt;, &lt;code&gt;pod-cpu-hog&lt;/code&gt;, &lt;code&gt;pod-memory-hog&lt;/code&gt;) plus a probe system that lets you attach &lt;code&gt;httpProbe&lt;/code&gt;, &lt;code&gt;promProbe&lt;/code&gt;, &lt;code&gt;cmdProbe&lt;/code&gt;, &lt;code&gt;k8sProbe&lt;/code&gt; guardrails that auto-abort on breach. It is the default choice when your data plane runs on Kubernetes and you want everything GitOps-managed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gremlin&lt;/strong&gt; is the commercial SaaS chaos platform with a control plane in the cloud and lightweight agents (&lt;code&gt;gremlind&lt;/code&gt;) on every target host, VM, container, or Kubernetes node. It exposes attack primitives (CPU, memory, disk, IO, latency, packet-loss, blackhole, DNS, shutdown) that stitch into "Scenarios" — multi-step attack workflows with explicit blast-radius sliders and halt-on-breach Health Checks tied to Datadog / New Relic / Prometheus. It is the choice when your data plane spans VMs, on-prem, cloud, and containers, and you want a single control plane with SOC-2-ready audit logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka / Airflow / Spark chaos&lt;/strong&gt; is the "data-plane" layer where you inject faults specific to the stateful data systems — broker kills, ISR shrinks, scheduler restarts, executor loss, shuffle-fetch failures. These are typically composed on top of LitmusChaos or Gremlin: the tool provides the primitive; you provide the guardrails that catch data-SLO breaches specific to your stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Game days&lt;/strong&gt; are the human ceremony that wraps all of the above — a scheduled hour where the team runs a chaos experiment against a production-like environment (or a small % of prod), watches the SLI dashboards live, halts on breach, and writes a post-mortem doc that lands as a backlog item. This is the pattern that turns chaos from an SRE side project into a resilience-forcing function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you name &lt;strong&gt;steady state, hypothesis, experiment, verify&lt;/strong&gt; without prompting? — senior signal; this is the Netflix / PoP chaos framework in one line.&lt;/li&gt;
&lt;li&gt;Do you say &lt;strong&gt;"my steady state is a data SLO, not CPU"&lt;/strong&gt; — freshness_p99, e2e lag, DAG success rate — rather than infrastructure metrics? — required answer.&lt;/li&gt;
&lt;li&gt;Do you name &lt;strong&gt;blast radius&lt;/strong&gt; as the first control and explain the dev → staging → 1% prod → 10% prod graduation? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you distinguish &lt;strong&gt;LitmusChaos (K8s-native, open source, GitOps) vs Gremlin (SaaS, cross-platform, commercial)&lt;/strong&gt; rather than treating them as interchangeable? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name a &lt;strong&gt;halt condition&lt;/strong&gt; — the automatic abort that fires when the steady-state SLI breaches — rather than describing chaos as an open-loop attack? — required answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the four-axis data chaos comparison table
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful artifact for a chaos-engineering interview is a memorised 4×N comparison table that maps each candidate chaos experiment against the four axes: state loss, ordering impact, exactly-once impact, freshness SLO impact. Every senior chaos discussion converges on this table within the first ten minutes; having it in your head is what separates a fluent answer from a stumbling one. Walk through building the table for a hypothetical stack that has Kafka → Spark Structured Streaming → Iceberg → dbt → Snowflake.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack under test.&lt;/strong&gt; Kafka (MSK, 6 brokers) → Spark Structured Streaming on EKS → Iceberg on S3 → dbt scheduled by Airflow → Snowflake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steady-state SLIs.&lt;/strong&gt; end-to-end freshness p99 &amp;lt; 5 min from producer to Iceberg; DAG success rate &amp;gt; 99%; Kafka consumer group lag &amp;lt; 10k messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Candidate experiments.&lt;/strong&gt; pod-delete on a broker, pod-network-loss on the Spark driver, disk-fill on the Airflow scheduler pod, blackhole to S3 for 60 s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Question at hand.&lt;/strong&gt; For each experiment, which axes are at risk and what is the halt condition?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the four-axis chaos comparison table and derive the halt condition for each experiment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Experiment&lt;/th&gt;
&lt;th&gt;State loss&lt;/th&gt;
&lt;th&gt;Ordering risk&lt;/th&gt;
&lt;th&gt;EOS risk&lt;/th&gt;
&lt;th&gt;Freshness risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pod-delete on Kafka broker&lt;/td&gt;
&lt;td&gt;leader for ~30 partitions&lt;/td&gt;
&lt;td&gt;in-partition order preserved; cross-partition unaffected&lt;/td&gt;
&lt;td&gt;low if &lt;code&gt;acks=all&lt;/code&gt; + &lt;code&gt;min.insync.replicas=2&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;~10-30 s bump in lag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pod-network-loss on Spark driver&lt;/td&gt;
&lt;td&gt;driver metadata unreachable&lt;/td&gt;
&lt;td&gt;streaming batch stuck; no re-order&lt;/td&gt;
&lt;td&gt;medium — checkpoint may skip micro-batch&lt;/td&gt;
&lt;td&gt;freshness p99 grows unboundedly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;disk-fill on Airflow scheduler&lt;/td&gt;
&lt;td&gt;scheduler pod OOMs on next heartbeat&lt;/td&gt;
&lt;td&gt;irrelevant (DAGs are not ordered)&lt;/td&gt;
&lt;td&gt;irrelevant&lt;/td&gt;
&lt;td&gt;DAG start delayed; downstream freshness slips&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blackhole to S3 (60 s)&lt;/td&gt;
&lt;td&gt;Iceberg writes fail; retries&lt;/td&gt;
&lt;td&gt;retries preserve order per Iceberg commit&lt;/td&gt;
&lt;td&gt;high — commit + manifest write are atomic&lt;/td&gt;
&lt;td&gt;freshness stalls for ~60 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ChaosEngine — pod-delete on Kafka broker with promProbe halt condition&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosEngine&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka-broker-chaos&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engineState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active'&lt;/span&gt;
  &lt;span class="na"&gt;appinfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;appns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kafka'&lt;/span&gt;
    &lt;span class="na"&gt;applabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=kafka-broker'&lt;/span&gt;
    &lt;span class="na"&gt;appkind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;statefulset'&lt;/span&gt;
  &lt;span class="na"&gt;chaosServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;experiments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;60'&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CHAOS_INTERVAL&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;30'&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FORCE&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;consumer-lag-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
              &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
              &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
              &lt;span class="na"&gt;probePollingInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
              &lt;span class="na"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max(kafka_consumergroup_lag{group="warehouse-sink"})'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;int&lt;/span&gt;
                &lt;span class="na"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;
                &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;50000'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;ChaosEngine&lt;/code&gt; custom resource declares a chaos experiment scoped to &lt;code&gt;applabel: app=kafka-broker&lt;/code&gt; in the &lt;code&gt;kafka&lt;/code&gt; namespace. LitmusChaos discovers the matching pods and runs the referenced experiment (&lt;code&gt;pod-delete&lt;/code&gt;) against a randomly-chosen one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TOTAL_CHAOS_DURATION=60&lt;/code&gt; bounds the chaos window to 60 s; &lt;code&gt;CHAOS_INTERVAL=30&lt;/code&gt; means the pod is deleted every 30 s within that window (so the broker will be killed once, then again 30 s later). &lt;code&gt;FORCE=false&lt;/code&gt; means Kubernetes performs a graceful shutdown (SIGTERM → configured grace period → SIGKILL) rather than an immediate SIGKILL, so the broker can drain in-flight requests.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;promProbe&lt;/code&gt; attaches a Prometheus query as a &lt;em&gt;continuous&lt;/em&gt; guardrail. Every 10 s during the chaos window, LitmusChaos runs &lt;code&gt;max(kafka_consumergroup_lag{group="warehouse-sink"})&lt;/code&gt; against Prometheus. If the returned value is not &lt;code&gt;&amp;lt; 50000&lt;/code&gt;, &lt;code&gt;stopOnFailure=true&lt;/code&gt; causes LitmusChaos to auto-abort the experiment and mark it Failed.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt; 50000&lt;/code&gt; threshold is the &lt;em&gt;halt condition&lt;/em&gt; — the point at which we've decided the experiment has produced too much data-SLO damage to continue. This is where infrastructure chaos and data chaos diverge: the halt condition is a data metric (consumer lag), not &lt;code&gt;pod_ready&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When the experiment completes, LitmusChaos writes a &lt;code&gt;ChaosResult&lt;/code&gt; CR with the verdict (&lt;code&gt;Pass&lt;/code&gt; if all probes passed, &lt;code&gt;Fail&lt;/code&gt; if any probe breached). The verdict + probe timeline is the artifact you attach to the GameDay doc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (s)&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Consumer lag&lt;/th&gt;
&lt;th&gt;Probe verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;broker-2 running&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;broker-2 killed (SIGTERM)&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;leader re-election underway&lt;/td&gt;
&lt;td&gt;1800&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;broker-2 restarted; ISR healing&lt;/td&gt;
&lt;td&gt;5200&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;broker-2 in ISR; lag draining&lt;/td&gt;
&lt;td&gt;3400&lt;/td&gt;
&lt;td&gt;Pass (final)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never write a data chaos experiment without a &lt;em&gt;continuous&lt;/em&gt; probe tied to a data SLI. The probe is the seatbelt; the SLI is what you're protecting; the halt condition is the crash sensor. Miss any one and you're doing chaos-as-a-stunt, not chaos engineering.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — steady state as a data SLO, not an infra metric
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The steady state is the single most under-specified concept in most first-attempt chaos programs. Teams reach for &lt;code&gt;pod_ready == true&lt;/code&gt; or &lt;code&gt;cpu_percent &amp;lt; 80&lt;/code&gt; because those metrics are easy to graph. But a Kafka broker can be "pod_ready" while its ISR is shrunk to a single replica and every consumer is 45 minutes behind — infra says green, data says on-fire. A senior data chaos program defines steady state as a &lt;em&gt;data SLI&lt;/em&gt; first, then adds infra SLIs as secondary indicators. Walk through the four canonical data SLIs and how to instrument each.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Freshness p99.&lt;/strong&gt; For any table or topic, the 99th percentile age of the newest committed record vs. wall clock time. The single most useful pipeline SLI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end lag.&lt;/strong&gt; Consumer group lag in messages, or &lt;code&gt;commit_ts - source_ts&lt;/code&gt; in a streaming SQL. Not the same as freshness (a lag of 0 with a stale source is not fresh).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DAG success rate.&lt;/strong&gt; For scheduled batch pipelines, &lt;code&gt;successful_runs / (successful_runs + failed_runs)&lt;/code&gt; over a rolling 24h window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correctness invariants.&lt;/strong&gt; Row-count parity vs source, no negative revenue values, foreign-key completeness — pipeline-specific business assertions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Define steady state as a data SLO for a Kafka → Iceberg pipeline and derive the halt conditions for a chaos experiment that kills a Spark executor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SLI&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Halt threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;freshness_p99_min&lt;/td&gt;
&lt;td&gt;max(now() - iceberg.commit_ts) p99&lt;/td&gt;
&lt;td&gt;&amp;lt; 5 min&lt;/td&gt;
&lt;td&gt;&amp;gt; 15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lag_messages&lt;/td&gt;
&lt;td&gt;sum(kafka_lag) across group&lt;/td&gt;
&lt;td&gt;&amp;lt; 10k&lt;/td&gt;
&lt;td&gt;&amp;gt; 100k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;batch_success_rate&lt;/td&gt;
&lt;td&gt;5-min rolling&lt;/td&gt;
&lt;td&gt;&amp;gt; 99%&lt;/td&gt;
&lt;td&gt;&amp;lt; 90% for 10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;row_count_delta&lt;/td&gt;
&lt;td&gt;source - iceberg over 1h&lt;/td&gt;
&lt;td&gt;&amp;lt; 100&lt;/td&gt;
&lt;td&gt;&amp;gt; 10000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Prometheus recording rules — one file, four data SLIs&lt;/span&gt;
&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline-slis&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline:freshness_p99_min&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;histogram_quantile(&lt;/span&gt;
            &lt;span class="s"&gt;0.99,&lt;/span&gt;
            &lt;span class="s"&gt;sum by (le) (&lt;/span&gt;
              &lt;span class="s"&gt;rate(iceberg_commit_age_seconds_bucket{table="events"}[5m])&lt;/span&gt;
            &lt;span class="s"&gt;)&lt;/span&gt;
          &lt;span class="s"&gt;) / 60&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline:lag_messages&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sum(kafka_consumergroup_lag{group="warehouse-sink"})&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline:batch_success_rate&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sum(rate(spark_streaming_batch_success_total[5m]))&lt;/span&gt;
          &lt;span class="s"&gt;/&lt;/span&gt;
          &lt;span class="s"&gt;sum(rate(spark_streaming_batch_total[5m]))&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline:row_count_delta&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;(source_events_total - iceberg_rows_total{table="events"})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ChaosEngine — Spark executor kill with all four SLIs as halt probes&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosEngine&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spark-executor-chaos&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spark&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engineState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active'&lt;/span&gt;
  &lt;span class="na"&gt;appinfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;appns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;spark'&lt;/span&gt;
    &lt;span class="na"&gt;applabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=spark-executor'&lt;/span&gt;
    &lt;span class="na"&gt;appkind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pod'&lt;/span&gt;
  &lt;span class="na"&gt;chaosServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;experiments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;300'&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PODS_AFFECTED_PERC&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10'&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;freshness-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:freshness_p99_min'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;15'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lag-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:lag_messages'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;100000'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch-success-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:batch_success_rate'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;float&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.90'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;correctness-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EOT&lt;/span&gt;   &lt;span class="c1"&gt;# end-of-test only&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:row_count_delta'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10000'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The four recording rules define the pipeline's steady state in Prometheus terms. Every one is a &lt;em&gt;data&lt;/em&gt; metric — freshness age, consumer lag, batch success ratio, source-vs-sink row delta. None of them mention &lt;code&gt;pod_ready&lt;/code&gt; or &lt;code&gt;cpu_percent&lt;/code&gt;; those become infra SLIs, tracked separately for triage but not gating the chaos verdict.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipeline:freshness_p99_min&lt;/code&gt; derives from a histogram of &lt;code&gt;iceberg_commit_age_seconds&lt;/code&gt; — the age (in seconds) of the newest committed Iceberg snapshot at scrape time. Dividing by 60 gives minutes. &lt;code&gt;histogram_quantile(0.99, ...)&lt;/code&gt; gives the p99, so short spikes don't panic the probe.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ChaosEngine&lt;/code&gt; attaches all four SLIs as probes. Three are &lt;code&gt;Continuous&lt;/code&gt; (evaluated every N seconds during chaos); one (&lt;code&gt;correctness-guard&lt;/code&gt;) is &lt;code&gt;EOT&lt;/code&gt; (end-of-test only) — you only compute row-count parity after the chaos window has closed and things have settled.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;PODS_AFFECTED_PERC=10&lt;/code&gt; sets the blast radius to 10% of matching pods. If you have 20 Spark executors, LitmusChaos kills 2. This is the "start small, graduate up" principle applied to a single experiment.&lt;/li&gt;
&lt;li&gt;When any continuous probe fails, LitmusChaos aborts the experiment. When the EOT probe fails, the experiment is marked failed even if nothing tripped during the run. The distinction matters: correctness bugs often surface &lt;em&gt;after&lt;/em&gt; the chaos, when the pipeline catches up and reveals a divergence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SLI dashboard&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;th&gt;During chaos&lt;/th&gt;
&lt;th&gt;Post-chaos&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;freshness_p99_min&lt;/td&gt;
&lt;td&gt;2.1&lt;/td&gt;
&lt;td&gt;4.8&lt;/td&gt;
&lt;td&gt;2.3&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lag_messages&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;td&gt;45,000&lt;/td&gt;
&lt;td&gt;1,200&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;batch_success_rate&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;row_count_delta&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;(n/a)&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;Pass (EOT)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Steady state = data SLI. Halt condition = threshold on that SLI. Anything else is chaos theatre. If you can't articulate what "the pipeline is healthy" means in a single Prometheus query, you're not ready to inject chaos.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — what interviewers actually probe on data chaos
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The senior data-platform chaos interview has a predictable structure: the interviewer opens with an ambiguous question ("how would you validate your Kafka + Spark stack is resilient?"), then progressively narrows to test whether you understand the framework. Candidates who name the steady-state / hypothesis / experiment / verify loop in sentence one score highest; candidates who describe "we'd kill some pods and see" score lowest. Walk through the interview grading rubric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous opener.&lt;/strong&gt; "How do you know your pipeline survives a broker crash?" — invites you to name the framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 1.&lt;/strong&gt; "What's your steady state?" — probes SLI axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 2.&lt;/strong&gt; "How do you decide the blast radius?" — probes blast-radius axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 3.&lt;/strong&gt; "What if the experiment breaks production?" — probes halt-condition axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 4.&lt;/strong&gt; "Where do you run this — dev, staging, or prod?" — probes graduation-of-trust axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 5.&lt;/strong&gt; "How is this different from load testing?" — probes methodology understanding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draft a 5-minute senior chaos engineering answer that covers all axes without waiting to be asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interview signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;Senior answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework named&lt;/td&gt;
&lt;td&gt;"we run chaos scripts"&lt;/td&gt;
&lt;td&gt;"steady state → hypothesis → experiment → verify"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steady state&lt;/td&gt;
&lt;td&gt;"everything green in Grafana"&lt;/td&gt;
&lt;td&gt;"freshness_p99_min &amp;lt; 5 and lag &amp;lt; 10k for 15 min"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blast radius&lt;/td&gt;
&lt;td&gt;"one broker"&lt;/td&gt;
&lt;td&gt;"1 of 6 brokers in stage; 1 in prod only after 3 passing stage runs"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Halt condition&lt;/td&gt;
&lt;td&gt;"we watch it"&lt;/td&gt;
&lt;td&gt;"promProbe on freshness &amp;gt; 15 min stops the experiment"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graduation&lt;/td&gt;
&lt;td&gt;"prod eventually"&lt;/td&gt;
&lt;td&gt;"dev → staging → 1% prod → 10% prod → 100% prod, one week between steps"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vs load testing&lt;/td&gt;
&lt;td&gt;"it's like load testing"&lt;/td&gt;
&lt;td&gt;"load tests break capacity; chaos breaks unavailability"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Senior chaos engineering answer template (5 minutes)
====================================================

Minute 1 — name the framework up front
  "I run chaos as a four-step loop: define the steady state as a
   data SLO, state a hypothesis about how the pipeline should
   behave under a specific failure, run the experiment inside a
   bounded blast radius with automatic halt on SLO breach, and
   verify by comparing SLI curves before/during/after."

Minute 2 — steady state
  "For our Kafka → Spark → Iceberg stack the steady state is:
   freshness_p99_min &amp;lt; 5, kafka_consumergroup_lag &amp;lt; 10000,
   batch_success_rate &amp;gt; 0.99, row_count_delta &amp;lt; 100 over 1 hour.
   Those four Prometheus queries are the SLI definition."

Minute 3 — hypothesis + experiment
  "Hypothesis: 'if we kill 1 of 6 Kafka brokers, freshness stays
   under 15 min and lag under 100k.' Experiment: LitmusChaos
   pod-delete on a broker labelled kafka-broker with
   PODS_AFFECTED_PERC=17, TOTAL_CHAOS_DURATION=60s, promProbes
   on all four SLIs, mode=Continuous, stopOnFailure=true."

Minute 4 — halt + blast radius
  "The blast radius is one broker out of six. Halt condition:
   any promProbe reads outside its threshold. Graduation:
   3 passing runs in staging over one week before promoting to
   a 1%-of-prod broker set, then 10%, then 100%. We never
   start with a bigger blast radius than we've already passed."

Minute 5 — verify + post-mortem
  "Verify: overlay before/during/after SLI curves in Grafana,
   write a GameDay doc with the hypothesis, actual outcome,
   probe timeline, and any backlog items. If the hypothesis
   failed, the backlog item is the fix; if it passed, the
   backlog item is the next larger blast radius."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Minute 1 names the four-step loop. This is the Netflix Principles-of-Chaos / PoP framework in one sentence; naming it up front signals you know the discipline exists and isn't just "kill some pods."&lt;/li&gt;
&lt;li&gt;Minute 2 states the steady state as four concrete Prometheus queries. This is the hardest thing for junior engineers to do — they'll say "the pipeline is healthy" but not codify what that means. Concrete numeric thresholds are senior signal.&lt;/li&gt;
&lt;li&gt;Minute 3 fuses hypothesis + experiment. The hypothesis is a &lt;em&gt;falsifiable&lt;/em&gt; prediction ("stays under 15 min lag"), not vague ("should be fine"). The experiment is the &lt;em&gt;smallest&lt;/em&gt; fault that tests the hypothesis; anything bigger is a shotgun.&lt;/li&gt;
&lt;li&gt;Minute 4 covers halt + blast radius — the safety controls. Naming &lt;code&gt;stopOnFailure=true&lt;/code&gt; and the graduation ladder (dev → stage → 1% prod → 10% → 100%) shows you understand that chaos is a controlled experiment, not a demolition.&lt;/li&gt;
&lt;li&gt;Minute 5 covers verify + post-mortem — the &lt;em&gt;learning&lt;/em&gt; step. This is what separates chaos engineering from chaos monkey: every experiment produces either "resilience confirmed, graduate the blast radius" or "resilience broken, here's the backlog item."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading criterion&lt;/th&gt;
&lt;th&gt;Weak score&lt;/th&gt;
&lt;th&gt;Senior score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Names the four-step loop in minute 1&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steady state as SLI queries&lt;/td&gt;
&lt;td&gt;occasional&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hypothesis is falsifiable&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Halt condition + blast radius&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify + post-mortem + backlog&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The senior chaos answer is a 5-minute monologue that walks the steady-state → hypothesis → experiment → verify loop with concrete SLI thresholds and a graduation ladder. Rehearse it once; deploy it every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on chaos engineering for data pipelines
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "Your team owns a Kafka + Spark Structured Streaming + Iceberg pipeline that promises 5-minute freshness to the warehouse. Design a chaos engineering program that proves the pipeline meets that SLO under broker crash, executor loss, and S3 latency spikes. Walk me through the steady-state definition, the hypothesis for each experiment, the blast-radius controls, the halt conditions, and how you graduate from staging to production."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using an SLI-first chaos program with graduated blast radius and halt-on-probe-failure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. Prometheus recording rules — the steady-state SLI catalog&lt;/span&gt;
&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline-steady-state&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline:freshness_p99_min&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;histogram_quantile(0.99,&lt;/span&gt;
            &lt;span class="s"&gt;sum by (le) (rate(iceberg_commit_age_seconds_bucket{table="events"}[5m]))&lt;/span&gt;
          &lt;span class="s"&gt;) / 60&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline:kafka_lag_messages&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum(kafka_consumergroup_lag{group="warehouse-sink"})&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline:batch_success_rate_5m&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sum(rate(spark_streaming_batch_success_total[5m]))&lt;/span&gt;
          &lt;span class="s"&gt;/ clamp_min(sum(rate(spark_streaming_batch_total[5m])), 1)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline:row_count_delta_1h&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;(source_events_1h_total - iceberg_rows_1h_total{table="events"})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. Three chaos experiments, one file, shared halt conditions&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosEngine&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline-chaos-suite&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engineState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active'&lt;/span&gt;
  &lt;span class="na"&gt;chaosServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;

  &lt;span class="na"&gt;experiments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;120'&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PODS_AFFECTED_PERC&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;                &lt;span class="c1"&gt;# 1 of 6 brokers&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TARGET_PODS&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=kafka-broker'&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;shared_probes&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;freshness-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:freshness_p99_min'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;15'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lag-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:kafka_lag_messages'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;100000'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;120'&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PODS_AFFECTED_PERC&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10'&lt;/span&gt;                &lt;span class="c1"&gt;# 1 of ~10 executors&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TARGET_PODS&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=spark-executor'&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*shared_probes&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-network-latency&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;60'&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NETWORK_LATENCY&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2000'&lt;/span&gt;              &lt;span class="c1"&gt;# +2s to S3 calls&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DESTINATION_HOSTS&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3.us-east-1.amazonaws.com'&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TARGET_PODS&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=iceberg-writer'&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*shared_probes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 3. GitOps promotion — kustomize overlay per blast radius stage&lt;/span&gt;
&lt;span class="c1"&gt;# overlays/staging/kustomization.yaml&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;../../base&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ChaosEngine&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;pipeline-chaos-suite&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;- op: replace&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/appinfo/appns&lt;/span&gt;
        &lt;span class="s"&gt;value: staging&lt;/span&gt;
      &lt;span class="s"&gt;- op: replace&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/experiments/0/spec/components/env&lt;/span&gt;
        &lt;span class="s"&gt;value:&lt;/span&gt;
          &lt;span class="s"&gt;- {name: PODS_AFFECTED_PERC, value: '17'}   # 1/6 brokers in stage&lt;/span&gt;
&lt;span class="s"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;# overlays/prod-1pct/kustomization.yaml — promoted only after 3 passing stage runs&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;../../base&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ChaosEngine&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;pipeline-chaos-suite&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;- op: replace&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/appinfo/appns&lt;/span&gt;
        &lt;span class="s"&gt;value: prod&lt;/span&gt;
      &lt;span class="s"&gt;- op: replace&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/experiments/0/spec/components/env&lt;/span&gt;
        &lt;span class="s"&gt;value:&lt;/span&gt;
          &lt;span class="s"&gt;- {name: PODS_AFFECTED_PERC, value: '17'}&lt;/span&gt;
          &lt;span class="s"&gt;- {name: NODE_SELECTOR, value: 'blast=1pct'}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Before (no chaos program)&lt;/th&gt;
&lt;th&gt;After (SLI-first chaos suite)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Freshness SLO measurement&lt;/td&gt;
&lt;td&gt;ad-hoc dashboards&lt;/td&gt;
&lt;td&gt;4 recording rules; single-query steady state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure discovery&lt;/td&gt;
&lt;td&gt;production incidents&lt;/td&gt;
&lt;td&gt;scheduled staging chaos + game days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Halt behaviour&lt;/td&gt;
&lt;td&gt;on-call reacts manually&lt;/td&gt;
&lt;td&gt;promProbe auto-aborts on SLI breach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blast radius control&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;PODS_AFFECTED_PERC per experiment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graduation&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;dev → staging → 1% prod → 10% → 100% via kustomize overlays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-mortem&lt;/td&gt;
&lt;td&gt;only after outages&lt;/td&gt;
&lt;td&gt;after every experiment, pass or fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backlog outcome&lt;/td&gt;
&lt;td&gt;vague "resilience" tickets&lt;/td&gt;
&lt;td&gt;concrete fix or "graduate radius" ticket&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, the chaos suite runs on a nightly cadence in staging, weekly in prod-1pct, monthly in prod-10pct. Every run writes a &lt;code&gt;ChaosResult&lt;/code&gt; CR consumed by a Grafana dashboard that overlays the steady-state SLIs on the experiment timeline; deviations surface as either "resilience regression" alerts or "blast-radius graduation" tickets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time-to-detect resilience regression&lt;/td&gt;
&lt;td&gt;production incident (~hours)&lt;/td&gt;
&lt;td&gt;staging chaos run (~30 min)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Halt latency on SLO breach&lt;/td&gt;
&lt;td&gt;operator-driven (~minutes)&lt;/td&gt;
&lt;td&gt;promProbe (~10 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blast radius audit trail&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;ChaosResult CR + GameDay doc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production chaos exposure&lt;/td&gt;
&lt;td&gt;none or too much&lt;/td&gt;
&lt;td&gt;1% → 10% → 100% graduation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-mortem coverage&lt;/td&gt;
&lt;td&gt;outage-driven&lt;/td&gt;
&lt;td&gt;every experiment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Steady state as SLI&lt;/strong&gt;&lt;/strong&gt; — the pipeline is "healthy" iff the four recording rules stay within their thresholds. This turns "how do I know if chaos broke things" from a debate into a Prometheus query. The four SLIs collectively cover freshness (age of the latest data), lag (queue depth), success rate (task health), and correctness (source-vs-sink parity) — the four dimensions of data-pipeline health.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Hypothesis-driven experiment&lt;/strong&gt;&lt;/strong&gt; — every ChaosEngine encodes a falsifiable prediction ("if I kill 1 of 6 brokers, freshness stays &amp;lt; 15 min"). Falsifiability is the difference between science and stunt; without a stated hypothesis, "we ran chaos" has no verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Bounded blast radius&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;PODS_AFFECTED_PERC=17&lt;/code&gt; (1 of 6 brokers) is the explicit dial. Every experiment starts with the smallest blast that still tests the hypothesis; graduation is an earned promotion, not a default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Halt on probe failure&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;stopOnFailure: true&lt;/code&gt; on the &lt;code&gt;promProbe&lt;/code&gt; gives you an automatic seatbelt. The experiment aborts the moment the SLI breaches; you don't need a human watching the dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;GitOps promotion via kustomize overlays&lt;/strong&gt;&lt;/strong&gt; — the same base ChaosEngine ships to dev / staging / prod-1pct / prod-10pct via kustomize overlays. This is the audit trail — the CR you ran in prod is provably the same one that passed in staging, differing only in blast-radius and namespace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one Prometheus (already deployed), LitmusChaos control plane (~1 pod, cluster-scoped), 4 recording rules (~ms/scrape), N ChaosEngine CRs per pipeline (~KB each). Compared to unmanaged production incidents, this is trading a small ops overhead for measurable resilience data. O(1) per experiment; O(scheduled cadence) per pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on resilient data pipelines&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Streaming&lt;/span&gt;
&lt;span&gt;Topic — streaming&lt;/span&gt;
&lt;strong&gt;Streaming resilience and back-pressure problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. LitmusChaos on Kubernetes — pod / network / node kill experiments
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;ChaosEngine&lt;/code&gt; CR + probe guardrails — the K8s-native chaos framework built for GitOps
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;litmuschaos&lt;/code&gt; runs chaos experiments as Kubernetes custom resources (&lt;code&gt;ChaosEngine&lt;/code&gt;, &lt;code&gt;ChaosExperiment&lt;/code&gt;, &lt;code&gt;ChaosResult&lt;/code&gt;) that target labelled workloads via a service account, execute a fault from a versioned experiment library (&lt;code&gt;pod-delete&lt;/code&gt;, &lt;code&gt;pod-network-loss&lt;/code&gt;, &lt;code&gt;node-taint&lt;/code&gt;, &lt;code&gt;disk-fill&lt;/code&gt;, &lt;code&gt;pod-cpu-hog&lt;/code&gt;, &lt;code&gt;pod-memory-hog&lt;/code&gt;, and dozens more), and gate the experiment behind probes (&lt;code&gt;httpProbe&lt;/code&gt;, &lt;code&gt;promProbe&lt;/code&gt;, &lt;code&gt;cmdProbe&lt;/code&gt;, &lt;code&gt;k8sProbe&lt;/code&gt;) that auto-abort on breach — the entire pattern is GitOps-friendly, CNCF-graduated, and free&lt;/strong&gt;. Every senior K8s-native data platform team has evaluated LitmusChaos; most have shipped it because the CR-driven model composes naturally with existing GitOps + RBAC + Prometheus tooling.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F925twguhu07xiggvanun.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F925twguhu07xiggvanun.jpeg" alt="Iconographic LitmusChaos diagram — a Kubernetes cluster with a ChaosEngine custom resource pointing at a target pod, four probe cards (httpProbe, promProbe, cmdProbe, k8sProbe) guarding the experiment, and a green PASS / red HALT verdict chip." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for LitmusChaos.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deployment surface.&lt;/strong&gt; Native Kubernetes. Runs as a Helm-installable control plane (&lt;code&gt;litmus-chaos-operator&lt;/code&gt;) plus a per-namespace &lt;code&gt;litmus-admin&lt;/code&gt; service account with RBAC scoped to the target workloads. No SaaS control plane, no external agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency.&lt;/strong&gt; Chaos starts within seconds of applying the &lt;code&gt;ChaosEngine&lt;/code&gt; CR. Probe evaluation is per-probe interval (typically 10 s). Auto-abort fires within one probe interval after breach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault library.&lt;/strong&gt; ~60 pre-built experiments across pod, node, network, disk, CPU, memory, and cloud (AWS EC2, EBS, RDS, Azure VM, GCP VM). Custom experiments are defined as &lt;code&gt;ChartServiceVersion&lt;/code&gt; CRs — you can ship your own fault.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Free (Apache-2.0). Optional paid observability + control plane via &lt;code&gt;chaos-center&lt;/code&gt;. Operational cost is one operator pod per cluster plus one &lt;code&gt;chaos-runner&lt;/code&gt; pod per active experiment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The four probe kinds — the guardrails that make chaos safe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;httpProbe&lt;/code&gt;.&lt;/strong&gt; Hits an HTTP endpoint on the target or a nearby service; checks response body / status / latency. Use for "is my /health endpoint returning 200?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;promProbe&lt;/code&gt;.&lt;/strong&gt; Runs a PromQL query against Prometheus; compares the result against a threshold. Use for &lt;em&gt;every&lt;/em&gt; data SLI — freshness, lag, success rate, correctness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cmdProbe&lt;/code&gt;.&lt;/strong&gt; Executes a shell command inside a chaos-runner pod (or a sidecar); checks the exit code or output. Use for "kafka-console-consumer sees a message" style checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;k8sProbe&lt;/code&gt;.&lt;/strong&gt; Reads a Kubernetes API object; compares its status field. Use for "is my StatefulSet Ready", "is my PVC Bound", "is my Deployment at the expected replica count."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Probe modes — when in the chaos lifecycle the probe runs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SOT&lt;/code&gt; (start-of-test).&lt;/strong&gt; Once, before chaos begins. Used to validate the pre-condition — "is the system healthy enough to inject chaos?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EOT&lt;/code&gt; (end-of-test).&lt;/strong&gt; Once, after chaos ends. Used to validate the recovery — "did the pipeline catch up within N minutes?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Edge&lt;/code&gt;.&lt;/strong&gt; Both SOT and EOT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Continuous&lt;/code&gt;.&lt;/strong&gt; Every N seconds throughout the chaos window. Used with &lt;code&gt;stopOnFailure: true&lt;/code&gt; to enforce halt conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OnChaos&lt;/code&gt;.&lt;/strong&gt; Only during the chaos period (skips before/after). Used for "verify a specific behaviour appears while the fault is active."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on LitmusChaos.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How does LitmusChaos abort an experiment?" — required answer: &lt;code&gt;probe stopOnFailure: true&lt;/code&gt; + &lt;code&gt;mode: Continuous&lt;/code&gt;; the operator writes a &lt;code&gt;ChaosResult&lt;/code&gt; verdict of Fail.&lt;/li&gt;
&lt;li&gt;"How do you scope which pods get killed?" — &lt;code&gt;applabel&lt;/code&gt; selector + &lt;code&gt;PODS_AFFECTED_PERC&lt;/code&gt;; both are enforced by the operator.&lt;/li&gt;
&lt;li&gt;"How do you version chaos experiments?" — &lt;code&gt;ChartServiceVersion&lt;/code&gt; CRs in a Git repo; &lt;code&gt;chaos-hub&lt;/code&gt; is the community index.&lt;/li&gt;
&lt;li&gt;"How is LitmusChaos different from &lt;code&gt;chaos-mesh&lt;/code&gt;?" — LitmusChaos is CR + probe + GitOps; chaos-mesh is CR + Web UI + dashboard. Both are CNCF; pick based on team's GitOps preference.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — &lt;code&gt;pod-delete&lt;/code&gt; on a Kafka broker with a &lt;code&gt;promProbe&lt;/code&gt; guardrail
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical LitmusChaos data-pipeline experiment: delete a Kafka broker pod, watch consumer lag stay under a threshold, verify the broker rejoins the ISR within a bounded time. Build the ChaosEngine, install the experiment chart, and apply it against a stage cluster.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target.&lt;/strong&gt; A &lt;code&gt;StatefulSet/kafka-broker&lt;/code&gt; in the &lt;code&gt;kafka&lt;/code&gt; namespace with 6 replicas (&lt;code&gt;kafka-broker-0&lt;/code&gt; through &lt;code&gt;kafka-broker-5&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault.&lt;/strong&gt; &lt;code&gt;pod-delete&lt;/code&gt; from the &lt;code&gt;generic&lt;/code&gt; chart on &lt;code&gt;chaos-hub&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrail.&lt;/strong&gt; &lt;code&gt;promProbe&lt;/code&gt; on &lt;code&gt;max(kafka_consumergroup_lag)&lt;/code&gt; with a &lt;code&gt;&amp;lt; 50000&lt;/code&gt; threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius.&lt;/strong&gt; 1 pod at a time (&lt;code&gt;PODS_AFFECTED_PERC=17&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the &lt;code&gt;ChaosEngine&lt;/code&gt; CR, the RBAC for the chaos service account, and the &lt;code&gt;kubectl&lt;/code&gt; apply that runs the experiment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ServiceAccount litmus-admin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;scoped identity for the chaos runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Role/RoleBinding&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RBAC on pods, chaosengines, chaosresults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ChaosExperiment pod-delete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fault library chart (installed once per namespace)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ChaosEngine kafka-broker-chaos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the actual experiment definition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;promProbe consumer-lag-guard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;halt on lag &amp;gt; 50k&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. RBAC — the chaos runner needs to delete pods in the kafka namespace&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceAccount&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Role&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka&lt;/span&gt;
&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pods&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;configmaps&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;list&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;watch&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;create&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;update&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;deletecollection&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;apps&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;statefulsets&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;replicasets&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;list&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;watch&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;litmuschaos.io&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;chaosengines&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;chaosexperiments&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;chaosresults&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;list&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;create&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;update&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;list&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;create&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RoleBinding&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka&lt;/span&gt;
&lt;span class="na"&gt;roleRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;rbac.authorization.k8s.io&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Role&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;litmus-admin&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;subjects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt;&lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;litmus-admin&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;kafka&lt;/span&gt;&lt;span class="pi"&gt;}]&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;# 2. Experiment chart — installed once per namespace&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosExperiment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Namespaced&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt;&lt;span class="nv"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pods&lt;/span&gt;&lt;span class="pi"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;create&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;list&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;update&lt;/span&gt;&lt;span class="pi"&gt;]}]&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos/go-runner:3.10.0&lt;/span&gt;
    &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Always&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;-c&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;./experiments -name pod-delete&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;/bin/bash&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;60'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;RAMP_TIME&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;FORCE&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;CHAOS_INTERVAL&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;30'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PODS_AFFECTED_PERC&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TARGET_CONTAINER&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;# 3. ChaosEngine — the experiment execution&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosEngine&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka-broker-chaos&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engineState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active'&lt;/span&gt;
  &lt;span class="na"&gt;chaosServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;appinfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;appns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kafka'&lt;/span&gt;
    &lt;span class="na"&gt;applabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=kafka-broker'&lt;/span&gt;
    &lt;span class="na"&gt;appkind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;statefulset'&lt;/span&gt;
  &lt;span class="na"&gt;experiments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;60'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;CHAOS_INTERVAL&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;30'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;FORCE&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PODS_AFFECTED_PERC&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;consumer-lag-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
              &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
              &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
              &lt;span class="na"&gt;probePollingInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
              &lt;span class="na"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max(kafka_consumergroup_lag{group="warehouse-sink"})'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;50000'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 4. Apply and watch&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; rbac.yaml &lt;span class="nt"&gt;-f&lt;/span&gt; experiment.yaml &lt;span class="nt"&gt;-f&lt;/span&gt; chaosengine.yaml

&lt;span class="c"&gt;# 5. Watch the ChaosResult verdict&lt;/span&gt;
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kafka get chaosresult kafka-broker-chaos-pod-delete &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;span class="c"&gt;# → phase: Running          → phase: Completed&lt;/span&gt;
&lt;span class="c"&gt;# → verdict: (blank)         → verdict: Pass | Fail&lt;/span&gt;

&lt;span class="c"&gt;# 6. Inspect the probe timeline&lt;/span&gt;
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kafka describe chaosresult kafka-broker-chaos-pod-delete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The RBAC block creates a &lt;code&gt;litmus-admin&lt;/code&gt; service account in the &lt;code&gt;kafka&lt;/code&gt; namespace with just-enough permission: delete pods, read statefulsets, manage its own chaos CRs, create runner jobs. Nothing cluster-wide, nothing outside &lt;code&gt;kafka&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ChaosExperiment&lt;/code&gt; chart is installed &lt;em&gt;once&lt;/em&gt; per namespace. It declares the fault image (&lt;code&gt;litmuschaos/go-runner&lt;/code&gt;), the entry-point, and the default env vars. Think of it as a Helm chart for a chaos primitive.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ChaosEngine&lt;/code&gt; is the actual experiment execution. &lt;code&gt;appinfo&lt;/code&gt; scopes the fault to pods labelled &lt;code&gt;app=kafka-broker&lt;/code&gt; (which selects &lt;code&gt;kafka-broker-0&lt;/code&gt; through &lt;code&gt;kafka-broker-5&lt;/code&gt; in the StatefulSet). &lt;code&gt;PODS_AFFECTED_PERC=17&lt;/code&gt; = ceil(6 × 0.17) = 1 pod.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;promProbe&lt;/code&gt; runs &lt;code&gt;max(kafka_consumergroup_lag{group="warehouse-sink"})&lt;/code&gt; against Prometheus every 10 s. If the query returns anything not &lt;code&gt;&amp;lt; 50000&lt;/code&gt;, &lt;code&gt;stopOnFailure: true&lt;/code&gt; halts the experiment, the chaos-runner exits, and the &lt;code&gt;ChaosResult&lt;/code&gt; records a &lt;code&gt;Fail&lt;/code&gt; verdict with the probe timeline.&lt;/li&gt;
&lt;li&gt;After the experiment completes, &lt;code&gt;kubectl describe chaosresult&lt;/code&gt; shows the phase (&lt;code&gt;Completed&lt;/code&gt;), verdict (&lt;code&gt;Pass&lt;/code&gt; / &lt;code&gt;Fail&lt;/code&gt;), and each probe's evaluation history. This is the CI-friendly artifact you check into GitOps or feed into your GameDay dashboard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (s)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Probe: consumer-lag&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;ChaosEngine applied&lt;/td&gt;
&lt;td&gt;800 (&amp;lt; 50k)&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;chaos-runner spawned&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;kafka-broker-3 deleted&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;leader re-election&lt;/td&gt;
&lt;td&gt;4800&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;kafka-broker-3 restarted&lt;/td&gt;
&lt;td&gt;12000&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;broker rejoined ISR&lt;/td&gt;
&lt;td&gt;8000&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;chaos window closed&lt;/td&gt;
&lt;td&gt;3200&lt;/td&gt;
&lt;td&gt;Pass (final)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any Kubernetes-native data platform, model chaos as &lt;code&gt;ChaosEngine&lt;/code&gt; CRs alongside your workload manifests. RBAC-scope the runner to the target namespace, ship the CR through GitOps, and gate every experiment behind a &lt;code&gt;promProbe&lt;/code&gt; bound to a data SLI.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — &lt;code&gt;pod-network-loss&lt;/code&gt; between Airflow scheduler and metadata DB
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A subtler chaos experiment: introduce packet loss between the Airflow scheduler pod and the metadata Postgres, verify the scheduler survives without corrupting DAG state, and confirm task instances resume cleanly. This is a &lt;em&gt;stateful&lt;/em&gt; chaos scenario — the scheduler is the sole writer to &lt;code&gt;dag_run&lt;/code&gt; / &lt;code&gt;task_instance&lt;/code&gt; tables and a partial write is much scarier than a clean pod kill.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target.&lt;/strong&gt; &lt;code&gt;Deployment/airflow-scheduler&lt;/code&gt;, 1 pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault.&lt;/strong&gt; &lt;code&gt;pod-network-loss&lt;/code&gt; — introduces packet loss on the pod's network interface to a specific destination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destination.&lt;/strong&gt; The Airflow metadata Postgres service (&lt;code&gt;postgres-airflow.airflow.svc&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrail.&lt;/strong&gt; &lt;code&gt;k8sProbe&lt;/code&gt; on the scheduler Deployment ready status + &lt;code&gt;cmdProbe&lt;/code&gt; that runs &lt;code&gt;airflow db check&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the ChaosEngine that injects 50% packet loss to Postgres for 60 s and verify the scheduler recovers without a corrupted metadata write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fault&lt;/td&gt;
&lt;td&gt;pod-network-loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NETWORK_PACKET_LOSS_PERCENTAGE&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DESTINATION_HOSTS&lt;/td&gt;
&lt;td&gt;postgres-airflow.airflow.svc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DESTINATION_PORTS&lt;/td&gt;
&lt;td&gt;5432&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TOTAL_CHAOS_DURATION&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-chaos verification&lt;/td&gt;
&lt;td&gt;airflow db check + no &lt;code&gt;stale&lt;/code&gt; &lt;code&gt;task_instance&lt;/code&gt; rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosEngine&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow-scheduler-netloss&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engineState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active'&lt;/span&gt;
  &lt;span class="na"&gt;chaosServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;appinfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;appns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;airflow'&lt;/span&gt;
    &lt;span class="na"&gt;applabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=airflow-scheduler'&lt;/span&gt;
    &lt;span class="na"&gt;appkind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deployment'&lt;/span&gt;
  &lt;span class="na"&gt;experiments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-network-loss&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;60'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NETWORK_PACKET_LOSS_PERCENTAGE&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;50'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;DESTINATION_HOSTS&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;postgres-airflow.airflow.svc'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;DESTINATION_PORTS&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5432'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;CONTAINER_RUNTIME&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;containerd'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;SOCKET_PATH&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/run/containerd/containerd.sock'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scheduler-ready-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;k8sProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;15&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;k8sProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apps'&lt;/span&gt;
              &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v1'&lt;/span&gt;
              &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deployments'&lt;/span&gt;
              &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;airflow'&lt;/span&gt;
              &lt;span class="na"&gt;fieldSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metadata.name=airflow-scheduler'&lt;/span&gt;
              &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;present'&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metadata-consistency-check&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cmdProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EOT&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;20&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;cmdProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                &lt;span class="s"&gt;psql -h postgres-airflow.airflow.svc -U airflow -d airflow \&lt;/span&gt;
                     &lt;span class="s"&gt;-tAc "SELECT count(*) FROM task_instance WHERE state='running' AND queued_dttm &amp;lt; NOW() - INTERVAL '10 minutes'"&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;='&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;postgres:16-alpine'&lt;/span&gt;
                &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
                &lt;span class="na"&gt;inheritInputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;pod-network-loss&lt;/code&gt; uses &lt;code&gt;tc netem&lt;/code&gt; (kernel traffic control) inside the target pod's network namespace to drop 50% of packets destined for &lt;code&gt;postgres-airflow.airflow.svc:5432&lt;/code&gt;. The fault is bounded by IP + port so &lt;em&gt;only&lt;/em&gt; Postgres traffic is affected; the scheduler can still reach the k8s API, the DAGs Git-sync sidecar, etc.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;k8sProbe&lt;/code&gt; runs continuously and checks the Airflow scheduler Deployment stays in a valid state. If the Deployment goes to zero ready replicas (i.e. the scheduler crashed and can't restart), &lt;code&gt;stopOnFailure&lt;/code&gt; aborts the experiment.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;cmdProbe&lt;/code&gt; runs at end-of-test, launching a temporary &lt;code&gt;postgres:16-alpine&lt;/code&gt; pod that connects to the Airflow metadata DB and asks: &lt;em&gt;"are there any task_instances in state 'running' with a queue time older than 10 minutes?"&lt;/em&gt; If the answer is not zero, we have stuck rows — the network loss corrupted a state transition and the experiment fails.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;cmdProbe&lt;/code&gt; pattern is powerful because it lets you assert &lt;em&gt;data-shape&lt;/em&gt; invariants that Prometheus can't natively express. "No stuck task instances" is a domain-specific invariant of Airflow's operational state; a PromQL query can approximate it but the &lt;code&gt;cmdProbe&lt;/code&gt; gives you the ground-truth SQL.&lt;/li&gt;
&lt;li&gt;The fault duration (60 s) is deliberately short — enough to force Postgres retries but not so long that scheduled DAG runs will start missing SLAs. Longer network-loss experiments should have &lt;code&gt;PODS_AFFECTED_PERC&lt;/code&gt; reduced accordingly and require the game-day escalation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Probe verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;ChaosEngine applied&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0-60 s&lt;/td&gt;
&lt;td&gt;50% packet loss to Postgres&lt;/td&gt;
&lt;td&gt;scheduler-ready-guard: Pass (deployment still Ready)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60-70 s&lt;/td&gt;
&lt;td&gt;packet loss removed; connection retries drain&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EOT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT count(*) FROM task_instance WHERE stuck&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;metadata-consistency-check: Pass (0 stuck)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;ChaosResult verdict&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For stateful-writer chaos (scheduler, metadata DB, coordinator), pair a &lt;code&gt;k8sProbe&lt;/code&gt; (liveness during chaos) with an &lt;code&gt;EOT cmdProbe&lt;/code&gt; (data consistency after chaos). One catches the crash, the other catches the corruption.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — &lt;code&gt;disk-fill&lt;/code&gt; on the Airflow scheduler pod
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A resource-exhaustion chaos experiment: fill the Airflow scheduler pod's ephemeral disk to 95% and observe how the scheduler handles the disk pressure. Kubernetes may evict the pod under &lt;code&gt;DiskPressure&lt;/code&gt;, the scheduler may fail to write logs, or the &lt;code&gt;dags/&lt;/code&gt; folder Git-sync sidecar may fail to pull new DAGs. Every one of these is a real production failure mode; making it a scheduled chaos experiment turns it from a surprise into a learning.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target.&lt;/strong&gt; &lt;code&gt;Deployment/airflow-scheduler&lt;/code&gt;, 1 pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault.&lt;/strong&gt; &lt;code&gt;disk-fill&lt;/code&gt; — writes a large file to the pod's ephemeral storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fill target.&lt;/strong&gt; 95% of the pod's ephemeral-storage limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrail.&lt;/strong&gt; &lt;code&gt;promProbe&lt;/code&gt; on &lt;code&gt;pipeline:batch_success_rate_5m&lt;/code&gt; + &lt;code&gt;k8sProbe&lt;/code&gt; on scheduler deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the ChaosEngine that fills the scheduler's ephemeral disk to 95% for 90 s and confirm DAG success rate stays above 90%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fault&lt;/td&gt;
&lt;td&gt;disk-fill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FILL_PERCENTAGE&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EPHEMERAL_STORAGE_MEBIBYTES&lt;/td&gt;
&lt;td&gt;4096&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TOTAL_CHAOS_DURATION&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Halt SLI&lt;/td&gt;
&lt;td&gt;batch_success_rate_5m &amp;gt; 0.90&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosEngine&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow-scheduler-diskfill&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engineState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active'&lt;/span&gt;
  &lt;span class="na"&gt;chaosServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;appinfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;appns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;airflow'&lt;/span&gt;
    &lt;span class="na"&gt;applabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=airflow-scheduler'&lt;/span&gt;
    &lt;span class="na"&gt;appkind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deployment'&lt;/span&gt;
  &lt;span class="na"&gt;experiments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disk-fill&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;90'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;FILL_PERCENTAGE&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;95'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;EPHEMERAL_STORAGE_MEBIBYTES&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;4096'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;CONTAINER_RUNTIME&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;containerd'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;SOCKET_PATH&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/run/containerd/containerd.sock'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;DATA_BLOCK_SIZE&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;256'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch-success-rate-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:batch_success_rate_5m'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;float&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.90'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scheduler-alive-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;k8sProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;15&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;k8sProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apps'&lt;/span&gt;
              &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v1'&lt;/span&gt;
              &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deployments'&lt;/span&gt;
              &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;airflow'&lt;/span&gt;
              &lt;span class="na"&gt;fieldSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metadata.name=airflow-scheduler'&lt;/span&gt;
              &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;present'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;disk-fill&lt;/code&gt; writes a &lt;code&gt;chaos-fill&lt;/code&gt; file inside the target pod's writable filesystem using &lt;code&gt;dd if=/dev/zero of=... bs=... count=...&lt;/code&gt;. It writes until the pod's ephemeral-storage usage hits &lt;code&gt;FILL_PERCENTAGE&lt;/code&gt; (95%) of the declared limit.&lt;/li&gt;
&lt;li&gt;If the pod has an &lt;code&gt;ephemeral-storage&lt;/code&gt; request/limit set in its spec, Kubernetes may trigger &lt;code&gt;DiskPressure&lt;/code&gt; eviction. If not, the pod's writable layer just fills up and any component that writes (Airflow's log rotator, the SQLite session store, the Git-sync sidecar) starts erroring. Both failure modes are interesting.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;promProbe&lt;/code&gt; on &lt;code&gt;batch_success_rate_5m&lt;/code&gt; catches the case where the scheduler survives but DAG runs start failing (e.g. because the scheduler can no longer write task logs). The &lt;code&gt;k8sProbe&lt;/code&gt; catches the eviction case.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DATA_BLOCK_SIZE=256&lt;/code&gt; tunes the &lt;code&gt;dd&lt;/code&gt; block size for reasonable I/O behaviour — small enough not to lock the disk, large enough to fill quickly.&lt;/li&gt;
&lt;li&gt;The 90-second window is long enough for the disk-pressure symptom to propagate through the scheduler's next-heartbeat, DAG-parse, and task-launch cycles. Shorter windows may not exercise the full failure envelope.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Probe: success rate&lt;/th&gt;
&lt;th&gt;Probe: scheduler alive&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;td&gt;Chaos applied&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 s&lt;/td&gt;
&lt;td&gt;Disk 90% full&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;td&gt;Disk 95% full; log writes failing&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90 s&lt;/td&gt;
&lt;td&gt;Chaos removed; disk freed&lt;/td&gt;
&lt;td&gt;0.96&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post&lt;/td&gt;
&lt;td&gt;Log-writer catches up&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;Pass (final)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every stateful pod deserves a &lt;code&gt;disk-fill&lt;/code&gt; chaos test. The failure modes are legion (log rotator crashes, sqlite locks, git-sync stalls, eviction storms) and every one of them surfaces only under actual disk pressure — no unit test finds them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on LitmusChaos
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You've adopted LitmusChaos as the K8s-native chaos framework for your data platform. Design the end-to-end pattern for shipping a new chaos experiment — from a Prometheus SLI definition through the &lt;code&gt;ChaosEngine&lt;/code&gt; CR, the RBAC, the GitOps promotion, and the &lt;code&gt;ChaosResult&lt;/code&gt; post-processing that lands in the on-call GameDay doc. Include how you'd version experiments and how you'd deprecate a broken one."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a GitOps chaos pipeline with SLI-first probes and PR-gated promotion
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# repo layout&lt;/span&gt;
&lt;span class="c1"&gt;# chaos/&lt;/span&gt;
&lt;span class="c1"&gt;#   base/&lt;/span&gt;
&lt;span class="c1"&gt;#     _slis.yaml               # Prometheus recording rules (shared)&lt;/span&gt;
&lt;span class="c1"&gt;#     _rbac.yaml               # litmus-admin ServiceAccount + Role&lt;/span&gt;
&lt;span class="c1"&gt;#     experiments/&lt;/span&gt;
&lt;span class="c1"&gt;#       pod-delete.yaml        # ChaosExperiment chart (installed once)&lt;/span&gt;
&lt;span class="c1"&gt;#       pod-network-loss.yaml&lt;/span&gt;
&lt;span class="c1"&gt;#       disk-fill.yaml&lt;/span&gt;
&lt;span class="c1"&gt;#     engines/&lt;/span&gt;
&lt;span class="c1"&gt;#       kafka-broker-kill.yaml&lt;/span&gt;
&lt;span class="c1"&gt;#       airflow-net-loss.yaml&lt;/span&gt;
&lt;span class="c1"&gt;#       spark-executor-kill.yaml&lt;/span&gt;
&lt;span class="c1"&gt;#   overlays/&lt;/span&gt;
&lt;span class="c1"&gt;#     dev/kustomization.yaml&lt;/span&gt;
&lt;span class="c1"&gt;#     staging/kustomization.yaml&lt;/span&gt;
&lt;span class="c1"&gt;#     prod-1pct/kustomization.yaml&lt;/span&gt;
&lt;span class="c1"&gt;#     prod-10pct/kustomization.yaml&lt;/span&gt;
&lt;span class="c1"&gt;#     prod-100pct/kustomization.yaml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# base/engines/kafka-broker-kill.yaml — the source of truth&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosEngine&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka-broker-kill&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;chaos.pipecode.ai/owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data-platform-sre'&lt;/span&gt;
    &lt;span class="na"&gt;chaos.pipecode.ai/tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;staging'&lt;/span&gt;
    &lt;span class="na"&gt;chaos.pipecode.ai/version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v3'&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engineState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active'&lt;/span&gt;
  &lt;span class="na"&gt;chaosServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;appinfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;appns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kafka'&lt;/span&gt;
    &lt;span class="na"&gt;applabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=kafka-broker'&lt;/span&gt;
    &lt;span class="na"&gt;appkind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;statefulset'&lt;/span&gt;
  &lt;span class="na"&gt;experiments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;120'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PODS_AFFECTED_PERC&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;freshness-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:freshness_p99_min'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;15'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# post_chaos.py — a small daemon that watches ChaosResult CRs and pipes to Slack + Jira
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;SLACK_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://hooks.slack.com/services/xxx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;JIRA_URL&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://acme.atlassian.net/rest/api/2/issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;watch_chaosresults&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kubectl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chaosresults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PIPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bufsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;evt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;experimentStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;experimentStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;probe_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;probeStatuses&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SLACK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:white_check_mark: chaos &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; passed. Consider graduating blast radius.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SLACK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:rotating_light: chaos &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; FAILED. Probe timeline: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;probe_status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JIRA_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chaos regression: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probe_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issuetype&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labels&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chaos-regression&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot@acme.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;***&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;watch_chaosresults&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watch died:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Engineer opens PR&lt;/td&gt;
&lt;td&gt;edit &lt;code&gt;base/engines/kafka-broker-kill.yaml&lt;/code&gt; — bump &lt;code&gt;PODS_AFFECTED_PERC&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;CI&lt;/td&gt;
&lt;td&gt;kustomize build; kubeconform schema check; OPA policy check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Argo CD&lt;/td&gt;
&lt;td&gt;sync PR branch to &lt;code&gt;dev&lt;/code&gt; cluster; nightly chaos run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;ChaosResult&lt;/td&gt;
&lt;td&gt;verdict recorded; posted to Slack + optional Jira&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;PR gets 3 green nightly runs&lt;/td&gt;
&lt;td&gt;reviewer approves promotion to &lt;code&gt;staging&lt;/code&gt; overlay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Same PR extended&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;overlays/prod-1pct/kustomization.yaml&lt;/code&gt; bumped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Argo CD&lt;/td&gt;
&lt;td&gt;sync to prod-1pct cluster; monthly chaos run during business hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;3 green monthly runs&lt;/td&gt;
&lt;td&gt;promote to prod-10pct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Deprecation&lt;/td&gt;
&lt;td&gt;old engine &lt;code&gt;engineState: 'stop'&lt;/code&gt; + PR labelled &lt;code&gt;chaos-retirement&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, every chaos experiment flows through a PR → dev → staging → prod-1pct → prod-10pct → prod-100pct GitOps pipeline. The &lt;code&gt;ChaosResult&lt;/code&gt; CR is the receipt; the Slack + Jira integration is the human interface; the RBAC is the safety boundary. New experiments cannot skip stages; failed experiments cannot self-graduate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ChaosEngine&lt;/code&gt; CR in Git&lt;/td&gt;
&lt;td&gt;source of truth for the experiment&lt;/td&gt;
&lt;td&gt;Argo CD / reviewer / audit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ChaosResult&lt;/code&gt; CR in cluster&lt;/td&gt;
&lt;td&gt;verdict + probe timeline&lt;/td&gt;
&lt;td&gt;Slack bot / Grafana / GameDay doc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kustomize overlays&lt;/td&gt;
&lt;td&gt;blast-radius graduation ladder&lt;/td&gt;
&lt;td&gt;PR reviewer / audit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack alert&lt;/td&gt;
&lt;td&gt;live notification&lt;/td&gt;
&lt;td&gt;on-call rotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jira ticket&lt;/td&gt;
&lt;td&gt;failure follow-up&lt;/td&gt;
&lt;td&gt;data-platform-sre backlog&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ChaosEngine as CR&lt;/strong&gt;&lt;/strong&gt; — the experiment is a Kubernetes object, versionable, reviewable, RBAC-scoped. Not a Terraform script, not a Jenkins job — a first-class cluster object. The GitOps toolchain treats it the same as any Deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Shared SLI recording rules&lt;/strong&gt;&lt;/strong&gt; — a single &lt;code&gt;_slis.yaml&lt;/code&gt; file defines the four data SLIs; every experiment's probes reference &lt;code&gt;pipeline:*&lt;/code&gt; metrics. New SLIs land in one place; new experiments reuse them for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;RBAC-scoped runner&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;litmus-admin&lt;/code&gt; has permission to delete pods only in the target namespace, and only for chaos CRs it created. A compromised chaos runner cannot escalate; the blast radius is bounded by the RBAC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;kustomize overlays as the graduation ladder&lt;/strong&gt;&lt;/strong&gt; — the same base CR ships to five stages differing only in &lt;code&gt;PODS_AFFECTED_PERC&lt;/code&gt;, node selectors, and namespace. The diff between &lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;prod-100pct&lt;/code&gt; is auditable in a single &lt;code&gt;git diff&lt;/code&gt; output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;PR-gated promotion&lt;/strong&gt;&lt;/strong&gt; — you cannot promote an experiment to a bigger blast radius without three green runs at the previous stage and a peer-reviewed PR. This is the human safety net that makes chaos in production defensible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ChaosResult post-processing&lt;/strong&gt;&lt;/strong&gt; — the verdict + probe timeline pipes to Slack (visibility) and Jira (accountability). Every failed experiment produces a ticket; every passed experiment produces a "consider graduating" signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one operator pod, one runner pod per active experiment, a few KB per CR, one Slack webhook, one Jira token. Compared to the incident-driven alternative (learn resilience during real outages), this is a 100× reduction in mean-time-to-learn. O(1) per experiment; O(N experiments × N stages) per pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on Kubernetes-native reliability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on scheduled DAG failure recovery&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Gremlin — scenarios, blast-radius controls, workflows
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Attack primitives → Scenarios → Halt-on-breach — the SaaS control plane for cross-platform chaos
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;gremlin&lt;/code&gt; is a commercial SaaS chaos platform whose control plane runs in the vendor's cloud and whose lightweight agent (&lt;code&gt;gremlind&lt;/code&gt;) runs on every VM, container, ECS/Fargate task, or Kubernetes node you want to attack — it exposes low-level &lt;em&gt;attack primitives&lt;/em&gt; (CPU, memory, disk, IO, latency, packet-loss, blackhole, DNS, shutdown, time-travel) that stitch into higher-level &lt;em&gt;Scenarios&lt;/em&gt; with explicit blast-radius sliders and Halt-on-Breach Health Checks tied to your observability platform (Datadog, New Relic, Prometheus, custom webhooks)&lt;/strong&gt;. Every senior data platform team evaluating chaos tooling faces the LitmusChaos-vs-Gremlin decision; the deciding factor is almost always deployment surface (K8s-only vs cross-platform) and audit posture (self-hosted OSS vs SaaS with SOC-2).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4tz21wjbcr7fv0zkfnxd.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4tz21wjbcr7fv0zkfnxd.jpeg" alt="Iconographic Gremlin scenario board — a scenario card with three stacked attack chips (CPU, blackhole, latency), a large blast-radius dial rotated to 10%, and a big red HALT button wired to health-check curves." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for Gremlin.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deployment surface.&lt;/strong&gt; Cross-platform. Runs on Linux, Windows, container, K8s, ECS, Fargate, on-prem, EC2, GCE. The &lt;code&gt;gremlind&lt;/code&gt; agent is a single ~50MB Go binary that phones home to the SaaS control plane; attacks are triggered from the web UI, Terraform provider, or API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault library.&lt;/strong&gt; ~15 attack primitives, richer than LitmusChaos on node-level and network-level primitives (DNS, packet-loss, time-travel, IO throttle). Weaker on Kubernetes-object-aware faults (no direct &lt;code&gt;k8sProbe&lt;/code&gt; analog).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast-radius model.&lt;/strong&gt; Explicit sliders: "attack N clients matching tag=X." Multi-dimensional selectors: &lt;code&gt;tag:cluster=prod&lt;/code&gt;, &lt;code&gt;tag:service=kafka&lt;/code&gt;, &lt;code&gt;container:image=confluent-kafka&lt;/code&gt;. Percentage sliders per selector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Halt behaviour.&lt;/strong&gt; "Halt-on-Breach" scenarios — a scenario references a Datadog / New Relic / Prometheus / webhook Health Check; the check runs on a cadence; if any check reports unhealthy the scenario aborts and the attack rolls back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Attack primitives — the low-level faults Gremlin ships.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource.&lt;/strong&gt; &lt;code&gt;cpu&lt;/code&gt; (peg N cores at X%), &lt;code&gt;memory&lt;/code&gt; (allocate X GB), &lt;code&gt;disk&lt;/code&gt; (fill X% of a volume), &lt;code&gt;io&lt;/code&gt; (throttle read/write bandwidth).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network.&lt;/strong&gt; &lt;code&gt;latency&lt;/code&gt; (add X ms to selected traffic), &lt;code&gt;packet-loss&lt;/code&gt; (drop X% of packets), &lt;code&gt;blackhole&lt;/code&gt; (drop all packets to/from a destination), &lt;code&gt;dns&lt;/code&gt; (return NXDOMAIN or spoofed IP), &lt;code&gt;bandwidth&lt;/code&gt; (throttle Mbps).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State.&lt;/strong&gt; &lt;code&gt;shutdown&lt;/code&gt; (halt the host), &lt;code&gt;process-killer&lt;/code&gt; (SIGTERM/SIGKILL specific PIDs/regex), &lt;code&gt;time-travel&lt;/code&gt; (shift the system clock forward/backward).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scenarios — the workflow layer.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Definition.&lt;/strong&gt; A Scenario is an ordered list of &lt;em&gt;Steps&lt;/em&gt;. Each Step is either an &lt;em&gt;Attack&lt;/em&gt; (an attack primitive with parameters and blast radius) or a &lt;em&gt;Delay&lt;/em&gt;. Scenarios can also declare &lt;em&gt;Health Checks&lt;/em&gt; that run continuously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Halt-on-Breach.&lt;/strong&gt; If any Health Check reports unhealthy during scenario execution, all remaining Steps are cancelled and any active attack is halted (reverting &lt;code&gt;tc netem&lt;/code&gt;, freeing memory, releasing CPU pegs). This is Gremlin's equivalent of LitmusChaos &lt;code&gt;stopOnFailure&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attack rollback.&lt;/strong&gt; Every attack primitive has an idempotent rollback path — killing the attack ID via API restores the pre-attack state. This is the safety property that makes production chaos possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on Gremlin.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How does Gremlin differ from LitmusChaos?" — required answer: cross-platform SaaS with agents vs K8s-native OSS with CRs; Gremlin has richer network/state primitives, LitmusChaos has richer K8s-object probes.&lt;/li&gt;
&lt;li&gt;"How do you halt a Gremlin attack on SLO breach?" — required answer: Halt-on-Breach Scenario with a Health Check tied to your observability platform.&lt;/li&gt;
&lt;li&gt;"How do you audit who ran what chaos?" — Gremlin control plane logs every attack with user, timestamp, blast-radius, targets; export via API to your SIEM.&lt;/li&gt;
&lt;li&gt;"How do you version Gremlin scenarios?" — Terraform &lt;code&gt;gremlin_scenario&lt;/code&gt; resource or the REST API; check the JSON into Git alongside your infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — &lt;code&gt;blackhole&lt;/code&gt; attack on Spark executors with halt on SLA breach
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A stateful chaos experiment: blackhole all network traffic on 10% of Spark executor nodes for 60 s, forcing shuffle-fetch failures on running Spark jobs. Verify the job SLA (&lt;code&gt;batch_processing_seconds_p99&lt;/code&gt;) stays under threshold and the driver reprovisions executors. Model the experiment as a Gremlin Scenario with three steps: a &lt;code&gt;latency&lt;/code&gt; warm-up, the main &lt;code&gt;blackhole&lt;/code&gt;, and a Datadog Halt-on-Breach check.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target.&lt;/strong&gt; EC2 instances tagged &lt;code&gt;service=spark-executor&lt;/code&gt;, &lt;code&gt;env=stage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius.&lt;/strong&gt; 10% of matched clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps.&lt;/strong&gt; 30 s of +200 ms latency to warm up, then 60 s of blackhole to a specific S3 endpoint, then 30 s of cooldown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Halt.&lt;/strong&gt; Datadog metric &lt;code&gt;spark.batch.processing.p99 &amp;lt; 30s&lt;/code&gt; (per 1-min tumble).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the Gremlin Scenario JSON, the Terraform resource that manages it, and the Datadog synthetic used as the Halt-on-Breach check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Target selector&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tag:service=spark-executor tag:env=stage&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blast radius&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step 1&lt;/td&gt;
&lt;td&gt;latency +200 ms, 30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step 2&lt;/td&gt;
&lt;td&gt;blackhole s3.us-east-1.amazonaws.com, 60 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step 3&lt;/td&gt;
&lt;td&gt;delay 30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health check&lt;/td&gt;
&lt;td&gt;Datadog: &lt;code&gt;spark.batch.processing.p99 &amp;lt; 30&lt;/code&gt; for 5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Gremlin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Scenario&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/scenarios&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spark-executor-blackhole-stage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Blackhole S3 traffic on 10% of Spark executors; halt on p99 batch time breach."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hypothesis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"If 10% of Spark executors lose S3 connectivity for 60s, batch processing p99 stays under 30s and the driver reprovisions."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"graph"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"nodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InfraAttack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warmup"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"warmup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InfraAttack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"impactDefinition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Latency"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"attackType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latency"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"targetType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Random"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Exact"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spark-executor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InfraAttack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"impactDefinition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Blackhole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"attackType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blackhole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"hostnames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3.us-east-1.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"targetType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Random"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Exact"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spark-executor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cooldown"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cooldown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Delay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"durationSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"healthChecks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hc-spark-batch-p99"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"haltOnUnhealthy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"datadog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"monitorId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12345678&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OK"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Terraform — manages the same scenario as code&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"gremlin_scenario"&lt;/span&gt; &lt;span class="s2"&gt;"spark_blackhole_stage"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"spark-executor-blackhole-stage"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Blackhole S3 traffic on 10% of Spark executors; halt on p99 batch time breach."&lt;/span&gt;
  &lt;span class="nx"&gt;hypothesis&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"If 10% of Spark executors lose S3 connectivity for 60s, batch p99 stays &amp;lt; 30s."&lt;/span&gt;

  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"latency"&lt;/span&gt;
    &lt;span class="nx"&gt;attack_definition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;ms&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
      &lt;span class="nx"&gt;length&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
      &lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
      &lt;span class="nx"&gt;tags&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"spark-executor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"stage"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"blackhole"&lt;/span&gt;
    &lt;span class="nx"&gt;attack_definition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;hostnames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3.us-east-1.amazonaws.com"&lt;/span&gt;
      &lt;span class="nx"&gt;length&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
      &lt;span class="nx"&gt;percent&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
      &lt;span class="nx"&gt;tags&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"spark-executor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"stage"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"delay"&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;duration_seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;health_check&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;provider&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"datadog"&lt;/span&gt;
    &lt;span class="nx"&gt;monitor_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12345678&lt;/span&gt;
    &lt;span class="nx"&gt;halt_on_unhealthy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Datadog monitor definition — the halt criterion&lt;/span&gt;
&lt;span class="c1"&gt;# monitor.yaml&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metric alert&lt;/span&gt;
&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg(last_5m):p99:spark.batch.processing.seconds{env:stage}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;30"&lt;/span&gt;
&lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Spark batch processing p99 breached during chaos experiment.&lt;/span&gt;
  &lt;span class="s"&gt;Gremlin will halt this scenario if this monitor goes OK -&amp;gt; Alert.&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;chaos&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;spark&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;notify_no_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;evaluation_delay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Scenario graph declares three ordered steps: &lt;code&gt;warmup&lt;/code&gt; (latency), &lt;code&gt;main&lt;/code&gt; (blackhole), &lt;code&gt;cooldown&lt;/code&gt; (delay). Gremlin executes them in graph order; the &lt;code&gt;next&lt;/code&gt; pointers describe the DAG.&lt;/li&gt;
&lt;li&gt;Each &lt;code&gt;InfraAttack&lt;/code&gt; node has an &lt;code&gt;impactDefinition&lt;/code&gt; with &lt;code&gt;targetType: Random&lt;/code&gt; and &lt;code&gt;percent: 10&lt;/code&gt;, meaning Gremlin will randomly pick 10% of the clients matching the tag selector &lt;code&gt;service=spark-executor env=stage&lt;/code&gt;. If you have 20 executors, 2 get blackholed.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;blackhole&lt;/code&gt; attack drops all IP traffic from the target host to &lt;code&gt;s3.us-east-1.amazonaws.com&lt;/code&gt; (which resolves to a set of IPs; the attack tracks the resolution). Spark's S3A committer will retry with exponential back-off; after N failed retries, the executor's task fails and the driver reschedules it.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;healthChecks[]&lt;/code&gt; array references a Datadog monitor. Gremlin polls the monitor state every ~30 s; if it transitions from OK to Alert, &lt;code&gt;haltOnUnhealthy: true&lt;/code&gt; causes the scenario to abort — all remaining steps cancelled, active attacks rolled back (blackhole removed, latency removed).&lt;/li&gt;
&lt;li&gt;The Terraform resource wraps the same underlying REST API; checking the resource into Git gives you an audit trail (who bumped the percent from 5 → 10, when, with what justification). The Datadog monitor is defined &lt;em&gt;outside&lt;/em&gt; the Gremlin resource because it's shared with other alerting; the Gremlin scenario only references its ID.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (s)&lt;/th&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Health check&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;warmup start&lt;/td&gt;
&lt;td&gt;OK (batch p99: 8s)&lt;/td&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;main start (blackhole)&lt;/td&gt;
&lt;td&gt;OK (batch p99: 12s)&lt;/td&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;shuffle-fetch failures begin&lt;/td&gt;
&lt;td&gt;OK (batch p99: 22s)&lt;/td&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;driver reprovisioning&lt;/td&gt;
&lt;td&gt;OK (batch p99: 28s)&lt;/td&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;main end; blackhole rolled back&lt;/td&gt;
&lt;td&gt;OK (batch p99: 18s)&lt;/td&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;cooldown end&lt;/td&gt;
&lt;td&gt;OK (batch p99: 10s)&lt;/td&gt;
&lt;td&gt;scenario Passed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For cross-platform (mixed VM + container + K8s) data planes, model chaos as Gremlin Scenarios with explicit &lt;code&gt;percent&lt;/code&gt; blast radius and a Datadog / New Relic Halt-on-Breach Health Check tied to a data SLI. The scenario graph is your chaos-as-code artifact; the halt behaviour is your safety net.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — &lt;code&gt;latency&lt;/code&gt; attack on S3 for Iceberg writes
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A subtler chaos experiment: add 500 ms of latency to all S3 API calls from the Iceberg writer nodes for 5 minutes. Verify the writer doesn't lose commits, doesn't corrupt metadata (Iceberg manifest atomicity), and the p99 commit latency stays within the SLO. Latency chaos surfaces retry storms, connection-pool exhaustion, and back-pressure paths that no functional test hits.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target.&lt;/strong&gt; EC2 instances tagged &lt;code&gt;service=iceberg-writer, env=stage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius.&lt;/strong&gt; 25% of matched clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attack.&lt;/strong&gt; &lt;code&gt;latency&lt;/code&gt; +500 ms to &lt;code&gt;s3.us-east-1.amazonaws.com&lt;/code&gt; and &lt;code&gt;s3.dualstack.us-east-1.amazonaws.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration.&lt;/strong&gt; 5 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Halt.&lt;/strong&gt; Prometheus: &lt;code&gt;iceberg_commit_seconds_p99 &amp;lt; 30&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the Gremlin Scenario, and add a Prometheus-backed Halt-on-Breach using a webhook health check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attack&lt;/td&gt;
&lt;td&gt;latency +500 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destinations&lt;/td&gt;
&lt;td&gt;s3.us-east-1.amazonaws.com, s3.dualstack.us-east-1.amazonaws.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duration&lt;/td&gt;
&lt;td&gt;300 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blast radius&lt;/td&gt;
&lt;td&gt;25% of &lt;code&gt;iceberg-writer&lt;/code&gt; nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Halt&lt;/td&gt;
&lt;td&gt;Prom: &lt;code&gt;iceberg_commit_seconds_p99 &amp;lt; 30&lt;/code&gt; for 3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iceberg-s3-latency-stage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hypothesis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"If S3 gains 500ms of latency on 25% of writer nodes, commit p99 stays under 30s and no manifest corruption occurs."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"graph"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"nodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"attack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InfraAttack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"impactDefinition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Latency"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"attackType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latency"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"hostnames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3.us-east-1.amazonaws.com,s3.dualstack.us-east-1.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"targetType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Random"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Exact"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iceberg-writer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"healthChecks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hc-iceberg-commit-p99"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"haltOnUnhealthy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://chaos-webhooks.internal/gremlin/iceberg-commit-p99"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"expectedStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"pollIntervalSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# chaos-webhooks/iceberg-commit-p99 handler
# Small FastAPI service that turns a PromQL query into a webhook Gremlin can poll.
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/gremlin/iceberg-commit-p99&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;iceberg_commit_p99&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090/api/v1/query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;histogram_quantile(0.99, sum by(le)(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate(iceberg_commit_seconds_bucket{env=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;stage&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}[3m])))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Report unhealthy — Gremlin halts the scenario
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p99 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;gt; 30s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p99_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;latency&lt;/code&gt; attack uses &lt;code&gt;tc netem&lt;/code&gt; on the target host to add 500 ms delay to any packet destined for the listed hostnames. Both the standard and dualstack S3 endpoints are covered so IPv4/IPv6 traffic both see the injected latency.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;targetType: Random, percent: 25&lt;/code&gt; picks a random quarter of the matching hosts. If you have 8 iceberg-writer nodes, 2 get the latency injection; the other 6 write to S3 normally, so the pipeline as a whole should self-balance if writer selection is uniform.&lt;/li&gt;
&lt;li&gt;The webhook health check is Gremlin's escape hatch when your observability platform isn't natively integrated. Any HTTP endpoint that returns 200 for "healthy" and non-200 for "unhealthy" works. Here we wrap a PromQL query into a FastAPI endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pollIntervalSeconds: 30&lt;/code&gt; sets the cadence — Gremlin hits the webhook every 30 s. If the endpoint returns 503, Gremlin halts the scenario. The webhook itself is stateless and can be co-located with any Prometheus-adjacent service.&lt;/li&gt;
&lt;li&gt;The 5-minute duration is chosen because Iceberg commit p99 is a lagging indicator — you need a few commit cycles for the latency to propagate into the SLI. Shorter windows may pass with false-negative "everything's fine" verdicts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (min)&lt;/th&gt;
&lt;th&gt;Attack&lt;/th&gt;
&lt;th&gt;webhook status&lt;/th&gt;
&lt;th&gt;Iceberg p99 (s)&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;applied&lt;/td&gt;
&lt;td&gt;200 (2.1)&lt;/td&gt;
&lt;td&gt;2.1&lt;/td&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~2 writers slow&lt;/td&gt;
&lt;td&gt;200 (6.4)&lt;/td&gt;
&lt;td&gt;6.4&lt;/td&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;retries exhaust pool&lt;/td&gt;
&lt;td&gt;200 (14.9)&lt;/td&gt;
&lt;td&gt;14.9&lt;/td&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;commit p99 spike&lt;/td&gt;
&lt;td&gt;200 (22.3)&lt;/td&gt;
&lt;td&gt;22.3&lt;/td&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;reprovisioning&lt;/td&gt;
&lt;td&gt;200 (18.7)&lt;/td&gt;
&lt;td&gt;18.7&lt;/td&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;attack ended&lt;/td&gt;
&lt;td&gt;200 (7.2)&lt;/td&gt;
&lt;td&gt;7.2&lt;/td&gt;
&lt;td&gt;Passed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For every external dependency (S3, DynamoDB, Snowflake, external HTTP) inject a latency chaos experiment. Latency chaos is the single highest-yield chaos primitive — it surfaces retry storms, connection-pool exhaustion, timeout misconfigurations, and back-pressure paths that no functional test finds.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the blast-radius graduation matrix
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Every Gremlin scenario is a candidate for graduation from dev → staging → 1% prod → 10% prod → 100% prod. The graduation matrix is the artifact that codifies the promotion rules: how many green runs at each stage before the next, who approves, what the rollback plan is. Walk through the matrix for the Kafka broker kill scenario.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stage 1 (dev).&lt;/strong&gt; Nightly automation, no on-call, no approval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage 2 (staging).&lt;/strong&gt; Weekly automation, tag-approved by data platform SRE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage 3 (prod-1pct).&lt;/strong&gt; Monthly ceremony, game-day format, senior SRE + platform lead required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage 4 (prod-10pct).&lt;/strong&gt; Quarterly, executive sponsor + full on-call bridge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage 5 (prod-100pct).&lt;/strong&gt; Reserved for annual DR drills; formal DR runbook + customer comms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Codify the graduation matrix as a decision table, and derive the CI check that gates promotion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;th&gt;Approvals&lt;/th&gt;
&lt;th&gt;Green runs required to promote&lt;/th&gt;
&lt;th&gt;Rollback plan&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dev&lt;/td&gt;
&lt;td&gt;nightly&lt;/td&gt;
&lt;td&gt;automation&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;halt-on-breach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;staging&lt;/td&gt;
&lt;td&gt;weekly&lt;/td&gt;
&lt;td&gt;1 SRE&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;halt-on-breach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prod-1pct&lt;/td&gt;
&lt;td&gt;monthly&lt;/td&gt;
&lt;td&gt;2 SREs + lead&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;halt + on-call bridge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prod-10pct&lt;/td&gt;
&lt;td&gt;quarterly&lt;/td&gt;
&lt;td&gt;lead + exec&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;halt + on-call bridge + customer comms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prod-100pct&lt;/td&gt;
&lt;td&gt;annual (DR drill)&lt;/td&gt;
&lt;td&gt;DR sponsor&lt;/td&gt;
&lt;td&gt;N/A (single ceremony)&lt;/td&gt;
&lt;td&gt;formal DR runbook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ci/gate_chaos_promotion.py — enforces the graduation matrix on PR merge
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;MATRIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required_green&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prev_stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staging&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required_green&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prev_stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-1pct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required_green&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prev_stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staging&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-10pct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required_green&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prev_stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-1pct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-100pct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required_green&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prev_stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-10pct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gremlin_recent_runs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag_env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.gremlin.com/v1/scenarios/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scenario_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/runs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag:env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tag_env&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                     &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;can_promote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_stage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_stage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;prev_required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MATRIX&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;to_stage&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required_green&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gremlin_recent_runs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_stage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prev_required&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;greens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Successful&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;prev_required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Only &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; green runs at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;from_stage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; need &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prev_required&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; green runs at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;from_stage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--scenario-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--to-stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MATRIX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MATRIX&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_stage&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prev_stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stage has no prerequisite; auto-allow.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;can_promote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scenario_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_stage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;MATRIX&lt;/code&gt; codifies "how many green runs at stage N do you need before you can promote to stage N+1." Dev requires 5 green runs (nightly for a week); staging requires 3; prod-1pct requires 3; prod-10pct requires 2; prod-100pct is a single-ceremony DR drill.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gremlin_recent_runs&lt;/code&gt; calls the Gremlin API to fetch the last N runs of a scenario tagged with the previous stage's environment. The tag is your foreign key between the CI check and the historical run data.&lt;/li&gt;
&lt;li&gt;The CI script is invoked from your PR pipeline: &lt;code&gt;gate_chaos_promotion.py --scenario-id ... --to-stage prod-1pct&lt;/code&gt;. If insufficient green runs exist at the previous stage, the CI check fails and the PR cannot merge.&lt;/li&gt;
&lt;li&gt;The design deliberately reads run history from Gremlin (source of truth) rather than tracking green-run counts in the repo. This prevents the "someone updated the counter without an actual run" failure mode.&lt;/li&gt;
&lt;li&gt;Executive approval for prod-10pct is enforced via GitHub CODEOWNERS + branch protection — the &lt;code&gt;platform/*&lt;/code&gt; codeowner (VP Data Platform) must approve any PR touching &lt;code&gt;overlays/prod-10pct/&lt;/code&gt;. The matrix is codified in both machine-checkable (CI) and human-checkable (CODEOWNERS) forms.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PR event&lt;/th&gt;
&lt;th&gt;Stage promotion&lt;/th&gt;
&lt;th&gt;CI verdict&lt;/th&gt;
&lt;th&gt;Human approval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create engine&lt;/td&gt;
&lt;td&gt;dev&lt;/td&gt;
&lt;td&gt;auto-allow&lt;/td&gt;
&lt;td&gt;1 SRE reviewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promote dev → staging&lt;/td&gt;
&lt;td&gt;staging&lt;/td&gt;
&lt;td&gt;required: 5 green dev runs&lt;/td&gt;
&lt;td&gt;1 SRE reviewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promote staging → prod-1pct&lt;/td&gt;
&lt;td&gt;prod-1pct&lt;/td&gt;
&lt;td&gt;required: 3 green staging runs&lt;/td&gt;
&lt;td&gt;2 SREs + team lead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promote prod-1pct → prod-10pct&lt;/td&gt;
&lt;td&gt;prod-10pct&lt;/td&gt;
&lt;td&gt;required: 3 green prod-1pct runs&lt;/td&gt;
&lt;td&gt;team lead + exec sponsor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promote to prod-100pct&lt;/td&gt;
&lt;td&gt;prod-100pct (DR)&lt;/td&gt;
&lt;td&gt;required: 2 green prod-10pct runs&lt;/td&gt;
&lt;td&gt;DR sponsor + formal runbook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every chaos scenario in production had to earn its way there by passing N green runs at each prior stage. Codify the graduation matrix in both CI (machine-checkable) and CODEOWNERS (human-checkable). The matrix is the artifact your auditor will ask to see.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on Gremlin
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're bringing chaos engineering to a multi-cloud data platform (Kafka on-prem, Airflow on EKS, Spark on Databricks, Snowflake). Design the Gremlin-based chaos program: which primitives, which scenarios, which halt criteria, and how you manage the tag hierarchy so on-call knows what's being attacked at any moment."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a tag-hierarchy-driven scenario library with SIEM-integrated audit trail
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Tag&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hierarchy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;every&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;gremlind&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;agent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;inherits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;these&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tags&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;gremlind&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/etc/gremlin/config.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"team"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data-platform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cluster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kafka-onprem-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kafka-broker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"criticality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tier-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chaos-eligible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chaos-tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1pct"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. Terraform module — reusable scenario definitions per stack&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"kafka_broker_kill_scenarios"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/gremlin-broker-kill"&lt;/span&gt;

  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"kafka-onprem-1-stage"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="s2"&gt;"kafka-onprem-1-1pct"&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chaos_tier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1pct"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="s2"&gt;"kafka-onprem-1-10pct"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chaos_tier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10pct"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;scenario_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;
  &lt;span class="nx"&gt;env&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;
  &lt;span class="nx"&gt;percent&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;
  &lt;span class="nx"&gt;chaos_tier&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;try&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chaos_tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;health_check_monitor_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datadog_monitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kafka_lag_slo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;halt_on_unhealthy&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 3. SIEM exporter — mirror every Gremlin attack to Splunk for audit
# runs as a scheduled job every 5 minutes
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="n"&gt;GREMLIN_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GREMLIN_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;SPLUNK_HEC&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SPLUNK_HEC_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;SPLUNK_HEC_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SPLUNK_HEC_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;poll_gremlin_since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since_iso&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.gremlin.com/v1/attacks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;since&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;since_iso&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                     &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GREMLIN_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attacks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_splunk_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sourcetype&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gremlin:attack&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attack_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scenario&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scenarioId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;createdBy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attack_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;targets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;targets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;started_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ended_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;endedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;halted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;halted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;push_to_splunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPLUNK_HEC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Splunk &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SPLUNK_HEC_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                      &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;attacks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;poll_gremlin_since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;push_to_splunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;to_splunk_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;attacks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agents&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gremlind&lt;/code&gt; on every VM/container&lt;/td&gt;
&lt;td&gt;inherit tag hierarchy; execute attack; report health&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control plane&lt;/td&gt;
&lt;td&gt;Gremlin SaaS&lt;/td&gt;
&lt;td&gt;scenario execution, blast-radius, halt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scenario definitions&lt;/td&gt;
&lt;td&gt;Terraform module&lt;/td&gt;
&lt;td&gt;one module per fault type; per-env instances via for_each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health checks&lt;/td&gt;
&lt;td&gt;Datadog monitors&lt;/td&gt;
&lt;td&gt;data SLI thresholds; Gremlin polls these&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Halt behaviour&lt;/td&gt;
&lt;td&gt;Gremlin &lt;code&gt;halt_on_unhealthy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Datadog Alert → scenario abort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit trail&lt;/td&gt;
&lt;td&gt;SIEM exporter → Splunk&lt;/td&gt;
&lt;td&gt;every attack ID, user, targets, timing mirrored to SIEM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval matrix&lt;/td&gt;
&lt;td&gt;GitHub CODEOWNERS + CI&lt;/td&gt;
&lt;td&gt;machine-checked green-run count + human sign-off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-call visibility&lt;/td&gt;
&lt;td&gt;Slack &lt;code&gt;#chaos-live&lt;/code&gt; channel&lt;/td&gt;
&lt;td&gt;scenario start / end / halt posts via Gremlin webhook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, every chaos attack against the data platform is: (a) defined in Terraform (audit trail), (b) executed via the Gremlin API (blast-radius enforced), (c) gated by Datadog Halt-on-Breach (data-SLO seatbelt), (d) mirrored to Splunk (compliance receipt), (e) announced to &lt;code&gt;#chaos-live&lt;/code&gt; (operator awareness). On-call always knows what's running; auditors always have the receipt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terraform state&lt;/td&gt;
&lt;td&gt;scenario definitions&lt;/td&gt;
&lt;td&gt;infra reviewer / audit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gremlin control plane&lt;/td&gt;
&lt;td&gt;live scenario execution&lt;/td&gt;
&lt;td&gt;data-platform SRE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Datadog monitor&lt;/td&gt;
&lt;td&gt;halt criterion&lt;/td&gt;
&lt;td&gt;Gremlin health-check poller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Splunk &lt;code&gt;gremlin:attack&lt;/code&gt; events&lt;/td&gt;
&lt;td&gt;audit trail&lt;/td&gt;
&lt;td&gt;compliance / SOC-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack &lt;code&gt;#chaos-live&lt;/code&gt; post&lt;/td&gt;
&lt;td&gt;live operator awareness&lt;/td&gt;
&lt;td&gt;on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-mortem doc (Confluence)&lt;/td&gt;
&lt;td&gt;scenario outcome + backlog&lt;/td&gt;
&lt;td&gt;data-platform team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Tag hierarchy on every agent&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;service&lt;/code&gt; / &lt;code&gt;env&lt;/code&gt; / &lt;code&gt;criticality&lt;/code&gt; / &lt;code&gt;chaos-tier&lt;/code&gt; tags define the selector language for every scenario. A well-designed tag hierarchy makes "blast radius = 10% of &lt;code&gt;service=kafka-broker&lt;/code&gt; in &lt;code&gt;env=prod&lt;/code&gt; &lt;code&gt;chaos-tier=1pct&lt;/code&gt;" a one-line selector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Terraform module per fault type&lt;/strong&gt;&lt;/strong&gt; — a Terraform module encodes the fault + its parameters + its halt criterion. &lt;code&gt;for_each&lt;/code&gt; over &lt;code&gt;{stage, 1pct, 10pct}&lt;/code&gt; instantiates the same scenario at each blast tier with the right selectors. Chaos-as-code becomes chaos-in-Terraform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Datadog Halt-on-Breach&lt;/strong&gt;&lt;/strong&gt; — the halt criterion is a Datadog monitor, not a Gremlin-internal check. This decouples the SLI definition from the chaos tool — the same SLI monitor pages on-call &lt;em&gt;and&lt;/em&gt; halts chaos. One source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;SIEM audit trail&lt;/strong&gt;&lt;/strong&gt; — every attack ID + user + targets + timing mirrors to Splunk. When a security auditor asks "who ran chaos in prod on 2026-07-30 at 14:00 and what did it hit," you point at a Splunk search. This is what makes prod chaos defensible in a SOC-2 environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Slack live channel&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;#chaos-live&lt;/code&gt; gets a post at scenario start (with hypothesis + expected duration + rollback path), during halts, and at scenario end (with verdict). Operators can silence chaos-triggered alerts because they know a chaos window is active.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CI-gated promotion&lt;/strong&gt;&lt;/strong&gt; — the same graduation-matrix pattern from LitmusChaos applies: dev → staging → 1% prod → 10% → 100%. CI counts green runs; CODEOWNERS enforces human approval. Chaos in prod is earned, not granted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — Gremlin SaaS licence (per-agent per-month), gremlind footprint (~50 MB per host), one Terraform module per fault type, one Datadog monitor per SLI, one Splunk index for chaos events. Compared to the DIY equivalent (write attack primitives, build a control plane, build audit, build halt), Gremlin trades money for time-to-value. O(1) per attack; O(N scenarios × N stages) per pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on multi-cloud data platform resilience&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;API&lt;/span&gt;
&lt;span&gt;Topic — api-integration&lt;/span&gt;
&lt;strong&gt;API integration problems on webhook and halt endpoints&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/api-integration" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Data-plane fault injection — Kafka broker kill, Airflow scheduler kill, Spark executor loss
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Stateful chaos — the three canonical data-plane experiments and their guardrails
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;data fault injection&lt;/code&gt; is the practice of applying chaos primitives (pod-delete, network-loss, blackhole, latency) to the &lt;em&gt;stateful&lt;/em&gt; data systems — Kafka brokers with partition leadership + ISR + transactional coordinators, Airflow schedulers with sole write-access to a metadata DB, and Spark executors with in-memory shuffle blocks and driver-tracked lineage — where the correctness contract binds the experiment far more tightly than for stateless HTTP services&lt;/strong&gt;. Every senior data platform team runs these three experiments; the difference between amateur and senior is whether the experiment ships with the specific data-system guardrail that makes the chaos survivable — &lt;code&gt;min.insync.replicas=2&lt;/code&gt; + &lt;code&gt;acks=all&lt;/code&gt; for Kafka, &lt;code&gt;catchup=False&lt;/code&gt; + &lt;code&gt;max_active_runs=1&lt;/code&gt; for Airflow, &lt;code&gt;dynamicAllocation.enabled=true&lt;/code&gt; + &lt;code&gt;blacklist.enabled=true&lt;/code&gt; for Spark.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6fp2gxlkky9u81vklr3y.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6fp2gxlkky9u81vklr3y.jpeg" alt="Iconographic data-plane chaos diagram — three side-by-side boxes labelled Kafka broker, Airflow scheduler, Spark executor, each with a lightning-bolt strike glyph and an SLO-guard chip below." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The three canonical data-plane chaos experiments.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kafka broker kill.&lt;/strong&gt; Kill a broker; verify the pipeline stays inside its freshness + lag SLO. Failure modes: unacked writes if &lt;code&gt;acks!=all&lt;/code&gt;, ISR shrink storms, coordinator loss (transactional-EOS blast). Guardrails: &lt;code&gt;min.insync.replicas=2&lt;/code&gt;, &lt;code&gt;acks=all&lt;/code&gt;, &lt;code&gt;producer.idempotence=true&lt;/code&gt;, &lt;code&gt;retention.ms&lt;/code&gt; sized to slot lag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airflow scheduler kill.&lt;/strong&gt; Kill the scheduler mid-DAG; verify running tasks continue and new tasks are scheduled after the restart. Failure modes: mid-parse crashes leave &lt;code&gt;dag_run&lt;/code&gt; in &lt;code&gt;queued&lt;/code&gt; forever, &lt;code&gt;task_instance&lt;/code&gt; rows stuck in &lt;code&gt;running&lt;/code&gt; when the executor is a Celery worker, catchup floods the queue on restart. Guardrails: &lt;code&gt;catchup=False&lt;/code&gt;, &lt;code&gt;max_active_runs=1&lt;/code&gt;, &lt;code&gt;retries&amp;gt;=2&lt;/code&gt;, &lt;code&gt;dagrun_timeout&lt;/code&gt; set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spark executor loss.&lt;/strong&gt; Kill a Spark executor; verify the driver reprovisions and the batch/streaming job continues. Failure modes: shuffle-fetch failures cascade into stage restarts, driver OOM if too many executors lost, dynamic-allocation flap. Guardrails: &lt;code&gt;spark.dynamicAllocation.enabled=true&lt;/code&gt;, &lt;code&gt;spark.blacklist.enabled=true&lt;/code&gt;, &lt;code&gt;spark.task.maxFailures=4&lt;/code&gt;, &lt;code&gt;spark.stage.maxConsecutiveAttempts=4&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The stateful chaos multiplier — why data-plane chaos is different.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-flight state.&lt;/strong&gt; A stateless HTTP service can be killed mid-request and the client just retries. A Kafka broker killed mid-transaction leaves the coordinator's state machine mid-way; recovery requires either a coordinator failover or a transactional abort. The chaos experiment must know which.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ordering contracts.&lt;/strong&gt; Kafka partitions guarantee in-partition order; a broker kill preserves it. Spark shuffle blocks are not ordered but they are addressed by (mapId, reduceId); executor loss forces the driver to re-fetch from surviving executors, which the shuffle service handles transparently &lt;em&gt;if configured&lt;/em&gt;. Airflow DAG runs are logical ordering only; the scheduler kill risks orphaning a mid-run &lt;code&gt;task_instance&lt;/code&gt; if the executor is out-of-process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLI selection.&lt;/strong&gt; For each experiment the correct SLI differs. Kafka: consumer group lag + producer error rate. Airflow: DAG success rate + scheduler heartbeat age. Spark: batch processing time p99 + stage retry count. Getting the SLI wrong makes the experiment blind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery topology.&lt;/strong&gt; Kafka has a metadata quorum (ZooKeeper or KRaft) and a partition leader per topic-partition — recovery is per-partition. Airflow has a single scheduler + N executors — recovery is scheduler-restart-plus-heartbeat. Spark has a driver + N executors — recovery is executor-reprovision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The three guardrails you cannot ship without.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kafka:&lt;/strong&gt; &lt;code&gt;acks=all&lt;/code&gt; on the producer + &lt;code&gt;min.insync.replicas=2&lt;/code&gt; on the topic. Without both, a broker kill can lose an unacknowledged write. This is the single most-cited Kafka correctness invariant in interviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airflow:&lt;/strong&gt; &lt;code&gt;catchup=False&lt;/code&gt; on the DAG + &lt;code&gt;max_active_runs=1&lt;/code&gt;. Without both, a scheduler restart after a downtime replays every missed logical date and can OOM the queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spark:&lt;/strong&gt; &lt;code&gt;spark.dynamicAllocation.enabled=true&lt;/code&gt; + &lt;code&gt;spark.blacklist.enabled=true&lt;/code&gt;. Without both, an executor loss either leaves the cluster under-provisioned or repeatedly retries onto the same bad node.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on data-plane chaos.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you verify a Kafka broker kill didn't lose writes?" — required answer: &lt;code&gt;acks=all&lt;/code&gt; + &lt;code&gt;min.insync.replicas=2&lt;/code&gt; + producer-error-rate SLI + downstream row-count reconciliation.&lt;/li&gt;
&lt;li&gt;"What breaks when you kill an Airflow scheduler mid-DAG?" — orphaned &lt;code&gt;task_instance&lt;/code&gt; rows in &lt;code&gt;running&lt;/code&gt; state if executor is separate; &lt;code&gt;dag_run&lt;/code&gt; stuck in &lt;code&gt;queued&lt;/code&gt; if the parse loop crashed.&lt;/li&gt;
&lt;li&gt;"What's the Spark executor-loss failure mode?" — shuffle-fetch failures on downstream stages; without &lt;code&gt;dynamicAllocation&lt;/code&gt;, the cluster shrinks and the job hangs.&lt;/li&gt;
&lt;li&gt;"How do you pick which broker to kill?" — random over &lt;code&gt;applabel=kafka-broker&lt;/code&gt; avoiding the controller; the controller loss is a separate experiment (&lt;code&gt;controller-kill&lt;/code&gt;) because it exercises different code paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — Kafka broker kill with &lt;code&gt;min.insync.replicas&lt;/code&gt; guard
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Kafka data-plane chaos experiment: kill one broker in a 6-broker cluster (&lt;code&gt;replication.factor=3&lt;/code&gt;, &lt;code&gt;min.insync.replicas=2&lt;/code&gt;) and verify no committed messages are lost and the producer's &lt;code&gt;record-error-rate&lt;/code&gt; stays at zero. This is the interview go-to; every senior Kafka engineer has run it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cluster.&lt;/strong&gt; 6 brokers, &lt;code&gt;replication.factor=3&lt;/code&gt;, &lt;code&gt;min.insync.replicas=2&lt;/code&gt;, KRaft-mode metadata quorum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Producer.&lt;/strong&gt; &lt;code&gt;acks=all&lt;/code&gt;, &lt;code&gt;enable.idempotence=true&lt;/code&gt;, &lt;code&gt;retries=Integer.MAX_VALUE&lt;/code&gt;, &lt;code&gt;max.in.flight.requests.per.connection=5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault.&lt;/strong&gt; LitmusChaos &lt;code&gt;pod-delete&lt;/code&gt; on 1 of 6 brokers (17% blast radius).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails.&lt;/strong&gt; promProbe on &lt;code&gt;kafka_consumergroup_lag &amp;lt; 100000&lt;/code&gt; + promProbe on &lt;code&gt;rate(kafka_producer_record_error_total[1m]) &amp;lt; 0.1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the ChaosEngine + the producer / topic config that makes this experiment survivable, and quantify what happens if you drop the &lt;code&gt;min.insync.replicas&lt;/code&gt; guard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;With guard&lt;/th&gt;
&lt;th&gt;Without guard&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;replication.factor&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;min.insync.replicas&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;acks (producer)&lt;/td&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;enable.idempotence&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker kill outcome&lt;/td&gt;
&lt;td&gt;0 lost writes&lt;/td&gt;
&lt;td&gt;~250 lost writes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. Topic configuration — enforced by kafka-topics.sh at creation&lt;/span&gt;
&lt;span class="c1"&gt;# kafka-topics.sh --create \&lt;/span&gt;
&lt;span class="c1"&gt;#   --topic events \&lt;/span&gt;
&lt;span class="c1"&gt;#   --partitions 24 \&lt;/span&gt;
&lt;span class="c1"&gt;#   --replication-factor 3 \&lt;/span&gt;
&lt;span class="c1"&gt;#   --config min.insync.replicas=2 \&lt;/span&gt;
&lt;span class="c1"&gt;#   --config unclean.leader.election.enable=false \&lt;/span&gt;
&lt;span class="c1"&gt;#   --bootstrap-server localhost:9092&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 2. Producer configuration — the two lines that make the chaos survivable&lt;/span&gt;
&lt;span class="nc"&gt;Properties&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Properties&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bootstrap.servers"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"kafka-0:9092,kafka-1:9092,kafka-2:9092"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"acks"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                                 &lt;span class="s"&gt;"all"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"enable.idempotence"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                   &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"retries"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                              &lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MAX_VALUE&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"max.in.flight.requests.per.connection"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"delivery.timeout.ms"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                  &lt;span class="s"&gt;"120000"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"request.timeout.ms"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                    &lt;span class="s"&gt;"30000"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"linger.ms"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                                &lt;span class="s"&gt;"20"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"compression.type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                      &lt;span class="s"&gt;"lz4"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 3. ChaosEngine — kill 1 of 6 brokers with two data-SLI probes&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosEngine&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka-broker-kill&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engineState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active'&lt;/span&gt;
  &lt;span class="na"&gt;chaosServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;appinfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;appns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kafka'&lt;/span&gt;
    &lt;span class="na"&gt;applabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=kafka-broker,role!=controller'&lt;/span&gt;
    &lt;span class="na"&gt;appkind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;statefulset'&lt;/span&gt;
  &lt;span class="na"&gt;experiments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;120'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;CHAOS_INTERVAL&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;60'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PODS_AFFECTED_PERC&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;FORCE&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;consumer-lag-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max(kafka_consumergroup_lag{group="warehouse-sink"})'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;100000'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;producer-error-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sum(rate(kafka_producer_record_error_total[1m]))'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;float&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.1'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;isr-integrity-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;15&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;min(kafka_topic_partition_in_sync_replica{topic="events"})'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;='&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The topic is created with &lt;code&gt;replication.factor=3, min.insync.replicas=2, unclean.leader.election.enable=false&lt;/code&gt;. When a broker holding a partition leader dies, controller elects a new leader from the ISR; if the ISR has &amp;lt; 2 in-sync replicas the producer receives &lt;code&gt;NotEnoughReplicasException&lt;/code&gt; and retries. With unclean-leader-election disabled, no out-of-sync replica is ever elected, so no committed writes vanish.&lt;/li&gt;
&lt;li&gt;The producer sets &lt;code&gt;acks=all&lt;/code&gt; (waits for the leader + all in-sync replicas to acknowledge) and &lt;code&gt;enable.idempotence=true&lt;/code&gt; (attaches a producer-id + sequence, so retries don't duplicate). Together these give exactly-once-per-producer semantics — the exact property the chaos experiment verifies.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;applabel: 'app=kafka-broker,role!=controller'&lt;/code&gt; explicitly excludes the controller pod. Controller loss is a &lt;em&gt;different&lt;/em&gt; chaos experiment because the controller drives partition-leader election — testing controller loss requires different halt criteria (controller-fail-over time SLO instead of consumer lag).&lt;/li&gt;
&lt;li&gt;The three probes cover three orthogonal SLIs: (a) consumer lag stays bounded (freshness); (b) producer error rate stays low (durability); (c) minimum ISR stays &amp;gt;= 2 (correctness contract). If any breaches, the experiment aborts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FORCE: 'false'&lt;/code&gt; means the broker gets a graceful shutdown (SIGTERM → configured shutdown.timeout.ms → SIGKILL). Kafka brokers flush their memory-mapped log segments on graceful shutdown; a forced kill would test crash-recovery instead. Both are valuable; label them as different experiments.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (s)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Consumer lag&lt;/th&gt;
&lt;th&gt;Producer error rate&lt;/th&gt;
&lt;th&gt;Min ISR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;broker-3 alive&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;broker-3 SIGTERM&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;broker-3 gone; ISR shrinks&lt;/td&gt;
&lt;td&gt;1400&lt;/td&gt;
&lt;td&gt;0.02&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;new leaders elected&lt;/td&gt;
&lt;td&gt;4200&lt;/td&gt;
&lt;td&gt;0.01&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;broker-3 restarted&lt;/td&gt;
&lt;td&gt;12000&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;ISR healing&lt;/td&gt;
&lt;td&gt;8000&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;full ISR restored&lt;/td&gt;
&lt;td&gt;3200&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Kafka broker chaos without &lt;code&gt;acks=all&lt;/code&gt; + &lt;code&gt;min.insync.replicas=2&lt;/code&gt; is theatre — you're not testing the resilient system, you're testing an unsafe one. Enforce both at the topic + producer config level &lt;em&gt;before&lt;/em&gt; the chaos experiment ships. Add an &lt;code&gt;isr-integrity-guard&lt;/code&gt; probe to make the min-ISR contract auditable during chaos.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Airflow scheduler kill with &lt;code&gt;catchup=False&lt;/code&gt; guard
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Airflow chaos experiment: kill the scheduler pod while a DAG has tasks in-flight, verify running tasks complete (executor is Celery or Kubernetes, out-of-process), verify the scheduler restarts within &lt;code&gt;HEALTHCHECK_MAX_AGE&lt;/code&gt; and doesn't flood the queue with catchup runs on restart, verify no &lt;code&gt;task_instance&lt;/code&gt; rows are stuck in &lt;code&gt;running&lt;/code&gt; state after the scheduler is healthy again.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setup.&lt;/strong&gt; Airflow 2.9 with KubernetesExecutor, scheduler HA disabled (single scheduler), &lt;code&gt;HEALTHCHECK_MAX_AGE=30s&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DAG.&lt;/strong&gt; Hourly ETL with &lt;code&gt;catchup=False&lt;/code&gt;, &lt;code&gt;max_active_runs=1&lt;/code&gt;, &lt;code&gt;retries=2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault.&lt;/strong&gt; &lt;code&gt;pod-delete&lt;/code&gt; on scheduler pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails.&lt;/strong&gt; &lt;code&gt;k8sProbe&lt;/code&gt; on scheduler Deployment + &lt;code&gt;cmdProbe&lt;/code&gt; on &lt;code&gt;select count(*) from task_instance where state='running' and queued_dttm &amp;lt; now() - interval '10 minutes'&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the ChaosEngine and demonstrate what breaks if &lt;code&gt;catchup=True&lt;/code&gt; is misconfigured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;With guard&lt;/th&gt;
&lt;th&gt;Without guard&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;catchup&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max_active_runs&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;unbounded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler downtime&lt;/td&gt;
&lt;td&gt;30 s&lt;/td&gt;
&lt;td&gt;30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-restart runs&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~24 (one per missed hour × 24 hrs)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The DAG with the correct guardrails
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.decorators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hourly_etl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@hourly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;# &amp;lt;-- guard 1: don't backfill missed runs
&lt;/span&gt;    &lt;span class="n"&gt;max_active_runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;# &amp;lt;-- guard 2: never more than one instance
&lt;/span&gt;    &lt;span class="n"&gt;dagrun_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry_delay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execution_timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosEngine&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow-scheduler-kill&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engineState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active'&lt;/span&gt;
  &lt;span class="na"&gt;chaosServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;appinfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;appns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;airflow'&lt;/span&gt;
    &lt;span class="na"&gt;applabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;component=scheduler'&lt;/span&gt;
    &lt;span class="na"&gt;appkind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deployment'&lt;/span&gt;
  &lt;span class="na"&gt;experiments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;90'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;CHAOS_INTERVAL&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;45'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PODS_AFFECTED_PERC&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;100'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;FORCE&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scheduler-restart-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;k8sProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EOT&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;30&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;k8sProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apps'&lt;/span&gt;
              &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v1'&lt;/span&gt;
              &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deployments'&lt;/span&gt;
              &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;airflow'&lt;/span&gt;
              &lt;span class="na"&gt;fieldSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metadata.name=airflow-scheduler'&lt;/span&gt;
              &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;present'&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;no-stuck-tasks&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cmdProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EOT&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;30&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;cmdProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                &lt;span class="s"&gt;psql -h postgres-airflow.airflow.svc -U airflow -d airflow -tAc \&lt;/span&gt;
                  &lt;span class="s"&gt;"SELECT COUNT(*) FROM task_instance&lt;/span&gt;
                   &lt;span class="s"&gt;WHERE  state='running'&lt;/span&gt;
                     &lt;span class="s"&gt;AND  queued_dttm &amp;lt; NOW() - INTERVAL '10 minutes'"&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;='&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;postgres:16-alpine'&lt;/span&gt;
                &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
                &lt;span class="na"&gt;inheritInputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;no-catchup-flood&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EOT&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;airflow_dagrun_active_count{dag_id="hourly_etl"}'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;='&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PODS_AFFECTED_PERC: 100&lt;/code&gt; on a Deployment with 1 replica means the entire scheduler dies. Kubernetes will reschedule the pod immediately; the scheduler restarts, re-parses DAGs, resumes its scheduling loop. Executor tasks (Kubernetes pods) continue running because they're separate pods; the scheduler only manages state transitions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;catchup=False&lt;/code&gt; is the load-bearing invariant. On restart, the scheduler asks "what's the last logical date I scheduled for this DAG?" Without catchup, it schedules only the &lt;em&gt;next&lt;/em&gt; logical date. With catchup=True and 24h of downtime, it would try to schedule 24 backfill runs immediately, potentially overwhelming the executor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_active_runs=1&lt;/code&gt; is the second guard: even if the scheduler somehow tries to schedule multiple runs, only 1 is allowed active. This bounds the blast radius of a scheduler bug.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;EOT cmdProbe&lt;/code&gt; runs SQL against the metadata DB after chaos ends: "any task_instances stuck in 'running' state where their queue time is &amp;gt; 10 minutes ago?" If the scheduler died mid-transition on a &lt;code&gt;task_instance&lt;/code&gt; (e.g. between &lt;code&gt;queued&lt;/code&gt; and &lt;code&gt;running&lt;/code&gt;), the row can be orphaned. The probe catches this.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;EOT promProbe&lt;/code&gt; on &lt;code&gt;airflow_dagrun_active_count &amp;lt;= 1&lt;/code&gt; verifies no catchup flood. If it fires, either the DAG has &lt;code&gt;catchup=True&lt;/code&gt; (config bug) or the scheduler restart triggered multiple runs (Airflow bug).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (s)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Scheduler ready&lt;/th&gt;
&lt;th&gt;Stuck tasks&lt;/th&gt;
&lt;th&gt;Active DAG runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Chaos applied&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Scheduler pod killed&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 (in-flight preserved)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Pod restarted; parsing DAGs&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;Scheduler resuming loop&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;Chaos window ends&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EOT&lt;/td&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any Airflow chaos experiment, verify the DAG has &lt;code&gt;catchup=False&lt;/code&gt; + &lt;code&gt;max_active_runs=1&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; running the experiment. Ship the two-line SQL probe that hunts orphaned &lt;code&gt;task_instance&lt;/code&gt; rows as an EOT guard — it's the single most under-instrumented Airflow failure mode.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Spark executor loss with dynamic allocation
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Spark chaos experiment: kill 2 of 10 Spark executors mid-batch, verify the driver reprovisions replacements (dynamic allocation), verify shuffle-fetch failures don't cascade into unbounded stage retries, verify the job SLA (batch processing p99) stays under threshold. This tests both Spark's own fault tolerance and the surrounding YARN / K8s resource-manager's ability to reprovision.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cluster.&lt;/strong&gt; Spark 3.5 on K8s, 1 driver + 10 executor pods, &lt;code&gt;spark.dynamicAllocation.enabled=true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guards.&lt;/strong&gt; &lt;code&gt;spark.blacklist.enabled=true&lt;/code&gt;, &lt;code&gt;spark.task.maxFailures=4&lt;/code&gt;, &lt;code&gt;spark.stage.maxConsecutiveAttempts=4&lt;/code&gt;, &lt;code&gt;spark.dynamicAllocation.minExecutors=8&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault.&lt;/strong&gt; LitmusChaos &lt;code&gt;pod-delete&lt;/code&gt; on 20% of executors (2 of 10).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Halt.&lt;/strong&gt; &lt;code&gt;spark_streaming_batch_processing_seconds_p99 &amp;lt; 30&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the ChaosEngine and the Spark submit config that makes executor loss survivable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;spark.dynamicAllocation.enabled&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.dynamicAllocation.minExecutors&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.dynamicAllocation.maxExecutors&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.blacklist.enabled&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.task.maxFailures&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.stage.maxConsecutiveAttempts&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.shuffle.service.enabled&lt;/td&gt;
&lt;td&gt;true (external shuffle service)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Spark submit config that survives executor loss&lt;/span&gt;
spark-submit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--master&lt;/span&gt; k8s://https://kubernetes.default.svc &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--deploy-mode&lt;/span&gt; cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.executor.instances&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.dynamicAllocation.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.dynamicAllocation.minExecutors&lt;span class="o"&gt;=&lt;/span&gt;8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.dynamicAllocation.maxExecutors&lt;span class="o"&gt;=&lt;/span&gt;20 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.dynamicAllocation.executorIdleTimeout&lt;span class="o"&gt;=&lt;/span&gt;60s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.blacklist.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.blacklist.timeout&lt;span class="o"&gt;=&lt;/span&gt;1h &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.task.maxFailures&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.stage.maxConsecutiveAttempts&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.shuffle.service.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.kubernetes.executor.label.app&lt;span class="o"&gt;=&lt;/span&gt;spark-executor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.kubernetes.executor.deleteOnTermination&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.streaming.stopGracefullyOnShutdown&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf&lt;/span&gt; spark.sql.streaming.gracefulShutdown.timeout&lt;span class="o"&gt;=&lt;/span&gt;60s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--class&lt;/span&gt; com.acme.EventStreamJob &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;local&lt;/span&gt;:///opt/spark/jobs/event-stream.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosEngine&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spark-executor-kill&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spark&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engineState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active'&lt;/span&gt;
  &lt;span class="na"&gt;chaosServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;
  &lt;span class="na"&gt;appinfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;appns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;spark'&lt;/span&gt;
    &lt;span class="na"&gt;applabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=spark-executor'&lt;/span&gt;
    &lt;span class="na"&gt;appkind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pod'&lt;/span&gt;
  &lt;span class="na"&gt;experiments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;180'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;CHAOS_INTERVAL&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;60'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PODS_AFFECTED_PERC&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;FORCE&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch-p99-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;15&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                &lt;span class="s"&gt;histogram_quantile(0.99,&lt;/span&gt;
                  &lt;span class="s"&gt;sum by(le)(rate(spark_streaming_batch_processing_seconds_bucket{job="event-stream"}[3m])))&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;30'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;executor-reprovision-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EOT&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;15&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;spark_executor_active_count{job="event-stream"}'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;='&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stage-retry-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EOT&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;15&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max(spark_stage_consecutive_attempts{job="event-stream"})'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;4'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;spark.dynamicAllocation.enabled=true&lt;/code&gt; lets the driver request new executors from the resource manager when load requires. &lt;code&gt;minExecutors=8&lt;/code&gt; sets the floor — even after two are killed, the driver will request replacements to get back above 8.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spark.blacklist.enabled=true&lt;/code&gt; + &lt;code&gt;spark.blacklist.timeout=1h&lt;/code&gt; prevents Spark from repeatedly retrying tasks onto a node that just crashed an executor. Without this, a bad node produces a cascade of retries as the driver keeps assigning tasks that keep failing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spark.shuffle.service.enabled=true&lt;/code&gt; runs the external shuffle service (as a DaemonSet on K8s or a NodeManager auxiliary on YARN) which persists shuffle blocks &lt;em&gt;outside&lt;/em&gt; the executor lifetime. Without it, executor death loses shuffle data and forces a re-compute; with it, surviving executors keep serving shuffle blocks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spark.streaming.stopGracefullyOnShutdown=true&lt;/code&gt; on the streaming job ensures the driver drains active batches before exiting on a SIGTERM. Combined with &lt;code&gt;spark.kubernetes.executor.deleteOnTermination=true&lt;/code&gt;, executor pods clean up their K8s resources on exit.&lt;/li&gt;
&lt;li&gt;The three probes cover three orthogonal invariants: (a) batch processing p99 stays under 30 s (SLA); (b) executor count recovers to &amp;gt;= 8 within the chaos window (reprovisioning works); (c) no stage hits max consecutive attempts (retry storm didn't happen).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (s)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Batch p99 (s)&lt;/th&gt;
&lt;th&gt;Active executors&lt;/th&gt;
&lt;th&gt;Max stage attempts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Chaos start&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;2 executors killed&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;Shuffle-fetch failures&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;Driver reprovisions&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;Job stable&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EOT&lt;/td&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every Spark chaos experiment must ship with &lt;code&gt;dynamicAllocation.enabled=true&lt;/code&gt; + &lt;code&gt;blacklist.enabled=true&lt;/code&gt; + &lt;code&gt;shuffle.service.enabled=true&lt;/code&gt;. Without the trio, executor loss produces either an under-provisioned job (no reprovisioning) or a retry storm (no blacklist) or a shuffle re-compute (no shuffle service).&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on data-plane chaos
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design a game-day chaos scenario that covers your entire ingest stack — Kafka → Spark Structured Streaming → Iceberg — with one composed experiment that kills a broker, then loses an executor, then latency-injects S3. Include the halt criteria, the ordering, and how you'd staff the game day."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a composed multi-step scenario with per-step data-SLI halts and a game-day runbook
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ChaosEngine — three sequential experiments, each with its own halt SLI&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmuschaos.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosEngine&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ingest-stack-game-day&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engineState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active'&lt;/span&gt;
  &lt;span class="na"&gt;chaosServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litmus-admin&lt;/span&gt;

  &lt;span class="na"&gt;experiments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;120'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PODS_AFFECTED_PERC&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TARGET_PODS&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=kafka-broker,role!=controller'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;data_sli_probes&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;freshness-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:freshness_p99_min'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;15'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lag-guard&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promProbe&lt;/span&gt;
            &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Continuous&lt;/span&gt;
            &lt;span class="na"&gt;runProperties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;probeTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stopOnFailure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;promProbe/inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://prometheus.monitoring:9090'&lt;/span&gt;
              &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:kafka_lag_messages'&lt;/span&gt;
              &lt;span class="na"&gt;comparator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;int&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;criteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;150000'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# 2-minute gap between experiments — let the pipeline settle&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-delete&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;RAMP_TIME&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;120'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;     &lt;span class="c1"&gt;# 2-min delay before this exp starts&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;180'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PODS_AFFECTED_PERC&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TARGET_PODS&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=spark-executor'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*data_sli_probes&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod-network-latency&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;RAMP_TIME&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;120'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TOTAL_CHAOS_DURATION&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;300'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NETWORK_LATENCY&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;500'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;DESTINATION_HOSTS&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3.us-east-1.amazonaws.com'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;TARGET_PODS&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app=iceberg-writer'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*data_sli_probes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- game-day-runbook.md — the human ceremony --&amp;gt;&lt;/span&gt;
&lt;span class="gh"&gt;# Ingest Stack Game Day — Runbook&lt;/span&gt;

&lt;span class="gs"&gt;**Duration.**&lt;/span&gt; 90 minutes (30 min setup + 30 min chaos + 30 min post-mortem)

&lt;span class="gs"&gt;**Roles.**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Game Master (GM): drives the ceremony; runs the chaos apply command
&lt;span class="p"&gt;-&lt;/span&gt; Observer 1 (Kafka): watches Kafka SLI dashboard; calls out anomalies
&lt;span class="p"&gt;-&lt;/span&gt; Observer 2 (Spark): watches Spark SLI dashboard
&lt;span class="p"&gt;-&lt;/span&gt; Observer 3 (Iceberg/S3): watches Iceberg + S3 dashboards
&lt;span class="p"&gt;-&lt;/span&gt; Scribe: takes running notes into the post-mortem doc
&lt;span class="p"&gt;-&lt;/span&gt; On-Call: shadows; can halt if real customer impact

&lt;span class="gs"&gt;**Pre-flight.**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] All observers connected to Zoom bridge
&lt;span class="p"&gt;-&lt;/span&gt; [ ] All SLI dashboards open (freshness, lag, batch p99, iceberg-commit-p99)
&lt;span class="p"&gt;-&lt;/span&gt; [ ] &lt;span class="sb"&gt;`#chaos-live`&lt;/span&gt; Slack channel posted with hypothesis
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Halt authority confirmed (any observer can shout "HALT" to trigger &lt;span class="sb"&gt;`kubectl delete chaosengine`&lt;/span&gt;)

&lt;span class="gs"&gt;**Sequence.**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; T+0 : GM applies &lt;span class="sb"&gt;`ChaosEngine`&lt;/span&gt;; observers narrate SLIs
&lt;span class="p"&gt;-&lt;/span&gt; T+2 : Kafka broker killed (probes gate lag &amp;lt; 150k)
&lt;span class="p"&gt;-&lt;/span&gt; T+4 : Kafka window ends; 2-min settle
&lt;span class="p"&gt;-&lt;/span&gt; T+6 : Spark executors killed (probes gate batch p99)
&lt;span class="p"&gt;-&lt;/span&gt; T+9 : Spark window ends; 2-min settle
&lt;span class="p"&gt;-&lt;/span&gt; T+11: S3 latency injected (probes gate iceberg-commit-p99)
&lt;span class="p"&gt;-&lt;/span&gt; T+16: S3 window ends
&lt;span class="p"&gt;-&lt;/span&gt; T+18: Chaos complete; observers verify all SLIs recovered
&lt;span class="p"&gt;-&lt;/span&gt; T+30: Scribe finalises post-mortem doc; GM assigns action items
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Halt criterion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;ChaosEngine applied&lt;/td&gt;
&lt;td&gt;kafka-broker-kill starts&lt;/td&gt;
&lt;td&gt;lag &amp;lt; 150k, freshness &amp;lt; 15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;120s chaos + settle&lt;/td&gt;
&lt;td&gt;broker recovers&lt;/td&gt;
&lt;td&gt;ISR heals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;spark-executor-kill starts&lt;/td&gt;
&lt;td&gt;2 executors killed&lt;/td&gt;
&lt;td&gt;batch p99 &amp;lt; 30s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;180s chaos + settle&lt;/td&gt;
&lt;td&gt;executors reprovision&lt;/td&gt;
&lt;td&gt;dynamic allocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;s3-latency starts&lt;/td&gt;
&lt;td&gt;+500ms to S3&lt;/td&gt;
&lt;td&gt;iceberg-commit-p99 &amp;lt; 30s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;300s chaos ends&lt;/td&gt;
&lt;td&gt;latency removed&lt;/td&gt;
&lt;td&gt;commits catch up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;GameDay doc&lt;/td&gt;
&lt;td&gt;scribe finalises&lt;/td&gt;
&lt;td&gt;action items filed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the ceremony, the game-day doc lists every observed deviation, every halted probe (with reason), and every backlog item filed in Jira. The doc becomes the reference for the next ceremony; every recurring ceremony either graduates the blast radius or logs a resilience regression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChaosEngine CRs (3 experiments)&lt;/td&gt;
&lt;td&gt;scripted chaos&lt;/td&gt;
&lt;td&gt;LitmusChaos operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data-SLI probes (2 per exp)&lt;/td&gt;
&lt;td&gt;halt criteria&lt;/td&gt;
&lt;td&gt;LitmusChaos runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;game-day-runbook.md&lt;/td&gt;
&lt;td&gt;human ceremony script&lt;/td&gt;
&lt;td&gt;GM + observers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live SLI dashboards&lt;/td&gt;
&lt;td&gt;real-time verdict&lt;/td&gt;
&lt;td&gt;observers + on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-mortem doc&lt;/td&gt;
&lt;td&gt;learning artifact&lt;/td&gt;
&lt;td&gt;data-platform team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jira backlog items&lt;/td&gt;
&lt;td&gt;fix commitments&lt;/td&gt;
&lt;td&gt;product + engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Composed sequential experiments&lt;/strong&gt;&lt;/strong&gt; — three chaos primitives run in sequence with 2-min settle gaps. Each experiment exercises a different subsystem (broker, executor, external dependency) while the others get to recover. Sequence-with-gaps is safer than parallel — the pipeline never faces two novel failure modes at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Shared data-SLI probe set (&lt;code&gt;&amp;amp;data_sli_probes&lt;/code&gt; YAML anchor)&lt;/strong&gt;&lt;/strong&gt; — the freshness + lag guards are defined once and referenced by all three experiments. This keeps the halt criteria consistent across the ceremony; every experiment is judged by the same yardstick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Ramp time between experiments&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;RAMP_TIME: '120'&lt;/code&gt; delays each experiment start so the previous one has 2 minutes to fully settle. Without ramp gaps, back-to-back failures mask each other's SLI signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Explicit game-day roles&lt;/strong&gt;&lt;/strong&gt; — GM, observers, scribe, on-call are named roles. This is the missing piece in most chaos programs: the ceremony has to have humans in the loop, or the automation looks like an unmanaged accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Halt authority = any observer&lt;/strong&gt;&lt;/strong&gt; — any observer can shout "HALT" and trigger &lt;code&gt;kubectl delete chaosengine ingest-stack-game-day&lt;/code&gt;. This is the human escape hatch that the automated probes can't provide (e.g. if a customer bug report comes in mid-ceremony).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Post-mortem doc as source of truth&lt;/strong&gt;&lt;/strong&gt; — the doc lands in a Confluence space with a stable URL. Every game-day ceremony produces one; every subsequent ceremony references the prior; the compounding effect is a written history of your pipeline's resilience posture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — 90 minutes of 5 engineers = ~7.5 engineer-hours per ceremony; quarterly cadence = 30 engineer-hours per year. Compared to the cost of one unplanned incident (~40+ engineer-hours + customer impact), the game day is a massive positive-ROI trade. O(1) per ceremony; O(quarterly cadence) per pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Streaming&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — streaming&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Streaming problems on Kafka consumer resilience&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Event&lt;/span&gt;
&lt;span&gt;Topic — event-processing&lt;/span&gt;
&lt;strong&gt;Event processing problems on exactly-once contracts&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/event-processing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Hypothesis-driven chaos loop — steady state → hypothesis → blast → verify
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The four-step loop that turns chaos from a stunt into a repeatable engineering practice
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;blast radius data&lt;/code&gt; chaos becomes a &lt;em&gt;practice&lt;/em&gt; (rather than a stunt) when every experiment cycles through four explicit stages — codify the steady state as a data SLI, state a falsifiable hypothesis about how the pipeline should behave under a specific failure, run the experiment inside a bounded blast radius with an auto-halt on SLI breach, and verify by comparing SLI curves before / during / after with a written post-mortem that either graduates the blast radius or produces a fix backlog item&lt;/strong&gt;. Every mature chaos program lives inside this loop; every immature one skips one or more stages and produces "we killed something and it seemed fine" as its output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F91p0zoibssnrsv94j1j0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F91p0zoibssnrsv94j1j0.jpeg" alt="Iconographic hypothesis-driven chaos loop diagram — four rounded nodes labelled steady state, hypothesis, experiment, verify arranged in a circle with a curving feedback arrow labelled 'learn' returning from verify to steady state." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four stages, mapped onto concrete data-pipeline artifacts.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Steady state.&lt;/strong&gt; One or more Prometheus recording rules that define "the pipeline is healthy." For a Kafka → Spark → Iceberg stack: &lt;code&gt;pipeline:freshness_p99_min&lt;/code&gt;, &lt;code&gt;pipeline:kafka_lag_messages&lt;/code&gt;, &lt;code&gt;pipeline:batch_success_rate_5m&lt;/code&gt;, &lt;code&gt;pipeline:row_count_delta_1h&lt;/code&gt;. The SLI is data-first, infra-second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hypothesis.&lt;/strong&gt; A written, falsifiable prediction of the form "if X breaks, Y stays within Z." Example: "If we kill 1 of 6 Kafka brokers, freshness_p99_min stays under 15 and kafka_lag_messages stays under 100k for the 120s chaos window."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experiment.&lt;/strong&gt; The chaos configuration that tests exactly the hypothesis — the smallest fault that exercises the failure mode, run inside the smallest blast radius that produces a signal, with halt-on-SLI-breach probes. The experiment is the ChaosEngine CR + probe set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify.&lt;/strong&gt; Compare SLI curves before / during / after (Grafana overlay). Write the post-mortem doc: hypothesis, actual outcome, probe timeline, root cause of any deviation, decision — graduate blast radius, or file a fix backlog item, or repeat the experiment next cycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The graduation ladder — how blast radius escalates over calendar time.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stage 1 — dev.&lt;/strong&gt; Nightly automated runs against a dev cluster. Blast radius = 100% of dev workloads (they're disposable). Failure = auto-file a Jira; no on-call impact. Goal: catch config errors before they touch shared environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage 2 — staging.&lt;/strong&gt; Weekly automated runs against staging. Blast radius = same percent as prod-target (17% of brokers, 20% of executors) so staging teaches you what prod will experience. Failure = Slack ping to data-platform-sre; no customer impact. Goal: validate the experiment's halt criteria against realistic-scale infra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage 3 — prod-1pct.&lt;/strong&gt; Monthly ceremony in prod during business hours. Blast radius = 1 broker of 6, or 2 executors of 20, or 1% of writers. GM + observers required; formal game-day format. Goal: prove the SLO holds under real production load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage 4 — prod-10pct.&lt;/strong&gt; Quarterly ceremony. Blast radius scales to 10% of tier-1 workloads. Executive sponsor required; on-call bridge open. Goal: verify the pipeline handles a "one AZ down" magnitude failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage 5 — prod-100pct.&lt;/strong&gt; Annual DR drill. Blast radius = full region outage or full-cluster loss. DR runbook + customer comms. Goal: annual DR compliance and executive-level confidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The written post-mortem template — what "verify" produces.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hypothesis (stated before the experiment).&lt;/strong&gt; "If X breaks, Y stays within Z."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actual outcome.&lt;/strong&gt; Screenshot of SLI dashboard with the chaos window highlighted; probe timeline extracted from &lt;code&gt;ChaosResult&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deviation analysis.&lt;/strong&gt; For each SLI, was the actual within predicted? If not, why? Was it a config bug, a code bug, or a genuine resilience gap?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision.&lt;/strong&gt; Graduate blast radius / repeat at same radius / halt this experiment class pending fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action items.&lt;/strong&gt; Each deviation gets a Jira ticket with owner + ETA.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on the chaos loop.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you decide when to graduate blast radius?" — required answer: N green runs at the current stage (matrix), no red runs in the last M runs, peer-reviewed PR to promote.&lt;/li&gt;
&lt;li&gt;"What if a chaos experiment reveals a bug in production?" — halt-on-breach immediately, on-call takes over, file a P1 bug with the SLI evidence, roll back any partially-applied fault.&lt;/li&gt;
&lt;li&gt;"How do you write a falsifiable hypothesis?" — required answer: "if X breaks, Y stays within Z" with X, Y, Z all quantitative. Weak: "the pipeline is resilient." Strong: "freshness_p99_min stays &amp;lt; 15 for 120 s of pod-delete on 1 of 6 brokers."&lt;/li&gt;
&lt;li&gt;"What separates a chaos experiment from a load test?" — chaos tests &lt;em&gt;unavailability&lt;/em&gt; failure modes; load tests capacity. Chaos gives you an SLO verdict under fault; load gives you a throughput ceiling under peak.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — writing a falsifiable chaos hypothesis
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The hypothesis is the single most-skipped step in first-attempt chaos programs. Teams reach for a fault ("let's kill a broker") without writing down what should happen. Without a written hypothesis, "we ran chaos" has no verdict — you either observe "it seemed fine" (which is not verifiable) or "it broke" (which is not attributable). Every mature chaos ceremony writes the hypothesis &lt;em&gt;in advance&lt;/em&gt; and pastes it into the game-day doc &lt;em&gt;before&lt;/em&gt; the experiment runs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Falsifiable.&lt;/strong&gt; The hypothesis must be a statement that concrete data can prove wrong. "The pipeline is resilient" is not falsifiable. "freshness_p99_min stays &amp;lt; 15 min for 120 s of pod-delete on 1 of 6 brokers" is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quantitative.&lt;/strong&gt; Every noun in the hypothesis must have a number. Not "some brokers" — "1 of 6." Not "low latency" — "&amp;lt; 15 min p99." Not "briefly" — "120 s."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded.&lt;/strong&gt; The hypothesis includes an explicit time window and blast radius. Without bounds, "stays &amp;lt; 15 min" is trivially violable at some future time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named SLIs.&lt;/strong&gt; The hypothesis references specific Prometheus recording rules (&lt;code&gt;pipeline:freshness_p99_min&lt;/code&gt;, &lt;code&gt;pipeline:kafka_lag_messages&lt;/code&gt;) — not vague concepts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write three chaos hypotheses for the ingest stack, and derive the halt criterion from each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hypothesis&lt;/th&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;SLI&lt;/th&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Blast radius&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kafka broker survives without lag spike&lt;/td&gt;
&lt;td&gt;pod-delete&lt;/td&gt;
&lt;td&gt;pipeline:kafka_lag_messages&lt;/td&gt;
&lt;td&gt;&amp;lt; 100k&lt;/td&gt;
&lt;td&gt;120s&lt;/td&gt;
&lt;td&gt;1/6 brokers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark stays inside p99 under executor loss&lt;/td&gt;
&lt;td&gt;pod-delete&lt;/td&gt;
&lt;td&gt;spark_streaming_batch_p99_sec&lt;/td&gt;
&lt;td&gt;&amp;lt; 30s&lt;/td&gt;
&lt;td&gt;180s&lt;/td&gt;
&lt;td&gt;2/10 executors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iceberg commits queue but don't fail under S3 latency&lt;/td&gt;
&lt;td&gt;pod-network-latency&lt;/td&gt;
&lt;td&gt;iceberg_commit_seconds_p99&lt;/td&gt;
&lt;td&gt;&amp;lt; 30s&lt;/td&gt;
&lt;td&gt;300s&lt;/td&gt;
&lt;td&gt;25% writers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Hypothesis card — checked into Git alongside every ChaosEngine&lt;/span&gt;
&lt;span class="c1"&gt;# hypothesis-cards/kafka-broker-kill.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipecode.ai/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ChaosHypothesis&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka-broker-kill&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hypothesis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;IF one Kafka broker (of six) is deleted via pod-delete,&lt;/span&gt;
    &lt;span class="s"&gt;THEN pipeline:kafka_lag_messages stays below 100,000&lt;/span&gt;
     &lt;span class="s"&gt;AND pipeline:freshness_p99_min stays below 15 minutes&lt;/span&gt;
     &lt;span class="s"&gt;AND min(kafka_topic_partition_in_sync_replica{topic="events"}) stays &amp;gt;= 2&lt;/span&gt;
    &lt;span class="s"&gt;FOR the duration of the 120-second chaos window.&lt;/span&gt;

  &lt;span class="na"&gt;falsifiable_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:kafka_lag_messages'&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;exceeds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;100000&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;10-second&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sample'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline:freshness_p99_min'&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;exceeds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;15&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;10-second&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sample'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;min(kafka_topic_partition_in_sync_replica{topic="events"})'&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;falls&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;below&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;15-second&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sample'&lt;/span&gt;

  &lt;span class="na"&gt;chaosengine_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos/base/engines/kafka-broker-kill.yaml&lt;/span&gt;

  &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data-platform-sre&lt;/span&gt;
  &lt;span class="na"&gt;reviewers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;alice&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;bob&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# hypothesis_check.py — a CI script that verifies every ChaosEngine has a hypothesis card
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;engines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chaos/base/engines/*.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cards&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chaos/hypothesis-cards/*.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;engine_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;engines&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;card_refs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chaosengine_ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine_names&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;card_refs&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR: ChaosEngines without hypothesis cards: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Verify each card has quantitative falsifiability
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;fbs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;falsifiable_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fb&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cond&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;condition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;some&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;few&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR: non-quantitative condition in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;All ChaosEngines have quantitative hypothesis cards.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;ChaosHypothesis&lt;/code&gt; CR is a &lt;em&gt;documentation&lt;/em&gt; artifact — it doesn't drive execution, it drives review. Every &lt;code&gt;ChaosEngine&lt;/code&gt; in Git must have a corresponding hypothesis card; the CI script enforces this.&lt;/li&gt;
&lt;li&gt;The hypothesis text uses &lt;code&gt;IF ... THEN ... AND ... FOR ...&lt;/code&gt; grammar. This isn't arbitrary — it forces the author to name the failure, name the SLI(s), name the threshold(s), and name the duration. Any missing piece becomes obviously wrong on review.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;falsifiable_by&lt;/code&gt; block enumerates the exact conditions that would falsify the hypothesis. Each &lt;code&gt;metric + condition&lt;/code&gt; pair maps 1:1 to a &lt;code&gt;promProbe&lt;/code&gt; in the ChaosEngine — if the two get out of sync, the experiment can pass while the hypothesis is falsified (a silent bug).&lt;/li&gt;
&lt;li&gt;The CI check catches two failure modes: (a) &lt;code&gt;ChaosEngine&lt;/code&gt; without a hypothesis card (execution without prediction), (b) hypothesis card with vague language ("some", "few", "briefly") — quantitative discipline enforced at commit time.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;reviewers&lt;/code&gt; field is not just administrative — chaos hypotheses need domain review. Killing a Kafka broker without ISR knowledge, or restarting the Airflow scheduler without catchup knowledge, is where teams ship dangerous experiments. Two reviewers is the norm; the more senior one should have shipped the specific system before.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ChaosHypothesis&lt;/code&gt; CR&lt;/td&gt;
&lt;td&gt;falsifiable prediction&lt;/td&gt;
&lt;td&gt;code review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI &lt;code&gt;hypothesis_check.py&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;every engine has a card&lt;/td&gt;
&lt;td&gt;pre-merge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grammar &lt;code&gt;IF ... THEN ... FOR ...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;forces quantitative discipline&lt;/td&gt;
&lt;td&gt;review-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;falsifiable_by&lt;/code&gt; block&lt;/td&gt;
&lt;td&gt;maps 1:1 to probes&lt;/td&gt;
&lt;td&gt;consistency check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two reviewers&lt;/td&gt;
&lt;td&gt;domain expertise&lt;/td&gt;
&lt;td&gt;governance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; No ChaosEngine ships to any environment without a hypothesis card. The hypothesis is the falsifiable prediction; the probes enforce the halt; the post-mortem records the outcome. Skip the hypothesis and you're not doing chaos engineering — you're doing chaos.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the quarterly ingest-stack game day
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The quarterly game day is where the chaos loop scales beyond one experiment. Teams gather for 90 minutes, run a pre-planned sequence of experiments against the whole ingest stack, watch the SLI dashboards live, and produce one post-mortem covering the ceremony. The ceremony has a fixed rhythm: 30-min pre-flight, 30-min chaos, 30-min post-mortem. Walk through the artifacts and cadence.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Attendees.&lt;/strong&gt; GM (drives), 3 Observers (Kafka / Spark / Iceberg), Scribe, On-Call (shadow with halt authority), optional Product owner (learns).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment.&lt;/strong&gt; Prod-1pct (early quarters) → Prod-10pct (later quarters). Never dev — dev doesn't produce actionable learning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-flight.&lt;/strong&gt; SLI dashboards open, &lt;code&gt;#chaos-live&lt;/code&gt; Slack post, hypothesis cards printed, halt authority confirmed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequence.&lt;/strong&gt; Three experiments (broker kill, executor loss, S3 latency) with 2-min settle between.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-mortem.&lt;/strong&gt; Written same day; action items assigned; blast-radius graduation decision recorded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the quarterly ceremony agenda, the responsibilities of each role, and the post-mortem template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GM&lt;/td&gt;
&lt;td&gt;apply ChaosEngine; drive tempo; call "next experiment"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observer 1 (Kafka)&lt;/td&gt;
&lt;td&gt;watch broker + lag + ISR panels; call anomalies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observer 2 (Spark)&lt;/td&gt;
&lt;td&gt;watch executor count + batch p99 + stage retries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observer 3 (Iceberg)&lt;/td&gt;
&lt;td&gt;watch commit p99 + write error rate + manifest count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scribe&lt;/td&gt;
&lt;td&gt;live-take notes into post-mortem doc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-Call&lt;/td&gt;
&lt;td&gt;shadow; halt authority; watches customer impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product&lt;/td&gt;
&lt;td&gt;learns; represents customer perspective&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- game-day/2026-Q3-ingest-stack.md --&amp;gt;&lt;/span&gt;
&lt;span class="gh"&gt;# Ingest Stack Game Day — 2026-Q3&lt;/span&gt;

&lt;span class="gs"&gt;**Date.**&lt;/span&gt; 2026-07-30 14:00 - 15:30 UTC
&lt;span class="gs"&gt;**Environment.**&lt;/span&gt; prod-10pct
&lt;span class="gs"&gt;**GM.**&lt;/span&gt; Alice Chen (data-platform-sre)
&lt;span class="gs"&gt;**Halt authority.**&lt;/span&gt; Any observer via &lt;span class="sb"&gt;`#chaos-live`&lt;/span&gt; "HALT" message → GM runs &lt;span class="sb"&gt;`kubectl delete chaosengine`&lt;/span&gt;

&lt;span class="gu"&gt;## Pre-flight checklist (14:00 - 14:30)&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; [ ] All observers on Zoom bridge
&lt;span class="p"&gt;-&lt;/span&gt; [ ] SLI dashboards open in tabs:
&lt;span class="p"&gt;  -&lt;/span&gt; [https://grafana/pipeline-freshness]
&lt;span class="p"&gt;  -&lt;/span&gt; [https://grafana/kafka-brokers]
&lt;span class="p"&gt;  -&lt;/span&gt; [https://grafana/spark-executors]
&lt;span class="p"&gt;  -&lt;/span&gt; [https://grafana/iceberg-commits]
&lt;span class="p"&gt;-&lt;/span&gt; [ ] &lt;span class="sb"&gt;`#chaos-live`&lt;/span&gt; posted with ceremony agenda + hypothesis cards
&lt;span class="p"&gt;-&lt;/span&gt; [ ] &lt;span class="sb"&gt;`kubectl`&lt;/span&gt; context confirmed = &lt;span class="sb"&gt;`prod-10pct`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Halt authority explicitly re-confirmed on the bridge
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Customer status page monitored (green expected)

&lt;span class="gu"&gt;## Experiment 1 — Kafka broker kill (14:30 - 14:35)&lt;/span&gt;

&lt;span class="gs"&gt;**Hypothesis.**&lt;/span&gt; IF one broker of six is deleted, THEN kafka_lag_messages stays &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="na"&gt;k&lt;/span&gt; &lt;span class="na"&gt;AND&lt;/span&gt; &lt;span class="na"&gt;freshness_p99_min&lt;/span&gt; &lt;span class="na"&gt;stays&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;15&lt;/span&gt; &lt;span class="na"&gt;AND&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt; &lt;span class="na"&gt;ISR&lt;/span&gt; &lt;span class="na"&gt;stays&lt;/span&gt; &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;= 2 FOR 120s.

&lt;span class="gs"&gt;**Apply.**&lt;/span&gt; &lt;span class="sb"&gt;`kubectl apply -f chaos/overlays/prod-10pct/kafka-broker-kill.yaml`&lt;/span&gt;

&lt;span class="gs"&gt;**Observations.**&lt;/span&gt; (Scribe fills in live)
&lt;span class="p"&gt;-&lt;/span&gt; Lag peak: ___
&lt;span class="p"&gt;-&lt;/span&gt; Freshness peak: ___
&lt;span class="p"&gt;-&lt;/span&gt; ISR minimum: ___
&lt;span class="p"&gt;-&lt;/span&gt; Halt triggered? ___

&lt;span class="gs"&gt;**Verdict.**&lt;/span&gt; [ ] Pass  [ ] Fail  [ ] Halted

&lt;span class="gu"&gt;## Experiment 2 — Spark executor loss (14:38 - 14:45)&lt;/span&gt;

&lt;span class="gs"&gt;**Hypothesis.**&lt;/span&gt; IF 2 of 10 executors are deleted, THEN batch_p99 stays &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;30&lt;/span&gt;&lt;span class="na"&gt;s&lt;/span&gt; &lt;span class="na"&gt;AND&lt;/span&gt; &lt;span class="na"&gt;executor_count&lt;/span&gt; &lt;span class="na"&gt;recovers&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt; &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;= 8 within 60s AND max stage attempts stays &amp;lt; 4 FOR 180s.

...

&lt;span class="gu"&gt;## Experiment 3 — S3 latency (14:48 - 14:58)&lt;/span&gt;

...

&lt;span class="gu"&gt;## Post-mortem (15:00 - 15:30)&lt;/span&gt;

&lt;span class="gs"&gt;**Overall verdict.**&lt;/span&gt; ___ / 3 experiments Pass

&lt;span class="gs"&gt;**Deviations.**&lt;/span&gt;
| Experiment | Predicted | Actual | Root cause | Action item |
|---|---|---|---|---|
| ... | ... | ... | ... | ... |

&lt;span class="gs"&gt;**Blast-radius decisions.**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Kafka broker kill: [ ] graduate to prod-100pct  [ ] repeat at prod-10pct  [ ] rollback to prod-1pct
&lt;span class="p"&gt;-&lt;/span&gt; Spark executor loss: [ ] ...
&lt;span class="p"&gt;-&lt;/span&gt; S3 latency: [ ] ...

&lt;span class="gs"&gt;**Action items (owner + ETA).**&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; ...
&lt;span class="p"&gt;2.&lt;/span&gt; ...

&lt;span class="gs"&gt;**Follow-up date.**&lt;/span&gt; 2026-10-30 (next quarterly)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The 90-min ceremony has a fixed shape: 30-min pre-flight, 30-min chaos, 30-min post-mortem. Deviating from the timing produces either "not enough setup" or "not enough learning" — the timings are load-bearing.&lt;/li&gt;
&lt;li&gt;Each experiment has a printed hypothesis card that pastes verbatim into the doc; the observations table is filled in live by the scribe. This makes the ceremony reproducible — anyone reading the doc a year later can retrace what was tried and what happened.&lt;/li&gt;
&lt;li&gt;The halt authority is deliberately human ("any observer via &lt;code&gt;#chaos-live&lt;/code&gt; HALT message"). The automated probes catch SLI breaches; the humans catch business-impact signals that no probe covers (a support ticket, a Twitter mention, a customer email).&lt;/li&gt;
&lt;li&gt;The post-mortem table forces attribution: for each deviation, name the root cause (config bug / code bug / genuine resilience gap) and file an action item. Without the table, deviations get forgotten.&lt;/li&gt;
&lt;li&gt;The blast-radius decisions are the promotion signal for the next quarter's ceremony. Each experiment either graduates (next quarter runs at bigger radius), repeats (same radius, verify stability), or rolls back (fix a bug, retry at smaller radius). This is what makes chaos a &lt;em&gt;compounding&lt;/em&gt; practice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pre-flight checklist&lt;/td&gt;
&lt;td&gt;operator readiness&lt;/td&gt;
&lt;td&gt;per ceremony&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-experiment hypothesis + observations&lt;/td&gt;
&lt;td&gt;falsifiable outcome&lt;/td&gt;
&lt;td&gt;per experiment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-mortem deviation table&lt;/td&gt;
&lt;td&gt;attributed learning&lt;/td&gt;
&lt;td&gt;per ceremony&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blast-radius decision matrix&lt;/td&gt;
&lt;td&gt;graduation signal&lt;/td&gt;
&lt;td&gt;per ceremony&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action items with owner + ETA&lt;/td&gt;
&lt;td&gt;fix commitments&lt;/td&gt;
&lt;td&gt;per deviation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Follow-up date&lt;/td&gt;
&lt;td&gt;compounding cadence&lt;/td&gt;
&lt;td&gt;per ceremony&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every game day produces exactly one written post-mortem, filed in a stable location, with one row per experiment and one action item per deviation. The doc is the compounding artifact — after 8 quarters you have 8 post-mortems that collectively tell the story of your pipeline's resilience trajectory.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the "verify" step — SLI-before / SLI-during / SLI-after overlay
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The verify step is where the hypothesis meets the data. Grafana overlays the SLI curves from a baseline (24h prior to chaos), the chaos window, and the post-chaos period. Deviations pop visually; agreement is boring (which is what you want). Every game-day doc includes screenshots of the overlays as the "here's what actually happened" evidence.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline.&lt;/strong&gt; Last 24h of the SLI, same window-of-day, same day-of-week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chaos.&lt;/strong&gt; The exact chaos window with the ChaosEngine start / end times marked as vertical lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-chaos.&lt;/strong&gt; 30 min after chaos ends — verify recovery is complete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlay.&lt;/strong&gt; Grafana &lt;code&gt;time_range&lt;/code&gt; templating shows all three periods on the same panel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the Grafana panel that provides the verify-step evidence for a Kafka broker kill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Panel&lt;/th&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Overlay&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Consumer lag&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sum(kafka_consumergroup_lag)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;baseline vs chaos vs post&lt;/td&gt;
&lt;td&gt;freshness verdict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Producer errors&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sum(rate(kafka_producer_record_error_total[1m]))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;baseline vs chaos&lt;/td&gt;
&lt;td&gt;durability verdict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Min ISR&lt;/td&gt;
&lt;td&gt;&lt;code&gt;min(kafka_topic_partition_in_sync_replica)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;chaos-only, annotated&lt;/td&gt;
&lt;td&gt;correctness verdict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker count ready&lt;/td&gt;
&lt;td&gt;&lt;code&gt;count(kube_pod_status_ready{pod=~"kafka-broker.*",condition="true"})&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;chaos-only&lt;/td&gt;
&lt;td&gt;infra recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Grafana&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;panel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Consumer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;lag&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;baseline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;overlay&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timeseries"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Consumer lag — chaos overlay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"refId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"expr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sum(kafka_consumergroup_lag{group=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;warehouse-sink&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;})"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"legendFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chaos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"datasource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prometheus"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"refId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"expr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sum(kafka_consumergroup_lag{group=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;warehouse-sink&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;} offset 24h)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"legendFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"baseline (24h ago)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"datasource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prometheus"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"legend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"placement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bottom"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tooltip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multi"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fieldConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"defaults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"thresholds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"absolute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"green"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yellow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chaos-window"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"datasource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prometheus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"expr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ALERTS{alertname=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ChaosEngineActive&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, chaosengine=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;kafka-broker-kill&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"iconColor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"purple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"titleFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chaos active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tagKeys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chaosengine"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# verify_generate.py — auto-generate the Grafana-URL for the verify step
# runs at ChaosResult completion; posts the URL to Slack + post-mortem doc
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_chaosresult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kubectl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chaosresult&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                  &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_grafana_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dashboard_uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_epoch_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_epoch_ms&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://grafana.internal/d/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;dashboard_uid&lt;/span&gt;
    &lt;span class="c1"&gt;# 30 min pre-chaos, chaos window, 30 min post-chaos
&lt;/span&gt;    &lt;span class="n"&gt;from_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start_epoch_ms&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;to_ms&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end_epoch_ms&lt;/span&gt;   &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;from_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;to_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;var-chaosengine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kafka-broker-kill&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post_verify_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chaosresult&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;exp&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chaosresult&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;experimentStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chaosresult&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startTimestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;endTimestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_grafana_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingest-chaos-overlay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://hooks.slack.com/services/xxx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:mag: Verify-step overlay for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_chaosresult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kafka-broker-kill-pod-delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kafka&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;post_verify_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Grafana panel plots two Prometheus series on the same y-axis: &lt;code&gt;kafka_consumergroup_lag&lt;/code&gt; (chaos) and &lt;code&gt;... offset 24h&lt;/code&gt; (baseline). The &lt;code&gt;offset&lt;/code&gt; shifts the query 24h back so we compare "now" to "same time yesterday" — same window-of-day traffic pattern.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;chaos-window&lt;/code&gt; annotation is driven by a Prometheus alert &lt;code&gt;ChaosEngineActive&lt;/code&gt; fired by an ancillary exporter that watches for active &lt;code&gt;ChaosEngine&lt;/code&gt; CRs. When the CR is active, the alert is firing, and Grafana overlays a shaded region on the panel.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;verify_generate.py&lt;/code&gt; script watches &lt;code&gt;ChaosResult&lt;/code&gt; CRs; when one completes, it extracts start/end timestamps and generates a Grafana URL scoped to the chaos window ± 30 min. The URL posts to &lt;code&gt;#chaos-live&lt;/code&gt; and lands in the post-mortem doc.&lt;/li&gt;
&lt;li&gt;The 30-min windows before/after the chaos give visual context — you can see the baseline SLI curve, the chaos-induced deviation, and the recovery back to baseline. The recovery segment is often more informative than the chaos segment; a slow recovery means the pipeline was more fragile than the point-in-time SLI suggested.&lt;/li&gt;
&lt;li&gt;The three-color threshold (green/yellow/red) on the y-axis gives an at-a-glance verdict — if the chaos curve stays in green throughout, hypothesis confirmed; if it dips into red, hypothesis falsified. The threshold values (50k warn, 100k halt) match the promProbe values.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Panel component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chaos series (query A)&lt;/td&gt;
&lt;td&gt;actual behaviour under fault&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baseline series (offset 24h)&lt;/td&gt;
&lt;td&gt;what "normal" looks like&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chaos-window annotation&lt;/td&gt;
&lt;td&gt;visual scope of the experiment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threshold bands&lt;/td&gt;
&lt;td&gt;SLI verdict at a glance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-generated URL&lt;/td&gt;
&lt;td&gt;shareable evidence for post-mortem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack post&lt;/td&gt;
&lt;td&gt;live operator awareness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every chaos experiment ships with a paired Grafana panel that overlays chaos-vs-baseline SLIs with the chaos window annotated. The panel URL lands in the post-mortem doc as the "verify" evidence. Without the overlay, "verify" degenerates into "we watched the dashboard and it seemed fine."&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the chaos loop
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design the chaos-engineering practice for a new data platform team from scratch. Include the SLI catalog, the hypothesis format, the graduation ladder, the game-day ceremony, the tooling choice (LitmusChaos vs Gremlin), and how you'd measure the practice's own maturity over time."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using an SLI-first hypothesis-driven program with quarterly ceremonies and a maturity ladder
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# chaos_maturity.py — score the practice itself, quarterly
# runs against the chaos GitOps repo + ChaosResult history
&lt;/span&gt;
&lt;span class="n"&gt;CRITERIA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLI catalog exists&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grep -r &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;record: pipeline:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; chaos/base/_slis.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Every engine has a hypothesis card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python hypothesis_check.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Every engine has continuous probes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python probe_check.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Every hypothesis card is falsifiable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python falsifiable_check.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Nightly dev chaos runs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kubectl get cronjob chaos-dev -n chaos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Weekly staging chaos runs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kubectl get cronjob chaos-staging -n chaos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Monthly prod-1pct ceremony&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gh api repos/acme/chaos/actions/workflows/prod-1pct.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Quarterly prod-10pct ceremony&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gh api repos/acme/chaos/actions/workflows/prod-10pct.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Post-mortem doc per ceremony&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ls game-day/*.md | wc -l&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Action-item follow-through &amp;gt; 80%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python action_item_check.py --window 90d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blast-radius graduation events &amp;gt; 0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python graduation_check.py --window 90d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SIEM audit trail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl -s splunk-hec/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CRITERIA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CRITERIA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shell&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  [x] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CalledProcessError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  [ ] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Maturity: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# CronJob — the practice's own heartbeat&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CronJob&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos-maturity-score&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*/3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;   &lt;span class="c1"&gt;# quarterly, 9am on the 1st&lt;/span&gt;
  &lt;span class="na"&gt;jobTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;score&lt;/span&gt;
              &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acme/chaos-maturity:latest&lt;/span&gt;
              &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;python'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/app/chaos_maturity.py'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OnFailure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SLI catalog&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_slis.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one recording rule per pipeline SLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hypothesis cards&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hypothesis-cards/*.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one per ChaosEngine; enforced by CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChaosEngine CRs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;engines/*.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one per experiment; probes reference SLIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blast-radius overlays&lt;/td&gt;
&lt;td&gt;&lt;code&gt;overlays/{dev,staging,prod-*}/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per-stage promotion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Game-day doc&lt;/td&gt;
&lt;td&gt;&lt;code&gt;game-day/*.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one per ceremony; scribe live-writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI checks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;hypothesis_check.py&lt;/code&gt;, &lt;code&gt;probe_check.py&lt;/code&gt;, &lt;code&gt;falsifiable_check.py&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pre-merge gates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maturity CronJob&lt;/td&gt;
&lt;td&gt;quarterly&lt;/td&gt;
&lt;td&gt;scores the practice itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIEM export&lt;/td&gt;
&lt;td&gt;&lt;code&gt;siem_exporter.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;attack audit trail&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, the practice grades itself quarterly. A team scoring 12/12 has a mature chaos practice: SLI catalog, hypothesis-driven experiments, probe-gated halt, graduated blast radius, written post-mortems, follow-through on action items, audit trail. A team scoring 4/12 has the tools but not the discipline — usually because they skipped the hypothesis step and can't produce a verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Practice level&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0-3&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;no chaos program&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-6&lt;/td&gt;
&lt;td&gt;Tooling-only&lt;/td&gt;
&lt;td&gt;ran experiments; no discipline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7-9&lt;/td&gt;
&lt;td&gt;Hypothesis-driven&lt;/td&gt;
&lt;td&gt;SLIs + hypotheses + halts; no ceremony&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-11&lt;/td&gt;
&lt;td&gt;Ceremony-driven&lt;/td&gt;
&lt;td&gt;quarterly game days + post-mortems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Compounding&lt;/td&gt;
&lt;td&gt;maturity score itself is monitored&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;SLI catalog before any experiment&lt;/strong&gt;&lt;/strong&gt; — the practice starts by defining what "healthy" means. The catalog is the single source of truth referenced by every probe and every hypothesis card. Without it, each experiment invents its own criteria and comparability dies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Hypothesis-driven experiments&lt;/strong&gt;&lt;/strong&gt; — every experiment ships with a falsifiable prediction. CI enforces the pairing (&lt;code&gt;hypothesis_check.py&lt;/code&gt;), the grammar (&lt;code&gt;falsifiable_check.py&lt;/code&gt;), and the probe consistency (&lt;code&gt;probe_check.py&lt;/code&gt;). Chaos becomes engineering, not stunts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Graduation ladder with matrix-gated promotion&lt;/strong&gt;&lt;/strong&gt; — dev → staging → prod-1pct → prod-10pct → prod-100pct, with N green runs required at each stage. CI enforces the counts; CODEOWNERS enforces the human approval. Prod chaos is earned, not granted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Quarterly game-day ceremony&lt;/strong&gt;&lt;/strong&gt; — the human ritual that reads the SLI evidence, discusses the deviations, and decides the next quarter's blast-radius adjustments. Ceremony frequency is the practice's heartbeat; skipping ceremonies collapses the compounding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Written post-mortem per ceremony&lt;/strong&gt;&lt;/strong&gt; — the doc is the compounding artifact. After 8 quarters you have 8 post-mortems that collectively tell the story of your pipeline's resilience trajectory. New team members read the docs to onboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Action-item follow-through as an SLI&lt;/strong&gt;&lt;/strong&gt; — the practice measures whether &lt;em&gt;its own&lt;/em&gt; action items are being closed. A team that files 20 action items and closes 3 is doing theatre; a team that files 8 and closes 7 is doing engineering. The 80% threshold in &lt;code&gt;chaos_maturity.py&lt;/code&gt; is the guard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Maturity score as a monitored SLI&lt;/strong&gt;&lt;/strong&gt; — the practice grades itself. When maturity drops (a ceremony was skipped, a check regressed), the CronJob output triggers a Slack alert. The practice's own health becomes a metric on the same dashboard as pipeline health.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one quarterly ceremony (7.5 engineer-hours) + nightly/weekly automation (~0 marginal ops) + one CI check bundle (~5 minutes per PR) + one CronJob (~1 minute per quarter). Compared to the cost of one unplanned incident, this is a 100× positive-ROI trade. O(1) per ceremony; O(quarterly) per pipeline; O(annual) per program review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Real-time&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — real-time-analytics&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Real-time analytics problems on SLI-first design&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/real-time-analytics" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;Data-Validation&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — data-validation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Data validation problems on post-chaos correctness&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — chaos engineering data recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Which tool when.&lt;/strong&gt; LitmusChaos is the 2026 default when your data plane is Kubernetes-native and you want everything GitOps-managed — free, CNCF, &lt;code&gt;ChaosEngine&lt;/code&gt; CR + probe guardrails + &lt;code&gt;ChaosResult&lt;/code&gt; verdict. Gremlin is the choice when your data plane spans VMs / on-prem / ECS / K8s and you want one SaaS control plane with SOC-2 audit, richer network/state primitives (DNS, time-travel, blackhole), and a Terraform provider. Pick both if your platform has both surfaces — LitmusChaos for K8s workloads, Gremlin for cross-platform infra chaos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steady-state SLI catalog template.&lt;/strong&gt; Define four Prometheus recording rules per pipeline: &lt;code&gt;pipeline:freshness_p99_min&lt;/code&gt; (histogram_quantile of &lt;code&gt;commit_age_seconds&lt;/code&gt; / 60), &lt;code&gt;pipeline:kafka_lag_messages&lt;/code&gt; (sum of consumer group lag), &lt;code&gt;pipeline:batch_success_rate_5m&lt;/code&gt; (success / total ratio over 5-min window), &lt;code&gt;pipeline:row_count_delta_1h&lt;/code&gt; (source − sink parity). Every probe on every ChaosEngine references these — one source of truth for "the pipeline is healthy."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Halt-condition template.&lt;/strong&gt; Halt criterion = &lt;code&gt;promProbe&lt;/code&gt; in &lt;code&gt;Continuous&lt;/code&gt; mode with &lt;code&gt;stopOnFailure: true&lt;/code&gt;, threshold set 2-3× tighter than the customer-facing SLO (so chaos aborts &lt;em&gt;before&lt;/em&gt; customers notice), &lt;code&gt;probeTimeout: 5&lt;/code&gt;, &lt;code&gt;interval: 10&lt;/code&gt;, &lt;code&gt;retry: 1&lt;/code&gt; (short retry — one flaky query shouldn't abort). Pair with an &lt;code&gt;EOT cmdProbe&lt;/code&gt; for correctness invariants Prometheus can't express (row counts, orphaned rows, manifest integrity).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LitmusChaos &lt;code&gt;ChaosEngine&lt;/code&gt; template.&lt;/strong&gt; &lt;code&gt;apiVersion: litmuschaos.io/v1alpha1&lt;/code&gt;, &lt;code&gt;spec.chaosServiceAccount: litmus-admin&lt;/code&gt;, &lt;code&gt;spec.appinfo.applabel&lt;/code&gt; selector, &lt;code&gt;experiments[0].name: pod-delete&lt;/code&gt; (or &lt;code&gt;pod-network-loss&lt;/code&gt;, &lt;code&gt;disk-fill&lt;/code&gt;, &lt;code&gt;pod-cpu-hog&lt;/code&gt;), env &lt;code&gt;TOTAL_CHAOS_DURATION&lt;/code&gt; + &lt;code&gt;PODS_AFFECTED_PERC&lt;/code&gt; + &lt;code&gt;FORCE: 'false'&lt;/code&gt; (graceful), &lt;code&gt;probe[]&lt;/code&gt; with 2-3 &lt;code&gt;promProbe&lt;/code&gt; + optional &lt;code&gt;k8sProbe&lt;/code&gt; + optional &lt;code&gt;cmdProbe&lt;/code&gt;. RBAC-scope the runner service account to the target namespace only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gremlin scenario YAML template.&lt;/strong&gt; &lt;code&gt;scenario.name&lt;/code&gt; + &lt;code&gt;hypothesis&lt;/code&gt; (falsifiable), &lt;code&gt;graph.nodes.{start,attack,cooldown}&lt;/code&gt; with &lt;code&gt;InfraAttack&lt;/code&gt; type + &lt;code&gt;impactDefinition&lt;/code&gt; (kind, attackType, params, &lt;code&gt;percent&lt;/code&gt; blast radius, &lt;code&gt;targets.tags&lt;/code&gt; selector), &lt;code&gt;healthChecks[]&lt;/code&gt; with &lt;code&gt;provider: datadog|webhook|newrelic|prometheus&lt;/code&gt; and &lt;code&gt;haltOnUnhealthy: true&lt;/code&gt;. Version via Terraform &lt;code&gt;gremlin_scenario&lt;/code&gt; resource; mirror every attack to SIEM via the API for audit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka broker chaos guardrails.&lt;/strong&gt; Topic: &lt;code&gt;replication.factor &amp;gt;= 3&lt;/code&gt;, &lt;code&gt;min.insync.replicas = 2&lt;/code&gt;, &lt;code&gt;unclean.leader.election.enable = false&lt;/code&gt;. Producer: &lt;code&gt;acks = all&lt;/code&gt;, &lt;code&gt;enable.idempotence = true&lt;/code&gt;, &lt;code&gt;retries = Integer.MAX_VALUE&lt;/code&gt;, &lt;code&gt;max.in.flight.requests.per.connection &amp;lt;= 5&lt;/code&gt;. Chaos: &lt;code&gt;PODS_AFFECTED_PERC &amp;lt;= 17&lt;/code&gt; (1 of 6), &lt;code&gt;FORCE: 'false'&lt;/code&gt;, halt on &lt;code&gt;kafka_consumergroup_lag &amp;gt; 100000&lt;/code&gt; OR &lt;code&gt;min(in_sync_replica) &amp;lt; 2&lt;/code&gt; OR &lt;code&gt;rate(kafka_producer_record_error_total[1m]) &amp;gt; 0.1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airflow scheduler chaos guardrails.&lt;/strong&gt; DAG: &lt;code&gt;catchup=False&lt;/code&gt;, &lt;code&gt;max_active_runs=1&lt;/code&gt;, &lt;code&gt;retries &amp;gt;= 2&lt;/code&gt;, &lt;code&gt;dagrun_timeout&lt;/code&gt; set. Chaos: &lt;code&gt;pod-delete&lt;/code&gt; on &lt;code&gt;component=scheduler&lt;/code&gt; with &lt;code&gt;FORCE: 'false'&lt;/code&gt;. Halt on &lt;code&gt;k8sProbe&lt;/code&gt; on scheduler Deployment ready + &lt;code&gt;EOT cmdProbe&lt;/code&gt; SQL &lt;code&gt;SELECT count(*) FROM task_instance WHERE state='running' AND queued_dttm &amp;lt; now() - INTERVAL '10 minutes' = 0&lt;/code&gt; (orphan check) + &lt;code&gt;EOT promProbe&lt;/code&gt; on &lt;code&gt;airflow_dagrun_active_count &amp;lt;= max_active_runs&lt;/code&gt; (no catchup flood).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spark executor chaos guardrails.&lt;/strong&gt; Spark submit: &lt;code&gt;dynamicAllocation.enabled=true&lt;/code&gt;, &lt;code&gt;dynamicAllocation.minExecutors=&amp;lt;floor&amp;gt;&lt;/code&gt;, &lt;code&gt;blacklist.enabled=true&lt;/code&gt;, &lt;code&gt;blacklist.timeout=1h&lt;/code&gt;, &lt;code&gt;task.maxFailures=4&lt;/code&gt;, &lt;code&gt;stage.maxConsecutiveAttempts=4&lt;/code&gt;, &lt;code&gt;shuffle.service.enabled=true&lt;/code&gt; (external shuffle service DaemonSet or NodeManager aux). Chaos: &lt;code&gt;PODS_AFFECTED_PERC &amp;lt;= 20&lt;/code&gt; on &lt;code&gt;app=spark-executor&lt;/code&gt;. Halt on &lt;code&gt;batch_processing_p99 &amp;gt; 30s&lt;/code&gt; + &lt;code&gt;EOT&lt;/code&gt; check &lt;code&gt;active_executor_count &amp;gt;= minExecutors&lt;/code&gt; + &lt;code&gt;max_stage_consecutive_attempts &amp;lt; 4&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast-radius graduation matrix.&lt;/strong&gt; Dev = 100% (disposable), 5 green runs to promote; Staging = same percent as prod-target (17% brokers, 20% executors, 25% writers), 3 green weekly runs to promote; Prod-1pct = 1 broker of 6 / 2 executors of 20 / 1% writers, monthly, 3 green ceremonies to promote; Prod-10pct = 10% of tier-1, quarterly, 2 green ceremonies to promote; Prod-100pct = annual DR drill only. CI script counts green runs; CODEOWNERS enforces human approval per stage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Game-day ceremony script.&lt;/strong&gt; 90 minutes total: 30-min pre-flight (SLI dashboards open, &lt;code&gt;#chaos-live&lt;/code&gt; posted, halt authority re-confirmed, &lt;code&gt;kubectl&lt;/code&gt; context verified) → 30-min chaos (three composed experiments with 2-min settle gaps, scribe live-writes observations, any observer can shout HALT) → 30-min post-mortem (deviation table, root cause per deviation, blast-radius decisions, action items with owner + ETA, follow-up date). Doc lives in Confluence at a stable URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hypothesis card template.&lt;/strong&gt; &lt;code&gt;IF &amp;lt;fault at blast radius X&amp;gt; THEN &amp;lt;SLI Y&amp;gt; stays &amp;lt;comparator threshold Z&amp;gt; AND &amp;lt;SLI Y'&amp;gt; stays &amp;lt;comparator threshold Z'&amp;gt; FOR &amp;lt;duration&amp;gt;.&lt;/code&gt; Every noun quantitative. Every SLI a Prometheus rule reference. Every &lt;code&gt;falsifiable_by&lt;/code&gt; entry maps 1:1 to a &lt;code&gt;promProbe&lt;/code&gt;. Two named reviewers, one with production experience on the specific system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SIEM audit trail.&lt;/strong&gt; Every chaos attack (LitmusChaos &lt;code&gt;ChaosResult&lt;/code&gt;, Gremlin attack event) mirrors to your SIEM (Splunk, Datadog, Elastic) with fields: &lt;code&gt;{attack_id, scenario, user, targets, tags, started_at, ended_at, halted, verdict, probe_timeline}&lt;/code&gt;. Retention: 1 year minimum (audit typical); 7 years for SOC-2 environments. Alert on any prod-scope chaos with no matching approval PR — that's an unauthorised attack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-mortem action-item follow-through.&lt;/strong&gt; Track action-item close-rate as a chaos-practice SLI: &lt;code&gt;(closed_within_90d / total_filed)&lt;/code&gt; &amp;gt; 80% is healthy; &amp;lt; 50% is theatre. Report the ratio in the quarterly maturity score; regress the ratio into the team's OKRs. An unclosed action item is a resilience gap that will surface as a real incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chaos maturity ladder.&lt;/strong&gt; 0-3 criteria = absent (no chaos program); 4-6 = tooling-only (running experiments, no discipline); 7-9 = hypothesis-driven (SLIs + hypotheses + halts, no ceremony); 10-11 = ceremony-driven (quarterly game days + written post-mortems); 12 = compounding (maturity score itself monitored, action-item follow-through &amp;gt; 80%, blast radius graduating quarter over quarter). Score the practice quarterly; target one level per year.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is chaos engineering for data pipelines in one sentence?
&lt;/h3&gt;

&lt;p&gt;Chaos engineering for data pipelines is the discipline of deliberately injecting infrastructure failures — Kafka broker kills, Airflow scheduler restarts, Spark executor loss, S3 latency spikes, disk fills, network partitions — into a &lt;em&gt;running&lt;/em&gt; pipeline while a codified data SLO (freshness_p99, end-to-end lag, DAG success rate, source-to-sink row parity) acts as the steady state that decides whether the experiment passes or halts, all inside a bounded blast radius that graduates from dev → staging → 1% prod → 10% prod → 100% prod. The four canonical tools in 2026 — &lt;code&gt;litmuschaos&lt;/code&gt; (K8s-native, open source, &lt;code&gt;ChaosEngine&lt;/code&gt; + probe CRs), &lt;code&gt;gremlin&lt;/code&gt; (SaaS, cross-platform, scenarios + blast-radius dial + halt-on-breach), plus the data-plane primitives (&lt;code&gt;kafka chaos&lt;/code&gt; broker kill, &lt;code&gt;airflow chaos&lt;/code&gt; scheduler kill, &lt;code&gt;spark chaos&lt;/code&gt; executor loss) — differ in deployment surface and fault library but converge on the same four-step loop: steady state → hypothesis → experiment → verify. Getting the axes right (data-first SLI, falsifiable hypothesis, halt-on-breach probe, written post-mortem) is what separates a mature practice from a stunt, and it's the load-bearing skill senior data platform interviewers probe.&lt;/p&gt;

&lt;h3&gt;
  
  
  LitmusChaos vs Gremlin — when do I pick each?
&lt;/h3&gt;

&lt;p&gt;Default to &lt;strong&gt;LitmusChaos&lt;/strong&gt; when your data plane runs entirely on Kubernetes and you want everything GitOps-managed — it's CNCF-graduated, Apache-2.0, and models chaos as first-class Kubernetes custom resources (&lt;code&gt;ChaosEngine&lt;/code&gt;, &lt;code&gt;ChaosExperiment&lt;/code&gt;, &lt;code&gt;ChaosResult&lt;/code&gt;) with a probe system (&lt;code&gt;httpProbe&lt;/code&gt;, &lt;code&gt;promProbe&lt;/code&gt;, &lt;code&gt;cmdProbe&lt;/code&gt;, &lt;code&gt;k8sProbe&lt;/code&gt;) that auto-aborts on breach. Ship it via Argo CD, RBAC-scope the runner to the target namespace, and the entire experiment lives in Git. Pick &lt;strong&gt;Gremlin&lt;/strong&gt; when your data plane spans VMs, on-prem, ECS, Databricks, mixed cloud — a single SaaS control plane with lightweight &lt;code&gt;gremlind&lt;/code&gt; agents on every target host, richer network/state attack primitives (DNS, blackhole, time-travel), and SOC-2-friendly SIEM-exportable audit logs. Gremlin is commercial (per-agent-per-month licence), so the trade is money-for-time-to-value vs LitmusChaos's ops-cost-for-flexibility. Larger platforms often run &lt;em&gt;both&lt;/em&gt; — LitmusChaos for the K8s data plane, Gremlin for the cross-platform infra chaos — with shared Datadog / Prometheus SLIs as the common halt criteria.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I define steady state for a data pipeline?
&lt;/h3&gt;

&lt;p&gt;Steady state must be a &lt;strong&gt;data SLO&lt;/strong&gt;, not an infra metric. For a Kafka → Spark Structured Streaming → Iceberg → warehouse stack, four Prometheus recording rules define steady state: &lt;code&gt;pipeline:freshness_p99_min&lt;/code&gt; (99th percentile age in minutes of the newest committed record vs wall clock), &lt;code&gt;pipeline:kafka_lag_messages&lt;/code&gt; (sum of consumer group lag across all partitions), &lt;code&gt;pipeline:batch_success_rate_5m&lt;/code&gt; (successful streaming batches / total batches over a 5-min window), and &lt;code&gt;pipeline:row_count_delta_1h&lt;/code&gt; (source events count − sink rows count over the last hour). Every chaos probe references these — no &lt;code&gt;pod_ready&lt;/code&gt;, no &lt;code&gt;cpu_percent &amp;lt; 80&lt;/code&gt;, because a pod can be Ready while the pipeline is 40 minutes behind. Set halt thresholds 2-3× tighter than the customer-facing SLO (chaos aborts &lt;em&gt;before&lt;/em&gt; customers notice) and pair Continuous &lt;code&gt;promProbe&lt;/code&gt; guards with EOT &lt;code&gt;cmdProbe&lt;/code&gt; checks for correctness invariants Prometheus can't express (orphaned task_instance rows, Iceberg manifest integrity, tombstone parity). If you can't articulate "the pipeline is healthy" in a single Prometheus query, you're not ready to inject chaos.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's a safe blast radius for a Kafka broker kill?
&lt;/h3&gt;

&lt;p&gt;The 2026 answer is &lt;strong&gt;one broker at a time&lt;/strong&gt;, scoped via &lt;code&gt;PODS_AFFECTED_PERC: '17'&lt;/code&gt; on a 6-broker cluster (or the equivalent selector on your topology), with a mandatory topic contract of &lt;code&gt;replication.factor &amp;gt;= 3&lt;/code&gt; + &lt;code&gt;min.insync.replicas = 2&lt;/code&gt; + &lt;code&gt;unclean.leader.election.enable = false&lt;/code&gt; and a producer contract of &lt;code&gt;acks = all&lt;/code&gt; + &lt;code&gt;enable.idempotence = true&lt;/code&gt;. Under those settings, killing one broker leaves each affected partition with 2 in-sync replicas — enough to satisfy &lt;code&gt;min.insync.replicas&lt;/code&gt; and continue accepting writes; the controller re-elects a new leader from the remaining ISR within seconds and the producer's retry loop absorbs the transient errors. Halt on &lt;code&gt;kafka_consumergroup_lag &amp;gt; 100000&lt;/code&gt; OR &lt;code&gt;min(in_sync_replica) &amp;lt; 2&lt;/code&gt; OR &lt;code&gt;rate(kafka_producer_record_error_total[1m]) &amp;gt; 0.1&lt;/code&gt; — any of these means the guardrails failed and further chaos will lose writes. Never kill the &lt;em&gt;controller&lt;/em&gt; in the same experiment (label the controller pod separately and exclude via &lt;code&gt;applabel: 'app=kafka-broker,role!=controller'&lt;/code&gt;); controller loss is a distinct experiment with different SLIs (controller failover time SLO).&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I run chaos engineering in production?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Yes, but only after the graduation ladder is climbed and the halt criteria are proven.&lt;/strong&gt; The mature 2026 pattern runs chaos through dev → staging → prod-1pct → prod-10pct → prod-100pct with N green runs required at each stage before promotion (5, 3, 3, 2, 1 respectively) and CI-gated + CODEOWNERS-approved PRs promoting each experiment between stages. Prod-1pct chaos is a monthly ceremony in a game-day format (GM, observers, scribe, on-call with halt authority); prod-10pct is quarterly with an executive sponsor; prod-100pct is reserved for annual DR drills with a formal runbook and customer comms. Every prod experiment ships with &lt;code&gt;stopOnFailure: true&lt;/code&gt; &lt;code&gt;promProbe&lt;/code&gt; guards tied to data SLIs 2-3× tighter than customer-facing SLOs and mirrors every attack to your SIEM (Splunk, Datadog, Elastic) for audit. The alternative to production chaos is &lt;em&gt;not&lt;/em&gt; zero chaos — it's &lt;em&gt;unplanned&lt;/em&gt; chaos when a real incident hits an untested failure mode. Scheduled, bounded, halted chaos is quantifiably safer than the incident lottery.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is chaos engineering different from load testing?
&lt;/h3&gt;

&lt;p&gt;Load testing measures &lt;strong&gt;capacity&lt;/strong&gt; — how many requests per second, how many concurrent users, how big a batch — the pipeline can handle before latency/throughput degrades. Chaos engineering measures &lt;strong&gt;availability&lt;/strong&gt; — whether the pipeline stays inside its SLO when a specific infrastructure component fails. Both are essential and both are complementary, but they answer different questions: load testing answers "how much load survives?" and chaos answers "which failure modes survive?" Load tests typically run in a dedicated environment with synthetic traffic; chaos experiments typically run in real environments with real traffic and inject &lt;em&gt;unavailability&lt;/em&gt; rather than &lt;em&gt;volume&lt;/em&gt;. A pipeline can pass a load test at 10× peak and still fail a chaos experiment because a Kafka broker kill exposes a &lt;code&gt;min.insync.replicas&lt;/code&gt; misconfig that only surfaces during failure. In the 2026 senior interview, describing chaos as "load testing but for failures" is a weak answer; describing it as "hypothesis-driven falsification of resilience assumptions under bounded infrastructure faults with SLO-tied halt criteria" is a senior signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;streaming practice library →&lt;/a&gt; for the Kafka broker chaos, ISR contract, and consumer-lag-under-failure problems senior interviewers love.&lt;/li&gt;
&lt;li&gt;Rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt; for the Airflow scheduler chaos, DAG catchup, and orphaned task_instance recovery patterns.&lt;/li&gt;
&lt;li&gt;Sharpen the architecture axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt; for graduated blast radius, game-day ceremony design, and SLO-first steady-state selection.&lt;/li&gt;
&lt;li&gt;Practise the &lt;a href="https://pipecode.ai/explore/practice/topic/event-processing" rel="noopener noreferrer"&gt;event-processing library →&lt;/a&gt; for exactly-once contracts, coordinator loss, and idempotent consumer patterns under chaos.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the four-step chaos loop against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lock in chaos engineering muscle memory&lt;/h3&gt;

&lt;p&gt;Docs explain chaos tools. PipeCode drills explain the decision — when a `promProbe` should halt, when `min.insync.replicas` earns its config line, when Airflow's `catchup=False` prevents a queue flood, when Spark's `dynamicAllocation` saves an executor-loss experiment, when a hypothesis is falsifiable versus vague. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — pattern-first practice tuned for the production trade-offs senior data platform engineers and SREs actually face.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice streaming problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice design problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>CI/CD for dbt + Airflow + Spark: Slim CI, State Comparison, Preview Environments</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 31 Jul 2026 14:41:26 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/cicd-for-dbt-airflow-spark-slim-ci-state-comparison-preview-environments-1i14</link>
      <guid>https://dev.to/gowthampotureddi/cicd-for-dbt-airflow-spark-slim-ci-state-comparison-preview-environments-1i14</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;dbt ci cd&lt;/code&gt;&lt;/strong&gt; is the load-bearing engineering discipline that decides whether every pull request to an analytics-engineering monorepo is a 90-second confidence signal or a 45-minute warehouse-burning re-run — and it is the single subsystem senior analytics engineers most often ship half-done, because "we already have GitHub Actions" is not the same as CI that understands &lt;code&gt;manifest.json&lt;/code&gt;, deferral, and zero-copy clones. A modern data platform ships three separately-versioned artifacts on every merge — a dbt project, a set of Airflow DAGs, and one or more Spark jobs — each of which mutates warehouse state, and each of which has its own axis of "what does it mean for this PR to be safe to merge?" that generic web-app CI/CD (unit tests + &lt;code&gt;docker build&lt;/code&gt; + push) simply does not model. The engineering trade-off does not live in "should we run CI on data pipelines" — every team past two engineers needs it — but in &lt;em&gt;how&lt;/em&gt; you scope each run so the fast path is fast, the slow path is honest, and the warehouse bill in November does not triple because CI decided to rebuild the entire mart on every whitespace-only PR.&lt;/p&gt;

&lt;p&gt;This guide is the senior data-platform walkthrough you wished existed the first time an interviewer asked "walk me through slim CI for dbt and why &lt;code&gt;state:modified+&lt;/code&gt; matters", or "your PR only edits one model — why did the CI job just spend $180 on Snowflake compute?", or "how do you give analysts a preview URL for the dashboard they're editing without cloning the entire warehouse?". It covers the four canonical CI axes (correctness, cost, latency-to-merge, blast radius), the slim CI recipe with &lt;code&gt;dbt build --select state:modified+ --defer --state prod-manifest/&lt;/code&gt; plus &lt;code&gt;dbt clone&lt;/code&gt; for zero-copy previews, the Airflow DAG-parse gate + operator unit-test matrix that catches import-time breaks before the scheduler ever sees them, the Spark packaged-JAR + local pytest + dry-run submit sequence, and the preview-environment pattern where every open pull request maps to its own &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; schema on Snowflake or Databricks Unity Catalog that is auto-created on PR open and auto-dropped on PR close. Each section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fedv46vj3dhez12m79ehu.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fedv46vj3dhez12m79ehu.jpeg" alt="PipeCode blog header for CI/CD for dbt, Airflow, and Spark — bold white headline 'CI/CD for Data Pipelines' over a hero composition of four medallions (dbt-brick, Airflow-DAG-graph, Spark-flame, preview-branch) arranged on a compass wheel around a central purple 'CI/CD' wax seal, on a dark gradient." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt;, rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt;, and sharpen the systems axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why generic CI/CD breaks on data pipelines&lt;/li&gt;
&lt;li&gt;Slim CI for dbt — state:modified, defer, and clone&lt;/li&gt;
&lt;li&gt;Airflow CI — DAG parse, integration tests, deployable artifacts&lt;/li&gt;
&lt;li&gt;Spark CI — packaged jobs, unit + integration tests, dry-run submits&lt;/li&gt;
&lt;li&gt;Preview environments — one branch, one warehouse schema&lt;/li&gt;
&lt;li&gt;Cheat sheet — CI/CD recipes for dbt + Airflow + Spark&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why generic CI/CD breaks on data pipelines — the four axes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Four systems, four different notions of "safe to merge" — the invariant every senior architect encodes
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;&lt;code&gt;ci for data pipelines&lt;/code&gt; is a picking exercise across four systems (dbt, Airflow, Spark, and the warehouse itself), each with its own definition of "the PR is safe" — dbt needs &lt;code&gt;state:modified+&lt;/code&gt; deferral so the CI job rebuilds only changed models against parent tables in prod, Airflow needs a DAG-parse gate so import-time errors never reach the scheduler, Spark needs a packaged job + local unit tests + a dry-run submit so the cluster is only touched after config is validated, and the warehouse needs a per-PR preview schema so blast radius stays bounded — and shipping any of the four without its native gate turns CI into either "45-minute full rebuild theatre" or "green build, broken prod"&lt;/strong&gt;. The default you pick in month one becomes the default you fight to migrate away from in year three, because every analyst dashboard, every Airflow DAG, and every Spark job hard-codes assumptions about where the "safe to merge" line lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes generic CI/CD misses.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correctness.&lt;/strong&gt; Web-app CI answers "did the unit tests pass?" Data-pipeline CI has to answer &lt;em&gt;and&lt;/em&gt; "does the new dbt model actually produce the same PK count as the old one?", "does the DAG still parse under the current Airflow version?", "does the Spark job's schema still match the upstream Kafka topic?". Each system's correctness gate is different; encoding all three is the senior-signal answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; A full &lt;code&gt;dbt build&lt;/code&gt; against Snowflake for a 200-model warehouse can cost $50-$500 per CI run. Multiply by 30 PRs/day and the CI compute bill dwarfs the actual production bill. Slim CI + preview schemas + XS warehouses are how you keep the November invoice under control. Interviewers open with this question because it separates people who have paid the bill from those who have not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency-to-merge.&lt;/strong&gt; A CI run that takes 45 minutes turns PR review into a next-day cycle. A CI run that takes 90 seconds keeps analysts in flow. The dbt slim-CI + Airflow parse-gate + Spark local-unit-test sequence is designed for the 90-second floor; skipping any layer pushes latency back into the 20-45 minute regime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius.&lt;/strong&gt; Web-app CI writes to a docker layer cache and a test bucket. Data-pipeline CI writes to &lt;em&gt;actual warehouse schemas&lt;/em&gt; — if you don't isolate per PR, one CI job's &lt;code&gt;dbt seed&lt;/code&gt; overwrites another CI job's fixture and both fail. The preview-schema pattern gives every PR its own &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; namespace so blast radius is exactly one PR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The 2026 reality — slim CI everywhere, state comparison, preview environments.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slim CI is the default for dbt.&lt;/strong&gt; &lt;code&gt;dbt build --select state:modified+ --defer --state prod-manifest/&lt;/code&gt; is the four-word answer. Every senior interview lists it in the first minute. Rebuilding only changed models against prod parents cuts CI from 45 minutes to 2 minutes on a typical monorepo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State comparison is the default for artifact promotion.&lt;/strong&gt; The &lt;code&gt;manifest.json&lt;/code&gt; from the last prod run is downloaded as an artifact; the CI job compares its own manifest against it and computes the &lt;code&gt;state:modified&lt;/code&gt; set. Without prod manifest, slim CI does not work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview environments are the default for warehouse UX.&lt;/strong&gt; Every PR gets a &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; schema (Snowflake &lt;code&gt;CREATE SCHEMA ... CLONE&lt;/code&gt;, Databricks &lt;code&gt;CREATE SCHEMA ... CLONE&lt;/code&gt;). Analysts hit the preview URL; when the PR merges or closes, the schema is dropped. Zero-copy clones make this cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airflow parse gates are non-negotiable.&lt;/strong&gt; A DAG that fails at import time takes down every DAG in the DagBag. The parse gate is a one-line CI check that catches it before the scheduler ever tries to load it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spark dry-run submits are the last-mile gate.&lt;/strong&gt; &lt;code&gt;spark-submit --deploy-mode client --num-executors 0&lt;/code&gt; (or the equivalent Databricks JOBS API dry-run) validates config, JAR resolution, and cluster reachability without actually spinning up executors — the cheapest possible "does this job actually work in this cluster" gate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you name &lt;strong&gt;all four systems&lt;/strong&gt; (dbt, Airflow, Spark, warehouse) and their per-system gates without prompting? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you say &lt;strong&gt;"slim CI with state:modified+ and deferral"&lt;/strong&gt; in the first minute when dbt CI comes up? — required answer.&lt;/li&gt;
&lt;li&gt;Do you push back on &lt;strong&gt;"just run the full test suite"&lt;/strong&gt; with the cost argument — "that's a $180 CI run per PR"? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name the &lt;strong&gt;preview-environment pattern&lt;/strong&gt; as the answer to "how do analysts review a PR"? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you describe CI as &lt;strong&gt;"gates that shrink blast radius"&lt;/strong&gt; rather than as vague "running tests"? — required answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the four-axis comparison table across dbt, Airflow, Spark
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful artifact for a data-pipeline CI/CD interview is a memorised 4×3 comparison table: four axes (correctness, cost, latency, blast radius) crossed with three systems (dbt, Airflow, Spark). Every senior CI/CD discussion converges on this table within the first ten minutes; having it in your head is what separates a fluent answer from a stumbling one. Walk through building the table for a hypothetical analytics-engineering monorepo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo shape.&lt;/strong&gt; One monorepo containing &lt;code&gt;dbt/&lt;/code&gt;, &lt;code&gt;airflow/dags/&lt;/code&gt;, &lt;code&gt;spark/jobs/&lt;/code&gt;. Deploys to Snowflake (warehouse), MWAA (Airflow), and EMR-on-EKS (Spark).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR volume.&lt;/strong&gt; ~30 pull requests per day; ~5 touch dbt, ~5 touch Airflow, ~2 touch Spark, the rest touch shared libraries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target CI budget.&lt;/strong&gt; Under 5 minutes p95 for a dbt-only PR; under 8 minutes for a mixed PR; under $10 per CI run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the four-axis CI comparison for dbt, Airflow, and Spark and pick the gate each system needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;dbt&lt;/th&gt;
&lt;th&gt;Airflow&lt;/th&gt;
&lt;th&gt;Spark&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Correctness gate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;state:modified+&lt;/code&gt; rebuild + &lt;code&gt;dbt test&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;DAG parse + operator unit tests&lt;/td&gt;
&lt;td&gt;pytest with local SparkSession&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost gate&lt;/td&gt;
&lt;td&gt;slim CI + XS warehouse&lt;/td&gt;
&lt;td&gt;container reuse + shared runner&lt;/td&gt;
&lt;td&gt;local mode + &lt;code&gt;--num-executors 0&lt;/code&gt; dry-run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency gate&lt;/td&gt;
&lt;td&gt;2 min for slim, 45 min for full&lt;/td&gt;
&lt;td&gt;30 s parse, 3 min unit tests&lt;/td&gt;
&lt;td&gt;4 min local pytest, 30 s dry-run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blast radius gate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; schema&lt;/td&gt;
&lt;td&gt;separate MWAA environment&lt;/td&gt;
&lt;td&gt;temp S3 prefix + separate EMR cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/data-pipeline-ci.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data-pipeline-ci&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;detect-changes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;dbt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;${{ steps.filter.outputs.dbt }}&lt;/span&gt;
      &lt;span class="na"&gt;airflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.filter.outputs.airflow }}&lt;/span&gt;
      &lt;span class="na"&gt;spark&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;${{ steps.filter.outputs.spark }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dorny/paths-filter@v3&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filter&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;dbt:&lt;/span&gt;
              &lt;span class="s"&gt;- 'dbt/**'&lt;/span&gt;
            &lt;span class="s"&gt;airflow:&lt;/span&gt;
              &lt;span class="s"&gt;- 'airflow/**'&lt;/span&gt;
            &lt;span class="s"&gt;spark:&lt;/span&gt;
              &lt;span class="s"&gt;- 'spark/**'&lt;/span&gt;

  &lt;span class="na"&gt;dbt-slim-ci&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect-changes&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect-changes.outputs.dbt == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/dbt-slim.yml&lt;/span&gt;

  &lt;span class="na"&gt;airflow-parse-and-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect-changes&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect-changes.outputs.airflow == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/airflow.yml&lt;/span&gt;

  &lt;span class="na"&gt;spark-unit-and-dry-run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect-changes&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect-changes.outputs.spark == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/spark.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The top-level workflow gates every downstream job on a &lt;code&gt;paths-filter&lt;/code&gt; step so a whitespace-only PR to &lt;code&gt;README.md&lt;/code&gt; does not trigger a &lt;code&gt;dbt build&lt;/code&gt;. Path-based short-circuiting is the cheapest CI optimisation and pays for itself the first day.&lt;/li&gt;
&lt;li&gt;Each system gets its own reusable workflow file. Isolating dbt, Airflow, and Spark into separate files keeps the top-level &lt;code&gt;data-pipeline-ci.yml&lt;/code&gt; readable and lets each team own its own gate logic without cross-editing.&lt;/li&gt;
&lt;li&gt;The dbt workflow (covered in section 2) runs slim CI against a preview schema. It downloads the prod &lt;code&gt;manifest.json&lt;/code&gt;, computes &lt;code&gt;state:modified+&lt;/code&gt;, defers parents to prod, and rebuilds only what changed.&lt;/li&gt;
&lt;li&gt;The Airflow workflow (covered in section 3) runs the DAG parse gate first (fastest fail), then the operator unit tests, then optionally an integration test against a LocalExecutor.&lt;/li&gt;
&lt;li&gt;The Spark workflow (covered in section 4) runs local unit tests with &lt;code&gt;pyspark.testing&lt;/code&gt;, packages the job into a wheel or JAR, and runs a dry-run submit against a small dev cluster to validate config.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PR touches&lt;/th&gt;
&lt;th&gt;Jobs that run&lt;/th&gt;
&lt;th&gt;p95 CI time&lt;/th&gt;
&lt;th&gt;Approximate cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dbt only&lt;/td&gt;
&lt;td&gt;dbt-slim-ci&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Airflow only&lt;/td&gt;
&lt;td&gt;airflow-parse-and-test&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;td&gt;$0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark only&lt;/td&gt;
&lt;td&gt;spark-unit-and-dry-run&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;td&gt;$0.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed (dbt + Airflow)&lt;/td&gt;
&lt;td&gt;both&lt;/td&gt;
&lt;td&gt;4 min (parallel)&lt;/td&gt;
&lt;td&gt;$0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;README only&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never run a &lt;code&gt;dbt build&lt;/code&gt; on a PR that does not touch dbt. Path-based gating is the single most impactful CI optimisation for a data-pipeline monorepo. Combine it with slim CI, DAG parse gates, and Spark dry-runs and a mixed-PR run finishes in under 5 minutes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — what interviewers actually probe on data-pipeline CI/CD
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The senior data-platform CI/CD interview has a predictable structure: the interviewer opens with an ambiguous question ("how do you set up CI for a dbt project?"), then progressively narrows to test whether you know the axes. The candidates who name slim CI and state comparison in sentence one score highest; the candidates who describe "GitHub Actions running dbt run" score lowest. Walk through the interview grading rubric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous opener.&lt;/strong&gt; "How would you set up CI for our dbt project?" — invites you to name slim CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 1.&lt;/strong&gt; "What happens when the PR only edits one model?" — probes state:modified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 2.&lt;/strong&gt; "How do you avoid rebuilding all parent tables?" — probes deferral.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 3.&lt;/strong&gt; "How much does a CI run cost?" — probes cost axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 4.&lt;/strong&gt; "How do analysts review the changed dashboard?" — probes preview environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 5.&lt;/strong&gt; "What about Airflow and Spark in the same repo?" — probes multi-system gates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draft a 5-minute senior CI/CD answer that covers all three systems and all four axes without waiting to be asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interview signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;Senior answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dbt gate named&lt;/td&gt;
&lt;td&gt;"we run dbt run in CI"&lt;/td&gt;
&lt;td&gt;"slim CI with &lt;code&gt;dbt build --select state:modified+ --defer --state prod-manifest/&lt;/code&gt;"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Airflow gate named&lt;/td&gt;
&lt;td&gt;"we run pytest"&lt;/td&gt;
&lt;td&gt;"DAG parse gate first, then operator unit tests, then integration test with LocalExecutor"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark gate named&lt;/td&gt;
&lt;td&gt;"we submit the job"&lt;/td&gt;
&lt;td&gt;"local pytest + packaged JAR + dry-run submit against dev cluster"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost bound&lt;/td&gt;
&lt;td&gt;"not sure"&lt;/td&gt;
&lt;td&gt;"under $1/run, XS warehouse with AUTO_SUSPEND=60"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview UX&lt;/td&gt;
&lt;td&gt;"analysts wait for merge"&lt;/td&gt;
&lt;td&gt;"&lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; schema created on PR open, dropped on close"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Senior data-pipeline CI/CD answer template (5 minutes)
======================================================

Minute 1 — name the gates up front
  "For each of the three systems in a data monorepo, CI has a native
   gate. dbt: slim CI with state:modified+ and deferral. Airflow:
   DAG parse gate + operator unit tests. Spark: local pytest +
   dry-run submit."

Minute 2 — dbt slim CI
  "The command is: dbt build --select state:modified+ --defer
   --state prod-manifest/ --target ci. The prod manifest.json is
   downloaded from S3 (uploaded on every prod run). state:modified+
   picks the changed models and their descendants; --defer routes
   any unbuilt parent refs to the prod schema instead of the CI
   schema. This turns a 45-minute full rebuild into a 2-minute
   incremental."

Minute 3 — Airflow CI
  "First gate is a DagBag import: python -c 'from airflow.models
   import DagBag; assert not DagBag(dag_folder=\"airflow/dags\",
   include_examples=False).import_errors'. Fails in 15 seconds if
   any DAG file breaks at import. Second gate is unit tests on
   custom operators and hooks. Third gate is an integration test
   with LocalExecutor for the DAGs that touch shared resources."

Minute 4 — Spark CI + preview envs
  "Spark: pytest against a local SparkSession fixture for logic,
   docker-compose Spark mini-cluster for schema and shuffle behaviour,
   and spark-submit --deploy-mode client --num-executors 0 as a
   dry-run gate that validates cluster config without spinning up
   executors. Preview envs: every PR gets a pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt; schema
   via Snowflake CREATE SCHEMA ... CLONE, with AUTO_SUSPEND=60 on the
   compute; dropped on PR close via GitHub webhook."

Minute 5 — cost + blast radius
  "Cost: slim CI + XS warehouse + AUTO_SUSPEND=60 keeps a dbt CI run
   under $0.30 typically. Blast radius: per-PR schema means one CI
   job cannot corrupt another. On PR close, the cleanup workflow
   drops the schema and de-registers the compute. November invoice
   is bounded because ephemeral compute suspends within 60 s of the
   last query."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Minute 1 is the crucial framing. Naming the three system-specific gates immediately signals you understand the axes; weak candidates dive into a single tool ("we use GitHub Actions and…") before naming any gate.&lt;/li&gt;
&lt;li&gt;Minute 2 addresses the dbt cost axis with the exact &lt;code&gt;state:modified+ --defer&lt;/code&gt; command. This preempts the "how do you avoid rebuilding all parents?" follow-up by answering it before it is asked.&lt;/li&gt;
&lt;li&gt;Minute 3 addresses the Airflow correctness axis: the DAG parse gate is the fast-fail; the operator unit tests are the coverage layer; the integration test is the "does the whole DAG wire together?" layer. Naming all three shows you have shipped Airflow CI, not just read the docs.&lt;/li&gt;
&lt;li&gt;Minute 4 combines Spark and preview environments into one minute because they share a design theme: cheap ephemeral compute that suspends when idle. &lt;code&gt;--num-executors 0&lt;/code&gt; for Spark and &lt;code&gt;AUTO_SUSPEND=60&lt;/code&gt; for Snowflake are both "compute suspends when unused" patterns.&lt;/li&gt;
&lt;li&gt;Minute 5 covers the cost + blast-radius axes explicitly. Naming the November invoice concern signals you have paid the bill; naming the auto-cleanup on PR close signals you have on-called the workflow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading criterion&lt;/th&gt;
&lt;th&gt;Weak score&lt;/th&gt;
&lt;th&gt;Senior score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Names all four axes&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names slim CI in minute 1&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names DAG parse gate&lt;/td&gt;
&lt;td&gt;occasional&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names dry-run Spark submit&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names preview env + cleanup&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names cost budget in dollars&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The senior data-pipeline CI/CD answer is a 5-minute monologue that covers dbt, Airflow, and Spark plus the four axes (correctness, cost, latency, blast radius) without waiting for the follow-ups. Rehearse it once; deploy it every time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the pick-the-gate decision tree per PR type
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Given a new pull request, the senior architect runs a fast decision tree in their head to pick which gates to run. Codifying the tree makes the CI system answer reproducible: any change fires only the gates that could possibly detect a regression from that change, and skips the rest. Walk through the tree for four canonical PR types.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Q1.&lt;/strong&gt; Which files did the PR touch? → path-filter output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q2.&lt;/strong&gt; If dbt files → does the PR add/modify a &lt;code&gt;.sql&lt;/code&gt; model, a &lt;code&gt;.yml&lt;/code&gt; schema test, a macro, or a seed? → each triggers different subsets of slim CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q3.&lt;/strong&gt; If Airflow files → is it a DAG, an operator, a hook, or a plugin? → each has a different test target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q4.&lt;/strong&gt; If Spark files → is it a job entry point, a UDF, or shared library code? → each has different local + dry-run coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q5.&lt;/strong&gt; If shared libraries → run every downstream system's smoke test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Walk the decision tree for four scenarios and record the gates that fire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Files touched&lt;/th&gt;
&lt;th&gt;Q2/3/4&lt;/th&gt;
&lt;th&gt;Gates that fire&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Edit one dbt model&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dbt/models/marts/orders.sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;model&lt;/td&gt;
&lt;td&gt;slim CI (state:modified+), dbt test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add an Airflow operator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;airflow/plugins/operators/reverse_etl.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;operator&lt;/td&gt;
&lt;td&gt;operator unit tests, DAG parse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix a Spark UDF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;spark/jobs/enrich/udf/geo.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UDF&lt;/td&gt;
&lt;td&gt;local pytest, dry-run submit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bump a shared library&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shared/pyutils/dates.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;shared&lt;/td&gt;
&lt;td&gt;all three systems' smoke tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/pick_ci_gates.py — illustrative
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pick_gates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files_touched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the set of CI gates that must run for a given file diff.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;gates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files_touched&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;gates&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt-slim-ci&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt-test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preview-schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files_touched&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;gates&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow-parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow-unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dags/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files_touched&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;gates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow-integration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files_touched&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;gates&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark-unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobs/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files_touched&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;gates&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark-package&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark-dry-run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files_touched&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Shared code — smoke-test every downstream system
&lt;/span&gt;        &lt;span class="n"&gt;gates&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt-slim-ci&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow-parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark-unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gates&lt;/span&gt;


&lt;span class="c1"&gt;# Walk the four scenarios
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_gates&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt/models/marts/orders.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="c1"&gt;# → {'dbt-slim-ci', 'dbt-test', 'preview-schema'}
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_gates&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow/plugins/operators/reverse_etl.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="c1"&gt;# → {'airflow-parse', 'airflow-unit'}
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_gates&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark/jobs/enrich/udf/geo.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="c1"&gt;# → {'spark-unit', 'spark-package', 'spark-dry-run'}
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_gates&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared/pyutils/dates.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="c1"&gt;# → {'dbt-slim-ci', 'airflow-parse', 'spark-unit'}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scenario 1 — a single dbt model edit. Slim CI computes &lt;code&gt;state:modified+&lt;/code&gt;, rebuilds &lt;code&gt;orders&lt;/code&gt; and its descendants against the preview schema, runs the model's &lt;code&gt;.yml&lt;/code&gt; tests, and posts pass/fail to the PR. Total: ~2 minutes.&lt;/li&gt;
&lt;li&gt;Scenario 2 — an operator edit. The operator is not imported by any DAG at file-scan time, so the parse gate is fast (~15 s). The operator unit tests exercise the operator directly with a mock hook. No integration test fires because no DAG file changed.&lt;/li&gt;
&lt;li&gt;Scenario 3 — a Spark UDF edit. Local pytest exercises the UDF against a &lt;code&gt;SparkSession.builder.master("local[2]")&lt;/code&gt; fixture. The job entry point that uses the UDF is repackaged and dry-run submitted to validate the cluster still accepts it.&lt;/li&gt;
&lt;li&gt;Scenario 4 — a shared library change. Because we don't know which downstream systems use &lt;code&gt;pyutils.dates&lt;/code&gt;, we run one smoke gate for each: dbt slim CI (which will trip if any macro imports the library), Airflow parse (which will trip if any DAG imports it), Spark unit tests (which will trip if any job imports it).&lt;/li&gt;
&lt;li&gt;The decision tree is not a heuristic — it is a &lt;em&gt;contract&lt;/em&gt;. Every gate in the tree has to run when the tree says it should; skipping any gate breaks the invariant that CI-green means merge-safe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Gates fired&lt;/th&gt;
&lt;th&gt;p95 latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single dbt model edit&lt;/td&gt;
&lt;td&gt;slim CI + dbt test + preview schema&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New operator&lt;/td&gt;
&lt;td&gt;parse + operator unit&lt;/td&gt;
&lt;td&gt;30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark UDF fix&lt;/td&gt;
&lt;td&gt;unit + package + dry-run&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared library bump&lt;/td&gt;
&lt;td&gt;slim CI + parse + spark unit (parallel)&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The four-question decision tree is a whiteboard-friendly answer. Practice walking it end-to-end so an interviewer can hand you any PR shape and get a gate list in under 30 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on data-pipeline CI/CD architecture
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You inherit a data-pipeline monorepo (dbt + Airflow + Spark) whose CI takes 45 minutes and costs $180 per PR. Walk me through the redesign — the per-system gates, the state comparison, the preview environments, the cost budget, and the on-call runbook when CI is red on prod-blocking PRs."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using slim CI + parse gate + dry-run submit + per-PR preview schemas
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/pipeline-ci.yml — the whole redesign&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipeline-ci&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request_target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;closed&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# 1. Path-based short-circuit — never run a system that did not change&lt;/span&gt;
  &lt;span class="na"&gt;detect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;dbt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;${{ steps.f.outputs.dbt }}&lt;/span&gt;
      &lt;span class="na"&gt;airflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.f.outputs.airflow }}&lt;/span&gt;
      &lt;span class="na"&gt;spark&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;${{ steps.f.outputs.spark }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;f&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dorny/paths-filter@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;dbt:     [ 'dbt/**' ]&lt;/span&gt;
            &lt;span class="s"&gt;airflow: [ 'airflow/**' ]&lt;/span&gt;
            &lt;span class="s"&gt;spark:   [ 'spark/**' ]&lt;/span&gt;

  &lt;span class="c1"&gt;# 2. Preview schema — created per PR, dropped on close (see section 5)&lt;/span&gt;
  &lt;span class="na"&gt;preview-schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect.outputs.dbt == 'true' &amp;amp;&amp;amp; github.event.action != 'closed'&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/preview-schema-create.yml&lt;/span&gt;

  &lt;span class="c1"&gt;# 3. dbt slim CI (see section 2)&lt;/span&gt;
  &lt;span class="na"&gt;dbt-ci&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;detect&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;preview-schema&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect.outputs.dbt == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/dbt-slim.yml&lt;/span&gt;

  &lt;span class="c1"&gt;# 4. Airflow gates (see section 3)&lt;/span&gt;
  &lt;span class="na"&gt;airflow-ci&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect.outputs.airflow == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/airflow.yml&lt;/span&gt;

  &lt;span class="c1"&gt;# 5. Spark gates (see section 4)&lt;/span&gt;
  &lt;span class="na"&gt;spark-ci&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect.outputs.spark == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/spark.yml&lt;/span&gt;

  &lt;span class="c1"&gt;# 6. On PR close — clean up preview schema regardless of merge/reject&lt;/span&gt;
  &lt;span class="na"&gt;cleanup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event.action == 'closed'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Drop preview schema&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/drop_preview_schema.py --pr ${{ github.event.number }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# CI budget contract — one line per gate
Gate                    p95 latency   Cost budget   Owner
────────────────────────────────────────────────────────────
dbt-slim-ci             120 s         $0.30         analytics-eng
airflow-parse           15 s          $0.02         platform
airflow-unit            180 s         $0.05         platform
spark-unit              240 s         $0.08         data-eng
spark-dry-run           30 s          $0.02         data-eng
preview-schema-create   20 s          $0.01         platform
preview-schema-drop     10 s          $0.00         platform
────────────────────────────────────────────────────────────
Mixed PR (all systems)  ~5 min        ~$0.50        —
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dbt strategy&lt;/td&gt;
&lt;td&gt;full &lt;code&gt;dbt run --target ci&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dbt build --select state:modified+ --defer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI duration (dbt-only PR)&lt;/td&gt;
&lt;td&gt;45 min&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI cost (dbt-only PR)&lt;/td&gt;
&lt;td&gt;$180&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Airflow gate&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;parse gate + operator unit tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark gate&lt;/td&gt;
&lt;td&gt;submit-to-prod-cluster&lt;/td&gt;
&lt;td&gt;local pytest + dry-run submit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse blast radius&lt;/td&gt;
&lt;td&gt;shared &lt;code&gt;analytics_ci&lt;/code&gt; schema&lt;/td&gt;
&lt;td&gt;per-PR &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview UX for analysts&lt;/td&gt;
&lt;td&gt;wait for merge&lt;/td&gt;
&lt;td&gt;preview URL on PR open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleanup&lt;/td&gt;
&lt;td&gt;manual quarterly purge&lt;/td&gt;
&lt;td&gt;auto-drop on PR close&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the migration, a dbt-only PR completes CI in about 2 minutes and costs roughly $0.30; a mixed PR touching all three systems completes in about 5 minutes because gates run in parallel; the November warehouse invoice drops from about $9,000 in CI compute to about $200. Analysts open the preview URL from the PR description and see the dashboard rendered against the changed models within 3 minutes of PR open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median CI time&lt;/td&gt;
&lt;td&gt;45 min&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 CI time (mixed PR)&lt;/td&gt;
&lt;td&gt;55 min&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median CI cost&lt;/td&gt;
&lt;td&gt;$180&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview environment&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;one per PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blast radius&lt;/td&gt;
&lt;td&gt;one shared schema&lt;/td&gt;
&lt;td&gt;per-PR isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November warehouse CI bill&lt;/td&gt;
&lt;td&gt;~$9,000&lt;/td&gt;
&lt;td&gt;~$200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Path-based short-circuit&lt;/strong&gt;&lt;/strong&gt; — the cheapest optimisation available. A whitespace-only PR to &lt;code&gt;README.md&lt;/code&gt; fires zero gates; the CI pipeline is essentially free for docs-only work. This is the "detect" job at the top of the workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Per-system gates&lt;/strong&gt;&lt;/strong&gt; — dbt, Airflow, and Spark each get the native gate their runtime actually cares about. Slim CI for dbt, parse gate for Airflow, dry-run submit for Spark. Running only the gates that could possibly detect a regression is the definition of proportionate CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Preview schemas&lt;/strong&gt;&lt;/strong&gt; — every PR gets its own &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; schema, so two concurrent CI runs cannot collide. The preview is also the analyst-facing URL; the same schema serves CI and human review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Auto-cleanup on PR close&lt;/strong&gt;&lt;/strong&gt; — the cleanup workflow is idempotent and fires whether the PR merges or is rejected. Without it, preview schemas accumulate indefinitely; with it, warehouse footprint is bounded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost budget per gate&lt;/strong&gt;&lt;/strong&gt; — publishing the cost budget makes it a &lt;em&gt;contract&lt;/em&gt;, not a hope. When a gate exceeds its budget, the SRE-of-CI catches it in the weekly review; when a new gate is proposed, the budget question comes up in code review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one CI orchestration layer, one preview-schema helper, one cleanup workflow, plus per-system gates that each stay under $0.30. Compared to the 45-minute full-rebuild baseline, this is one order of magnitude faster at less than 1% of the cost. Net O(delta) per PR versus O(catalog) per PR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;ETL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;ETL problems on pipeline-CI design&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Design&lt;/span&gt;
&lt;span&gt;Topic — design&lt;/span&gt;
&lt;strong&gt;Design problems on data platform CI/CD&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Slim CI for dbt — state:modified, defer, and clone
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;dbt build --select state:modified+ --defer --state prod-manifest/&lt;/code&gt; is the four-word answer to every dbt CI/CD interview
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;slim ci dbt&lt;/code&gt; is the pattern where a CI job downloads the last successful production &lt;code&gt;manifest.json&lt;/code&gt;, compares its own manifest against it to compute &lt;code&gt;state:modified&lt;/code&gt; (the set of models whose SQL, config, or upstream sources actually changed), rebuilds only that set plus its descendants (&lt;code&gt;state:modified+&lt;/code&gt;) into a per-PR schema, and defers any unbuilt parent refs to the production schema instead of re-materialising them — turning a 45-minute full rebuild into a 2-minute incremental that costs cents instead of dollars while still testing every change against real prod data&lt;/strong&gt;. Every senior analytics engineer has adopted this pattern once; every senior interviewer probes for it in the first minute.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsmlo6lr2z0t4zstfpjlh.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsmlo6lr2z0t4zstfpjlh.jpeg" alt="Iconographic slim CI dbt diagram — Git PR card on the left, a state:modified diff card, a defer-to-prod arrow into a prod-manifest cylinder, and a tested subset chip on the right." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for slim CI dbt.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correctness.&lt;/strong&gt; &lt;code&gt;state:modified&lt;/code&gt; catches every model whose SQL, config, or upstream &lt;code&gt;.yml&lt;/code&gt; schema actually changed. The &lt;code&gt;+&lt;/code&gt; selector suffix adds every descendant (models that ref the changed ones), so a change to a staging model rebuilds the mart that depends on it. This is the correctness contract: if the PR could affect a model, that model gets rebuilt and tested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; A slim CI run rebuilds 3-15 models on average (versus 200-1500 for a full run). Snowflake XS warehouse with AUTO_SUSPEND=60 keeps a typical run under $0.30. This is the cost contract; without slim CI, a 200-model warehouse hits $30-$500 per CI run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency-to-merge.&lt;/strong&gt; Two-minute CI turns "I'll review this after lunch" into "I'll review this in the next 5 minutes." The latency payoff compounds across a team of ten analysts each opening 3 PRs a day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius.&lt;/strong&gt; Slim CI writes to the per-PR preview schema (&lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt;), never to &lt;code&gt;analytics_prod&lt;/code&gt;. Deferral routes &lt;em&gt;reads&lt;/em&gt; to the prod schema (safe — read-only) and &lt;em&gt;writes&lt;/em&gt; to the preview schema (isolated — per-PR). The prod schema is never touched by CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;manifest.json&lt;/code&gt; — the state comparison substrate.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it is.&lt;/strong&gt; dbt's &lt;code&gt;target/manifest.json&lt;/code&gt; describes the parsed project: every model, its SQL fingerprint, its config, its upstream refs, its sources. Two manifests are directly comparable; &lt;code&gt;state:modified&lt;/code&gt; diffs them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where it lives.&lt;/strong&gt; Uploaded to S3 (or GCS or Azure Blob) as an artifact at the end of every successful prod run. Path convention: &lt;code&gt;s3://dbt-artifacts/prod/latest/manifest.json&lt;/code&gt; plus a versioned copy at &lt;code&gt;s3://dbt-artifacts/prod/&amp;lt;run_id&amp;gt;/manifest.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How CI downloads it.&lt;/strong&gt; The CI job pulls the "prod latest" manifest into a local &lt;code&gt;prod-manifest/&lt;/code&gt; directory before running slim CI. The &lt;code&gt;--state prod-manifest/&lt;/code&gt; flag points dbt at it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bootstrap problem.&lt;/strong&gt; The first CI run has no prod manifest to compare against; the slim-CI logic falls back to a full build. Most teams solve this by seeding the S3 path from a manual initial run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;state:modified+&lt;/code&gt; selector — what it catches and what it doesn't.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What &lt;code&gt;state:modified&lt;/code&gt; catches.&lt;/strong&gt; Models whose SQL text changed, whose config (materialization, tags, cluster_by, etc.) changed, whose upstream sources' definitions changed, or whose upstream refs' fingerprints changed. Also new models and deleted models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What &lt;code&gt;+&lt;/code&gt; adds.&lt;/strong&gt; Every descendant (children, grandchildren, etc.) of the modified set. If you change a staging model, every mart that depends on it rebuilds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it does not catch.&lt;/strong&gt; Behaviour changes in seeds (use &lt;code&gt;state:modified,resource_type:seed&lt;/code&gt; for that), macro changes (use &lt;code&gt;state:modified.macros&lt;/code&gt;), or changes in a dbt package version bump (use &lt;code&gt;state:modified.macros&lt;/code&gt; + &lt;code&gt;state:modified.contracts&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The safety escape hatch.&lt;/strong&gt; For a suspicious PR (a big refactor, a dbt-core upgrade), append &lt;code&gt;--select "state:modified+" "state:new+"&lt;/code&gt; or fall back to a full build on the &lt;code&gt;main&lt;/code&gt; branch periodically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;--defer --state prod-manifest/&lt;/code&gt; pair — the parent-routing magic.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The read.&lt;/strong&gt; When a CI-built model runs &lt;code&gt;{{ ref('unmodified_upstream') }}&lt;/code&gt;, dbt sees that model was not rebuilt in this run, checks the state manifest, and rewrites the ref to point at the prod schema instead of the CI schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The write.&lt;/strong&gt; All modified models still write to the CI schema (&lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt;); deferred parents are only &lt;em&gt;read&lt;/em&gt; from prod. Prod is never mutated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The permission model.&lt;/strong&gt; The CI role needs SELECT on the prod schema (to read deferred parents) and CREATE + SELECT + INSERT on the preview schema (to build modified models). Configure once in Snowflake / BigQuery / Redshift; use forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on slim CI dbt.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What does &lt;code&gt;state:modified+&lt;/code&gt; mean?" — required answer: modified models plus all descendants.&lt;/li&gt;
&lt;li&gt;"How do you get the prod manifest into CI?" — download from S3 at the top of the CI job.&lt;/li&gt;
&lt;li&gt;"What does &lt;code&gt;--defer&lt;/code&gt; do?" — routes unbuilt refs to the state manifest's schema (prod) instead of the CI schema.&lt;/li&gt;
&lt;li&gt;"How do you handle the bootstrap case where no prod manifest exists yet?" — fall back to a full build on the first run and seed the artifact.&lt;/li&gt;
&lt;li&gt;"How do you avoid slim CI missing a macro change?" — either use &lt;code&gt;state:modified.macros&lt;/code&gt; selector or trigger a full build on macro-file changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — GitHub Actions job that runs slim CI on every PR
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical slim CI setup: a GitHub Actions workflow that installs dbt, downloads the prod manifest from S3, computes &lt;code&gt;state:modified+&lt;/code&gt;, defers parents to prod, and posts the results to the PR. Build the whole thing from scratch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger.&lt;/strong&gt; &lt;code&gt;pull_request&lt;/code&gt; open, synchronize, reopen; skip on PR close.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runner.&lt;/strong&gt; &lt;code&gt;ubuntu-latest&lt;/code&gt;; Python 3.11; dbt-snowflake 1.9+.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State source.&lt;/strong&gt; Prod manifest from &lt;code&gt;s3://dbt-artifacts/prod/latest/manifest.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target schema.&lt;/strong&gt; &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; derived from GitHub context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post.&lt;/strong&gt; Slim-CI summary as a PR comment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the full GitHub Actions workflow that runs &lt;code&gt;dbt build --select state:modified+ --defer --state prod-manifest/&lt;/code&gt; on every PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CI runner&lt;/td&gt;
&lt;td&gt;ubuntu-latest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt adapter&lt;/td&gt;
&lt;td&gt;dbt-snowflake 1.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State source&lt;/td&gt;
&lt;td&gt;s3://dbt-artifacts/prod/latest/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;ci (pr__ schema)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;OIDC to AWS + Snowflake key-pair from GitHub secret&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/dbt-slim.yml — reusable slim CI workflow&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-slim-ci&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;slim-ci&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;          &lt;span class="c1"&gt;# OIDC to AWS&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;     &lt;span class="c1"&gt;# to post the comment&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DBT_PROFILES_DIR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./ci&lt;/span&gt;
      &lt;span class="na"&gt;DBT_TARGET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci&lt;/span&gt;
      &lt;span class="na"&gt;DBT_PR_NUMBER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.number }}&lt;/span&gt;
      &lt;span class="na"&gt;DBT_BRANCH_SLUG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.head_ref }}&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out PR&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dbt&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
          &lt;span class="s"&gt;dbt --version&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS credentials (OIDC)&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::123456789012:role/gha-dbt-ci&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download prod manifest&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p prod-manifest&lt;/span&gt;
          &lt;span class="s"&gt;aws s3 cp s3://dbt-artifacts/prod/latest/manifest.json \&lt;/span&gt;
                    &lt;span class="s"&gt;prod-manifest/manifest.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compute preview schema name&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;schema&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# pr_&amp;lt;PR&amp;gt;_&amp;lt;branch-slug&amp;gt; — sanitize branch name for SQL identifier&lt;/span&gt;
          &lt;span class="s"&gt;slug=$(echo "$DBT_BRANCH_SLUG" | tr '/' '_' | tr -cd '[:alnum:]_' | tr '[:upper:]' '[:lower:]' | cut -c1-40)&lt;/span&gt;
          &lt;span class="s"&gt;echo "schema=pr_${DBT_PR_NUMBER}_${slug}" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt deps&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt deps&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt build (slim CI)&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DBT_SNOWFLAKE_PRIVATE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SNOWFLAKE_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;DBT_TARGET_SCHEMA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.schema.outputs.schema }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;dbt build \&lt;/span&gt;
            &lt;span class="s"&gt;--select state:modified+ \&lt;/span&gt;
            &lt;span class="s"&gt;--defer \&lt;/span&gt;
            &lt;span class="s"&gt;--state ../prod-manifest \&lt;/span&gt;
            &lt;span class="s"&gt;--target ci \&lt;/span&gt;
            &lt;span class="s"&gt;--fail-fast&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload CI manifest for downstream jobs&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci-manifest-${{ env.DBT_PR_NUMBER }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt/target/manifest.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post slim-CI summary to PR&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const fs   = require('fs');&lt;/span&gt;
            &lt;span class="s"&gt;const path = require('path');&lt;/span&gt;
            &lt;span class="s"&gt;const run  = fs.readFileSync('dbt/target/run_results.json', 'utf8');&lt;/span&gt;
            &lt;span class="s"&gt;const j    = JSON.parse(run);&lt;/span&gt;
            &lt;span class="s"&gt;const rows = j.results.map(r =&amp;gt;&lt;/span&gt;
              &lt;span class="s"&gt;`| ${r.unique_id} | ${r.status} | ${r.execution_time.toFixed(2)}s |`&lt;/span&gt;
            &lt;span class="s"&gt;).join('\n');&lt;/span&gt;
            &lt;span class="s"&gt;const body = `### dbt slim CI — PR #${{ env.DBT_PR_NUMBER }}\n\n` +&lt;/span&gt;
              &lt;span class="s"&gt;`| Model | Status | Time |\n|---|---|---|\n${rows}\n\n` +&lt;/span&gt;
              &lt;span class="s"&gt;`Preview schema: \`${{ steps.schema.outputs.schema }}\`.\n`;&lt;/span&gt;
            &lt;span class="s"&gt;github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
              &lt;span class="s"&gt;owner:  context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo:   context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;body:   body&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dbt/ci/profiles.yml — CI target profile&lt;/span&gt;
&lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
  &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ci&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snowflake&lt;/span&gt;
      &lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;env_var('SNOWFLAKE_ACCOUNT')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;env_var('SNOWFLAKE_USER')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;private_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;env_var('DBT_SNOWFLAKE_PRIVATE_KEY')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DBT_CI_ROLE&lt;/span&gt;
      &lt;span class="na"&gt;warehouse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DBT_CI_WH_XS&lt;/span&gt;   &lt;span class="c1"&gt;# AUTO_SUSPEND=60&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ANALYTICS&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;env_var('DBT_TARGET_SCHEMA')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;  &lt;span class="c1"&gt;# pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;threads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;
      &lt;span class="na"&gt;client_session_keep_alive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The workflow is &lt;code&gt;workflow_call&lt;/code&gt;-able so the top-level &lt;code&gt;pipeline-ci.yml&lt;/code&gt; can invoke it as a reusable job. This keeps the dbt-specific logic in one file and lets the top-level orchestrator focus on gate selection.&lt;/li&gt;
&lt;li&gt;OIDC + &lt;code&gt;aws-actions/configure-aws-credentials&lt;/code&gt; is the modern auth pattern; there is no long-lived AWS key in a GitHub secret. The IAM role &lt;code&gt;gha-dbt-ci&lt;/code&gt; has read access to the &lt;code&gt;dbt-artifacts&lt;/code&gt; bucket only.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Compute preview schema name&lt;/code&gt; derives &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;slug&amp;gt;&lt;/code&gt; from the GitHub Actions context. The slug sanitiser strips non-alphanumeric characters, lowercases, and caps at 40 characters — Snowflake identifiers must fit in 255 characters and be lowercase-safe.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dbt build --select state:modified+ --defer --state ../prod-manifest --target ci&lt;/code&gt; is the core command. &lt;code&gt;build&lt;/code&gt; is &lt;code&gt;run + test + snapshot + seed&lt;/code&gt; in dependency order — one command runs everything. &lt;code&gt;--fail-fast&lt;/code&gt; stops on the first error so CI feedback is fast.&lt;/li&gt;
&lt;li&gt;The final step posts a table of every rebuilt model + its status + its runtime as a PR comment. Reviewers see the exact impact of the PR without opening the CI logs. The comment updates on every push (or a new comment; use a comment-updater action for one-comment-per-PR UX).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rebuild scope&lt;/th&gt;
&lt;th&gt;Full CI&lt;/th&gt;
&lt;th&gt;Slim CI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Models rebuilt (median PR)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 CI time&lt;/td&gt;
&lt;td&gt;45 min&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 CI cost&lt;/td&gt;
&lt;td&gt;$180&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview schema&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;analytics_ci&lt;/code&gt; (shared)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; (isolated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR comment&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;model-by-model table&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any dbt CI setup, mount slim CI as the default gate: &lt;code&gt;state:modified+ --defer --state prod-manifest/&lt;/code&gt;. Post the results as a PR comment. Upload the CI manifest so downstream jobs (e.g. a doc-preview or dbt-freshness job) can consume it. These three habits move a dbt project from "CI is a chore" to "CI is a review tool."&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — state comparison against &lt;code&gt;manifest.json&lt;/code&gt; from main
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; State comparison is the substrate slim CI runs on: dbt compares two manifests and computes the changed set. Walk through what "state comparison" actually means at the manifest level — which fields dbt looks at, how it computes &lt;code&gt;state:modified&lt;/code&gt;, and how you can inspect the comparison manually to debug false positives.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fields dbt fingerprints.&lt;/strong&gt; SQL text (after Jinja render), config (materialization, cluster_by, partition_by, tags, meta), upstream refs, upstream sources, contracts (if enabled), and constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fingerprint algorithm.&lt;/strong&gt; dbt computes a SHA-256 hash of the concatenation of the above fields per model. Two models with the same hash are "unmodified"; differing hashes mark &lt;code&gt;state:modified&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why false positives happen.&lt;/strong&gt; Whitespace changes in SQL, Jinja variable resolution differences (e.g. &lt;code&gt;{{ var('env') }}&lt;/code&gt; resolves differently in CI vs prod), or macro upgrades can flip the hash even when the effective SQL is identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging.&lt;/strong&gt; &lt;code&gt;dbt ls --select state:modified --state prod-manifest/&lt;/code&gt; lists exactly what changed; &lt;code&gt;dbt-checkpoint&lt;/code&gt; or the &lt;code&gt;dbt-project-evaluator&lt;/code&gt; package can help diagnose surprising diffs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a debug script that lists exactly which fields of a modified model changed between the CI manifest and the prod manifest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prod manifest&lt;/td&gt;
&lt;td&gt;prod-manifest/manifest.json&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI manifest&lt;/td&gt;
&lt;td&gt;dbt/target/manifest.json&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model to inspect&lt;/td&gt;
&lt;td&gt;model.analytics.orders_mart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fields to diff&lt;/td&gt;
&lt;td&gt;raw_code, config.materialized, depends_on.nodes, checksum.checksum&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/dbt_state_diff.py — diff two manifests for a single model
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Usage:
  python scripts/dbt_state_diff.py &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;      --prod prod-manifest/manifest.json &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;      --ci   dbt/target/manifest.json &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;      --model model.analytics.orders_mart
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;difflib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unique_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unique_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;unique_id&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; not in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;diff_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return (field, prod_value, ci_value) for each differing top-level field.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checksum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;depends_on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pv&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--ci&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;prod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ci&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;diffs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;diff_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;diffs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: identical (unexpected — would not be state:modified)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;diffs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;=== &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ===&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;difflib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unified_diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;pv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;fromfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod::&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tofile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ci::&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;lineterm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Example output for a legit SQL change
=== raw_code ===
--- prod::raw_code
+++ ci::raw_code
@@ -12,7 +12,7 @@
 SELECT
     o.order_id,
     o.customer_id,
-    o.total_cents          AS gross_cents,
+    o.total_cents          AS gross_cents,
+    o.discount_cents       AS discount_cents,
     o.status
 FROM {{ ref('stg_orders') }} o

=== checksum ===
--- prod::checksum
+++ ci::checksum
@@ -1,4 +1,4 @@
 {
   "name": "sha256",
-  "checksum": "5a2f...9c1d"
+  "checksum": "8d3b...4e77"
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The script loads both manifests, extracts the requested &lt;code&gt;unique_id&lt;/code&gt; (dbt's fully-qualified node key), and diffs a curated list of top-level fields. Diffing every field is noisy; the curated list matches what dbt actually fingerprints.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;raw_code&lt;/code&gt; is the post-Jinja-render SQL. A whitespace-only change here still flips the checksum, so if a model is unexpectedly &lt;code&gt;state:modified&lt;/code&gt;, this is the first field to inspect. Consider adding an &lt;code&gt;.editorconfig&lt;/code&gt; and &lt;code&gt;sqlfmt&lt;/code&gt; pre-commit hook to eliminate whitespace-only diffs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checksum&lt;/code&gt; is the SHA-256 that dbt actually compares. If checksums match, the model is &lt;em&gt;not&lt;/em&gt; &lt;code&gt;state:modified&lt;/code&gt;; if they differ, it &lt;em&gt;is&lt;/em&gt;. This is the ground-truth field.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config&lt;/code&gt; catches materialization / cluster_by / tag changes. A model that switches from &lt;code&gt;view&lt;/code&gt; to &lt;code&gt;table&lt;/code&gt; is &lt;code&gt;state:modified&lt;/code&gt; even if the SQL is identical.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;depends_on.nodes&lt;/code&gt; catches upstream ref changes. Adding a new &lt;code&gt;{{ ref('foo') }}&lt;/code&gt; marks the model modified; removing one also marks it modified.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Change type&lt;/th&gt;
&lt;th&gt;Marks model as state:modified?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;raw_code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;any character diff (incl whitespace)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checksum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SHA-256 changed&lt;/td&gt;
&lt;td&gt;yes (derived from raw_code + config)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config.materialized&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;view → table&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config.tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;added / removed&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;depends_on.nodes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;new / removed ref&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;description&lt;/code&gt; (yml)&lt;/td&gt;
&lt;td&gt;text-only change&lt;/td&gt;
&lt;td&gt;no (documentation only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; When a PR unexpectedly rebuilds a model that "shouldn't have changed," run the state-diff script against the model — the culprit is almost always a whitespace change in &lt;code&gt;raw_code&lt;/code&gt;, a Jinja-var resolution difference, or a macro upgrade that touched the render. Fix the root cause; do not disable slim CI.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — &lt;code&gt;dbt clone&lt;/code&gt; a schema for preview
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; For dashboards, downstream apps, and analysts who want to &lt;em&gt;use&lt;/em&gt; the CI schema, an empty &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; schema is not enough — they need it seeded with all the tables that were &lt;em&gt;not&lt;/em&gt; rebuilt by slim CI. &lt;code&gt;dbt clone&lt;/code&gt; (added in dbt-core 1.6) does this in one command: it zero-copy clones every model in the state manifest that was not rebuilt, so the preview schema is a full mirror of prod overlaid with the rebuilt models. This is the pattern that turns slim CI into a genuine preview environment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What &lt;code&gt;dbt clone&lt;/code&gt; does.&lt;/strong&gt; For every model in &lt;code&gt;--state&lt;/code&gt;, if the CI schema doesn't already have the model, dbt executes &lt;code&gt;CREATE OR REPLACE &amp;lt;schema&amp;gt;.&amp;lt;model&amp;gt; CLONE prod.&amp;lt;model&amp;gt;&lt;/code&gt; (Snowflake) or the equivalent zero-copy on the warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it's cheap.&lt;/strong&gt; Snowflake and Databricks Unity Catalog implement zero-copy cloning as a metadata operation; no data is physically copied. A 10 TB table clones in ~1 second and costs $0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use it.&lt;/strong&gt; Whenever a human (analyst, PM, stakeholder) needs to &lt;code&gt;SELECT * FROM pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;.orders_mart&lt;/code&gt; and expect all the joined tables to be there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Extend the slim CI workflow to clone unmodified models into the preview schema after the slim build completes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse&lt;/td&gt;
&lt;td&gt;Snowflake (or Databricks UC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clone command&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dbt clone --state prod-manifest/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target schema&lt;/td&gt;
&lt;td&gt;pr__&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime for 200-table clone&lt;/td&gt;
&lt;td&gt;~5 s (zero-copy metadata op)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost for 200-table clone&lt;/td&gt;
&lt;td&gt;~$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Extension to .github/workflows/dbt-slim.yml — add after "dbt build" step&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt clone unmodified models into preview schema&lt;/span&gt;
  &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DBT_SNOWFLAKE_PRIVATE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SNOWFLAKE_PRIVATE_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;DBT_TARGET_SCHEMA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.schema.outputs.schema }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;dbt clone \&lt;/span&gt;
      &lt;span class="s"&gt;--state ../prod-manifest \&lt;/span&gt;
      &lt;span class="s"&gt;--target ci \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-type model \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-type seed \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-type snapshot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- What `dbt clone` executes on Snowflake, per model&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;ANALYTICS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PR_4712_ADD_ORDERS_V2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STG_CUSTOMERS&lt;/span&gt;
  &lt;span class="n"&gt;CLONE&lt;/span&gt; &lt;span class="n"&gt;ANALYTICS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PROD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STG_CUSTOMERS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ANALYTICS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PR_4712_ADD_ORDERS_V2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DIM_DATE&lt;/span&gt;
  &lt;span class="n"&gt;CLONE&lt;/span&gt; &lt;span class="n"&gt;ANALYTICS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PROD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DIM_DATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- ... one CREATE OR REPLACE ... CLONE per model in the state manifest&lt;/span&gt;
&lt;span class="c1"&gt;-- that was not rebuilt by slim CI ...&lt;/span&gt;

&lt;span class="c1"&gt;-- The rebuilt models (from slim CI) are already in the preview schema&lt;/span&gt;
&lt;span class="c1"&gt;-- as freshly-materialized objects, so `CREATE OR REPLACE ... CLONE` on&lt;/span&gt;
&lt;span class="c1"&gt;-- them would overwrite the rebuild. `dbt clone` skips those.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/verify_preview_schema.py — smoke test after clone
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preview_schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snowflake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme-prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_ci&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;private_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;load_private_key&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBT_CI_ROLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBT_CI_WH_XS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANALYTICS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT COUNT(*)
            FROM   ANALYTICS.INFORMATION_SCHEMA.TABLES
            WHERE  TABLE_SCHEMA = %s
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preview_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(),))&lt;/span&gt;
        &lt;span class="n"&gt;n_tables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT COUNT(*)
            FROM   ANALYTICS.INFORMATION_SCHEMA.TABLES
            WHERE  TABLE_SCHEMA = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PROD&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;n_prod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;n_tables&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;n_prod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Preview schema has &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n_tables&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tables; prod has &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n_prod&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Slim CI + clone should have parity.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK — preview schema &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;preview_schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; has &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n_tables&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
    &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;dbt clone&lt;/code&gt; step runs &lt;em&gt;after&lt;/em&gt; the slim build so the rebuilt models already exist in the preview schema. Clone only fills in the gaps — the models that were not rebuilt because they were not &lt;code&gt;state:modified&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Under the hood, &lt;code&gt;dbt clone&lt;/code&gt; emits &lt;code&gt;CREATE OR REPLACE ... CLONE prod.&amp;lt;table&amp;gt;&lt;/code&gt; for each unmodified model. Snowflake's zero-copy clone is a metadata-only operation: no data is physically duplicated, no compute is billed, and clone completes in milliseconds per table.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--resource-type model --resource-type seed --resource-type snapshot&lt;/code&gt; scopes what gets cloned. Skip macros and analyses (they aren't materialised). Include seeds so hard-coded reference tables are present.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;verify_preview_schema.py&lt;/code&gt; smoke test confirms parity: the number of tables in the preview schema equals the number in prod. If it doesn't, something in the clone step silently failed and the preview would show missing-table errors to analysts.&lt;/li&gt;
&lt;li&gt;On PR close (see section 5), the cleanup workflow &lt;code&gt;DROP SCHEMA pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; reclaims all of it — the clones are metadata pointers, so dropping the schema releases nothing (no storage was ever allocated), but keeps the catalog tidy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model type&lt;/th&gt;
&lt;th&gt;After slim build&lt;/th&gt;
&lt;th&gt;After clone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modified model&lt;/td&gt;
&lt;td&gt;freshly materialized&lt;/td&gt;
&lt;td&gt;unchanged (skipped by clone)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unmodified upstream&lt;/td&gt;
&lt;td&gt;missing&lt;/td&gt;
&lt;td&gt;zero-copy clone from prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed&lt;/td&gt;
&lt;td&gt;(only if changed)&lt;/td&gt;
&lt;td&gt;zero-copy clone from prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot&lt;/td&gt;
&lt;td&gt;(only if changed)&lt;/td&gt;
&lt;td&gt;zero-copy clone from prod&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any dbt preview environment that humans will actually query, follow slim build with &lt;code&gt;dbt clone&lt;/code&gt; to backfill unmodified tables. Zero-copy cloning is free on Snowflake and Databricks UC; on BigQuery use table snapshots (also free); on Redshift you must physically CTAS, which costs — reconsider preview UX there or use &lt;code&gt;--defer&lt;/code&gt; alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on slim CI dbt
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You inherit a dbt project with 800 models running full &lt;code&gt;dbt build --target ci&lt;/code&gt; on every PR — 55-minute CI, $220 per run, analysts complain they can't review dashboards until merge. Design the slim CI migration: the state artifact contract, the CI workflow, the preview schema, the fallback for macro changes, and the on-call runbook when the prod manifest is missing or stale."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using slim CI with state comparison, deferral, &lt;code&gt;dbt clone&lt;/code&gt;, and a manifest freshness guard
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/dbt-slim-full.yml — production-grade slim CI&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-slim-ci&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;slim-ci&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DBT_PROFILES_DIR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./ci&lt;/span&gt;
      &lt;span class="na"&gt;DBT_PR_NUMBER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;${{ github.event.pull_request.number }}&lt;/span&gt;
      &lt;span class="na"&gt;DBT_BRANCH_SLUG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;${{ github.head_ref }}&lt;/span&gt;
      &lt;span class="na"&gt;STATE_BUCKET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;dbt-artifacts&lt;/span&gt;
      &lt;span class="na"&gt;STATE_KEY_PREFIX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod/latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dbt&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r dbt/requirements.txt&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::123456789012:role/gha-dbt-ci&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;

      &lt;span class="c1"&gt;# 1. Download prod manifest — with freshness guard&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download prod manifest&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manifest&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p prod-manifest&lt;/span&gt;
          &lt;span class="s"&gt;aws s3 cp s3://${STATE_BUCKET}/${STATE_KEY_PREFIX}/manifest.json prod-manifest/&lt;/span&gt;
          &lt;span class="s"&gt;aws s3 cp s3://${STATE_BUCKET}/${STATE_KEY_PREFIX}/generated_at.txt prod-manifest/&lt;/span&gt;
          &lt;span class="s"&gt;age_hours=$(( ( $(date +%s) - $(date -d "$(cat prod-manifest/generated_at.txt)" +%s) ) / 3600 ))&lt;/span&gt;
          &lt;span class="s"&gt;echo "age_hours=${age_hours}" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
          &lt;span class="s"&gt;if [ "$age_hours" -gt 48 ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "::warning::Prod manifest is ${age_hours}h old; slim CI may over-select"&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="c1"&gt;# 2. Detect macro-only or dbt-core changes — fall back to full build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Detect full-build triggers&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mode&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if git diff --name-only origin/main | grep -qE '(macros/|dbt_project\.yml|packages\.yml|requirements\.txt)'; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "mode=full" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;echo "mode=slim" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="c1"&gt;# 3. Compute preview schema&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compute preview schema name&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;schema&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;slug=$(echo "$DBT_BRANCH_SLUG" | tr '/' '_' | tr -cd '[:alnum:]_' | tr '[:upper:]' '[:lower:]' | cut -c1-40)&lt;/span&gt;
          &lt;span class="s"&gt;echo "schema=pr_${DBT_PR_NUMBER}_${slug}" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;

      &lt;span class="c1"&gt;# 4. dbt deps&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt deps&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt deps&lt;/span&gt;

      &lt;span class="c1"&gt;# 5. dbt build — slim or full&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt build&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DBT_SNOWFLAKE_PRIVATE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SNOWFLAKE_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;DBT_TARGET_SCHEMA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;         &lt;span class="s"&gt;${{ steps.schema.outputs.schema }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if [ "${{ steps.mode.outputs.mode }}" = "full" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "Full build (macro / dbt-core change detected)"&lt;/span&gt;
            &lt;span class="s"&gt;dbt build --target ci --fail-fast&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;echo "Slim build (state:modified+)"&lt;/span&gt;
            &lt;span class="s"&gt;dbt build \&lt;/span&gt;
              &lt;span class="s"&gt;--select state:modified+ \&lt;/span&gt;
              &lt;span class="s"&gt;--defer \&lt;/span&gt;
              &lt;span class="s"&gt;--state ../prod-manifest \&lt;/span&gt;
              &lt;span class="s"&gt;--target ci \&lt;/span&gt;
              &lt;span class="s"&gt;--fail-fast&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="c1"&gt;# 6. dbt clone — fill preview schema with unmodified prod tables&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt clone unmodified models&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.mode.outputs.mode == 'slim'&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DBT_SNOWFLAKE_PRIVATE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SNOWFLAKE_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;DBT_TARGET_SCHEMA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;         &lt;span class="s"&gt;${{ steps.schema.outputs.schema }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;dbt clone \&lt;/span&gt;
            &lt;span class="s"&gt;--state ../prod-manifest \&lt;/span&gt;
            &lt;span class="s"&gt;--target ci \&lt;/span&gt;
            &lt;span class="s"&gt;--resource-type model \&lt;/span&gt;
            &lt;span class="s"&gt;--resource-type seed \&lt;/span&gt;
            &lt;span class="s"&gt;--resource-type snapshot&lt;/span&gt;

      &lt;span class="c1"&gt;# 7. Upload CI manifest for downstream jobs and post PR comment&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci-manifest-${{ env.DBT_PR_NUMBER }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt/target/manifest.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post summary to PR&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DBT_MODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;            &lt;span class="s"&gt;${{ steps.mode.outputs.mode }}&lt;/span&gt;
          &lt;span class="na"&gt;DBT_PREVIEW_SCHEMA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;${{ steps.schema.outputs.schema }}&lt;/span&gt;
          &lt;span class="na"&gt;DBT_MANIFEST_AGE_H&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;${{ steps.manifest.outputs.age_hours }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const fs = require('fs');&lt;/span&gt;
            &lt;span class="s"&gt;const j  = JSON.parse(fs.readFileSync('dbt/target/run_results.json', 'utf8'));&lt;/span&gt;
            &lt;span class="s"&gt;const rows = j.results.slice(0, 40).map(r =&amp;gt;&lt;/span&gt;
              &lt;span class="s"&gt;`| ${r.unique_id} | ${r.status} | ${r.execution_time.toFixed(2)}s |`&lt;/span&gt;
            &lt;span class="s"&gt;).join('\n');&lt;/span&gt;
            &lt;span class="s"&gt;const body =&lt;/span&gt;
              &lt;span class="s"&gt;`### dbt CI — PR #${process.env.DBT_PR_NUMBER} (${process.env.DBT_MODE})\n\n` +&lt;/span&gt;
              &lt;span class="s"&gt;`Preview schema: \`${process.env.DBT_PREVIEW_SCHEMA}\`\n` +&lt;/span&gt;
              &lt;span class="s"&gt;`Manifest age: ${process.env.DBT_MANIFEST_AGE_H}h\n\n` +&lt;/span&gt;
              &lt;span class="s"&gt;`| Node | Status | Time |\n|---|---|---|\n${rows}\n`;&lt;/span&gt;
            &lt;span class="s"&gt;await github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
              &lt;span class="s"&gt;owner:  context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo:   context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;body:   body&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prod dbt job — uploads manifest at end of every successful run
# (runs in Airflow; upload snippet at task boundary)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upload_prod_manifest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt-artifacts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt/target/manifest.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod/latest/manifest.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt/target/run_results.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod/latest/run_results.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod/latest/generated_at.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Also keep a versioned copy for backfills
&lt;/span&gt;    &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;dT&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;SZ&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt/target/manifest.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/manifest.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manifest download&lt;/td&gt;
&lt;td&gt;pull prod-manifest/manifest.json + generated_at.txt&lt;/td&gt;
&lt;td&gt;freshness guard prevents stale state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest age check&lt;/td&gt;
&lt;td&gt;warn if &amp;gt;48h old&lt;/td&gt;
&lt;td&gt;stale manifest over-selects models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-build fallback&lt;/td&gt;
&lt;td&gt;trigger on macros/, dbt_project.yml, packages.yml, requirements.txt&lt;/td&gt;
&lt;td&gt;slim CI can miss macro impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview schema name&lt;/td&gt;
&lt;td&gt;pr__&lt;/td&gt;
&lt;td&gt;deterministic; sanitized for Snowflake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt build&lt;/td&gt;
&lt;td&gt;slim (state:modified+ --defer) or full&lt;/td&gt;
&lt;td&gt;mode-aware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt clone&lt;/td&gt;
&lt;td&gt;zero-copy clone unmodified models into preview&lt;/td&gt;
&lt;td&gt;preview UX = full mirror&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR comment&lt;/td&gt;
&lt;td&gt;model-by-model results with schema name&lt;/td&gt;
&lt;td&gt;reviewers see impact + preview URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest upload&lt;/td&gt;
&lt;td&gt;store dbt/target/manifest.json for downstream&lt;/td&gt;
&lt;td&gt;doc-preview / freshness jobs consume it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, a typical PR rebuilds 4-15 models in ~90 seconds against the preview schema, clones the remaining 785 tables in ~5 seconds (zero-copy metadata operations), and posts a PR comment with the model table + preview schema name; analysts open &lt;code&gt;pr_4712_add_orders_v2.orders_mart&lt;/code&gt; in Snowsight and see the new column immediately. Macro-touching PRs fall back to a full build (still under 15 minutes on the XS warehouse with parallel threads). The November invoice drops from ~$9k to ~$200 for CI compute; the average PR is reviewed within 10 minutes of open instead of the next day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median CI time&lt;/td&gt;
&lt;td&gt;55 min&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 CI cost per PR&lt;/td&gt;
&lt;td&gt;$220&lt;/td&gt;
&lt;td&gt;$0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview UX&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;full mirror on PR open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Macro-change safety&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;auto-fallback to full build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest staleness alert&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;warns at 48h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer feedback loop&lt;/td&gt;
&lt;td&gt;next day&lt;/td&gt;
&lt;td&gt;~10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;state:modified+ --defer --state&lt;/strong&gt;&lt;/strong&gt; — the slim CI trinity. &lt;code&gt;state:modified+&lt;/code&gt; scopes the build to only what changed and its descendants; &lt;code&gt;--defer&lt;/code&gt; routes unbuilt refs to prod; &lt;code&gt;--state&lt;/code&gt; points at the downloaded prod manifest. Together they make the CI job proportionate to the PR, not proportionate to the catalog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Prod manifest as artifact contract&lt;/strong&gt;&lt;/strong&gt; — the production run uploads &lt;code&gt;manifest.json&lt;/code&gt; to a well-known S3 key at the end of every successful run; the CI job downloads it at the top. This is the durable state contract that makes state comparison possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Manifest freshness guard&lt;/strong&gt;&lt;/strong&gt; — if the prod manifest is more than 48 hours old, slim CI may over-select (mark models as modified because prod is stale) or under-select (miss real changes because prod-manifest fingerprints don't match current prod). The warning surfaces the problem before it silently distorts the CI output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Macro-change full-build fallback&lt;/strong&gt;&lt;/strong&gt; — a change to &lt;code&gt;macros/&lt;/code&gt; can affect every model that references the macro without changing the model's SQL text. Slim CI would miss it. Detecting the touched paths and falling back to a full build closes the correctness gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;dbt clone for preview parity&lt;/strong&gt;&lt;/strong&gt; — slim CI only builds the changed subset; without cloning, the preview schema has 4 tables and analysts see missing-table errors. &lt;code&gt;dbt clone&lt;/code&gt; fills the gap in seconds at zero cost via zero-copy metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one prod-side upload step (~$0.00), one CI job with slim build + clone (~$0.30 typical), one XS warehouse with AUTO_SUSPEND=60. The eliminated cost is the 55-minute full rebuild ($220 per PR × 30 PRs/day = ~$200k/year). Net O(changed) per PR versus O(catalog) per PR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL modeling and warehouse-CI problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on incremental modeling&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Airflow CI — DAG parse, integration tests, deployable artifacts
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;DagBag(import_errors={})&lt;/code&gt; is the first gate every Airflow monorepo needs — parse before you promote
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;airflow ci cd&lt;/code&gt; is the pattern where every PR that touches &lt;code&gt;airflow/&lt;/code&gt; runs three gates in sequence — a DAG-parse gate that constructs a &lt;code&gt;DagBag&lt;/code&gt; in under 30 seconds and asserts zero &lt;code&gt;import_errors&lt;/code&gt;, a unit-test matrix that exercises custom operators and hooks with mocked backends, and (optionally) an integration test that runs the changed DAG end-to-end against a &lt;code&gt;LocalExecutor&lt;/code&gt; — before the versioned deployable artifact (an OCI image, a &lt;code&gt;dags.zip&lt;/code&gt; for MWAA, or an Astronomer deployment bundle) is even built — because a DAG that fails at import time takes down every other DAG in the DagBag, not just its own scheduling&lt;/strong&gt;. Every senior Airflow deployment has been rescued by the parse gate at least once; skipping it turns "one broken DAG" into "the whole scheduler is red."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjp88adgfvkjnsytznaf4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjp88adgfvkjnsytznaf4.jpeg" alt="Iconographic Airflow CI diagram — DAG parse gate card, unit test grid, and artifact bundle cylinder with version tag." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for Airflow CI.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correctness.&lt;/strong&gt; Three levels: parse (does every DAG file import without error?), unit (do custom operators return the right values for mocked inputs?), integration (does a whole DAG execute end-to-end?). Each layer catches a different class of bug. The parse gate is non-negotiable; the unit tests are the coverage layer; the integration test is the "does this DAG actually work?" layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Airflow CI is essentially free — parse and unit tests run in seconds on &lt;code&gt;ubuntu-latest&lt;/code&gt;. The optional integration test is more expensive because it spins a Postgres + LocalExecutor, but still under $0.10 per run. Cost is not the axis that bites for Airflow CI; latency is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency-to-merge.&lt;/strong&gt; Parse gate in ~15 seconds is the fastest fail loop of any data-pipeline CI gate. Unit tests in 2-3 minutes. Integration tests in 4-6 minutes. A DAG-only PR should complete CI in under 5 minutes p95.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius.&lt;/strong&gt; A DAG that fails at import time fails &lt;em&gt;every&lt;/em&gt; DAG in the DagBag — the scheduler pauses because &lt;code&gt;dagbag_import_error_traceback_depth&lt;/code&gt; triggers. The parse gate stops that class of change from reaching the scheduler at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The DAG parse gate — 15 seconds, one assertion.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The mechanism.&lt;/strong&gt; Instantiate &lt;code&gt;DagBag(dag_folder="airflow/dags", include_examples=False)&lt;/code&gt; in a subprocess. If any DAG file raises during import, &lt;code&gt;dagbag.import_errors&lt;/code&gt; is a non-empty dict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The assertion.&lt;/strong&gt; &lt;code&gt;assert not dagbag.import_errors, dagbag.import_errors&lt;/code&gt;. The assertion message dumps the file paths and tracebacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trap.&lt;/strong&gt; DAG files often do work at module import time (query metadata DB, hit Airflow variables, call &lt;code&gt;Variable.get&lt;/code&gt;). CI runs without those variables set. Solution: use &lt;code&gt;Variable.get("x", default_var="ci_placeholder")&lt;/code&gt; or gate DAG-time work behind &lt;code&gt;if not os.getenv("SKIP_DAG_INIT_WORK")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The timing.&lt;/strong&gt; A modern DagBag with 200 DAGs parses in 5-15 seconds. The 30-second Airflow scheduler timeout is your reference point — if CI takes longer, the scheduler will too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The operator + hook unit-test matrix.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it covers.&lt;/strong&gt; Custom operators (&lt;code&gt;class MyOperator(BaseOperator)&lt;/code&gt;), custom hooks (&lt;code&gt;class MyHook(BaseHook)&lt;/code&gt;), custom sensors, and utilities. Everything you &lt;em&gt;wrote&lt;/em&gt; rather than imported from &lt;code&gt;airflow&lt;/code&gt; or a provider package.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to structure tests.&lt;/strong&gt; &lt;code&gt;tests/operators/test_my_operator.py&lt;/code&gt;; instantiate the operator with mock upstream context; assert on the return value or the mock's call args.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What to mock.&lt;/strong&gt; External services (S3, Snowflake, Kafka) with &lt;code&gt;unittest.mock&lt;/code&gt; or &lt;code&gt;moto&lt;/code&gt; for AWS. XCom pushes; assert via &lt;code&gt;mock_ti.xcom_push.assert_called_with(...)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What not to mock.&lt;/strong&gt; Airflow's own execution machinery — use &lt;code&gt;airflow.models.taskinstance.TaskInstance&lt;/code&gt; in "test mode" where possible. Excessive mocking of Airflow itself creates tests that pass but tell you nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The DAG integration test — LocalExecutor + ephemeral Postgres.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When to run.&lt;/strong&gt; For DAGs that touch shared resources (a common warehouse table, a critical Kafka topic) or for DAGs that use complex dynamic task mapping. Not every DAG needs one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The setup.&lt;/strong&gt; &lt;code&gt;airflow db init&lt;/code&gt; against a Postgres in docker-compose; &lt;code&gt;airflow dags test &amp;lt;dag_id&amp;gt; &amp;lt;execution_date&amp;gt;&lt;/code&gt; runs the DAG end-to-end synchronously without a scheduler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The runtime.&lt;/strong&gt; 30 seconds to 6 minutes depending on DAG complexity. Cap the integration-test matrix at the DAGs whose failure would cost more than 6 minutes of CI time to catch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The deployable artifact — MWAA vs Astronomer vs self-hosted.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MWAA.&lt;/strong&gt; &lt;code&gt;dags.zip&lt;/code&gt; uploaded to S3; &lt;code&gt;requirements.txt&lt;/code&gt; in the same bucket; &lt;code&gt;plugins.zip&lt;/code&gt; for custom operators. Versioned by S3 object version + &lt;code&gt;airflow_version&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Astronomer.&lt;/strong&gt; &lt;code&gt;astro deploy&lt;/code&gt; builds an OCI image, tags it, and rolls it out to the deployment. Versioned by image digest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted on Kubernetes.&lt;/strong&gt; Custom OCI image built from &lt;code&gt;apache/airflow:2.10.x&lt;/code&gt; with your DAGs baked in; Helm chart with the image tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The versioning contract.&lt;/strong&gt; Every deployable artifact carries the git SHA, the branch name, the CI run ID, and the Airflow version. Any deployed artifact must be traceable to a specific commit and CI run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on Airflow CI.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What's the first CI check for Airflow?" — required answer: DAG parse gate (&lt;code&gt;DagBag(import_errors={})&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;"Why does DAG parse fail differently from operator execution?" — required answer: parse errors take down every DAG in the DagBag, not just the broken one.&lt;/li&gt;
&lt;li&gt;"How do you unit-test a custom operator?" — instantiate, provide mock context, assert on return / mock calls.&lt;/li&gt;
&lt;li&gt;"How do you handle Airflow Variables in CI?" — default-value pattern; do not connect to prod metadata DB from CI.&lt;/li&gt;
&lt;li&gt;"How do you version a deployable Airflow artifact?" — git SHA + branch + CI run ID + Airflow version, baked into the image label or zip metadata.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — GitHub Actions matrix for DAG parse + operator unit tests
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Airflow CI matrix: one job for DAG parse (fastest), one job for the operator unit tests (parallel), one job (conditional) for the DAG integration test. Build the whole thing from scratch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Matrix.&lt;/strong&gt; parse (always), unit (always), integration (only if DAG files changed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runners.&lt;/strong&gt; ubuntu-latest for parse + unit; ubuntu-latest with docker-compose for integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python.&lt;/strong&gt; 3.11 (match the Airflow deployment).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airflow.&lt;/strong&gt; 2.10.x pinned to match prod.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the GitHub Actions workflow that runs parse + unit tests + optional integration on every airflow-touching PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;When it runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dag-parse&lt;/td&gt;
&lt;td&gt;15-30 s&lt;/td&gt;
&lt;td&gt;every airflow-touching PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;operator-unit&lt;/td&gt;
&lt;td&gt;2-3 min&lt;/td&gt;
&lt;td&gt;every airflow-touching PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dag-integration&lt;/td&gt;
&lt;td&gt;4-6 min&lt;/td&gt;
&lt;td&gt;only if &lt;code&gt;airflow/dags/**&lt;/code&gt; changed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/airflow.yml — parse, unit, integration&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow-ci&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# 1. DAG parse gate — fastest fail, 15-30 s&lt;/span&gt;
  &lt;span class="na"&gt;dag-parse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW_HOME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                       &lt;span class="s"&gt;/tmp/airflow&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__LOAD_EXAMPLES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;False'&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__UNIT_TEST_MODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;True'&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__EXECUTOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;            &lt;span class="s"&gt;SequentialExecutor&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Airflow + providers&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pip install -r airflow/requirements-ci.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Initialize Airflow metadata DB&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow db migrate&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Parse every DAG file&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python -m airflow.utils.cli_action_loggers ; python scripts/dag_parse_gate.py&lt;/span&gt;

  &lt;span class="c1"&gt;# 2. Operator + hook unit tests — 2-3 min&lt;/span&gt;
  &lt;span class="na"&gt;operator-unit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW_HOME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                       &lt;span class="s"&gt;/tmp/airflow&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__LOAD_EXAMPLES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;False'&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__UNIT_TEST_MODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;True'&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r airflow/requirements-ci.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run unit tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest airflow/tests/operators airflow/tests/hooks -v --tb=short&lt;/span&gt;

  &lt;span class="c1"&gt;# 3. DAG integration — only when a DAG file changed&lt;/span&gt;
  &lt;span class="na"&gt;dag-integration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dag-parse&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;contains(join(github.event.pull_request.changed_files, ','), 'airflow/dags/')&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW_HOME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                       &lt;span class="s"&gt;/tmp/airflow&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__LOAD_EXAMPLES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;False'&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__EXECUTOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;            &lt;span class="s"&gt;LocalExecutor&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__DATABASE__SQL_ALCHEMY_CONN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://airflow:airflow@localhost/airflow&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;airflow&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;airflow&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5432:5432'&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--health-cmd "pg_isready -U airflow"&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r airflow/requirements-ci.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Init metadata DB&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;airflow db migrate&lt;/span&gt;
          &lt;span class="s"&gt;airflow connections add fs_default --conn-type fs&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;List changed DAG ids&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dags&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;changed=$(git diff --name-only origin/main -- 'airflow/dags/*.py' | \&lt;/span&gt;
                     &lt;span class="s"&gt;xargs -I{} python -c 'import ast, sys, pathlib&lt;/span&gt;
&lt;span class="s"&gt;p = pathlib.Path(sys.argv[1])&lt;/span&gt;
&lt;span class="s"&gt;tree = ast.parse(p.read_text())&lt;/span&gt;
&lt;span class="na"&gt;for n in ast.walk(tree)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;if isinstance(n, ast.Assign)&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;for t in n.targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="s"&gt;if getattr(t, "id", None) == "DAG_ID"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
                &lt;span class="s"&gt;if isinstance(n.value, ast.Constant)&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
                    &lt;span class="s"&gt;print(n.value.value)' {})&lt;/span&gt;
          &lt;span class="s"&gt;echo "ids=${changed}" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow dags test each changed DAG&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;for dag_id in ${{ steps.dags.outputs.ids }}; do&lt;/span&gt;
            &lt;span class="s"&gt;echo "=== testing $dag_id ==="&lt;/span&gt;
            &lt;span class="s"&gt;airflow dags test "$dag_id" 2026-07-30&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/dag_parse_gate.py — the parse gate assertion
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Assert every DAG in airflow/dags/ imports without error.

The gate mirrors the check the Airflow scheduler performs — if any DAG
raises during import, the scheduler flags an `import_errors` entry and
the DAG is invisible until fixed. Catching this in CI stops broken DAGs
from ever reaching the scheduler.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.models.dagbag&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DagBag&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;dag_folder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow/dags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;dag_folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dag_folder&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; does not exist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

    &lt;span class="n"&gt;dagbag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DagBag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag_folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag_folder&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;include_examples&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;import_errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DAG parse errors detected:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;import_errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;--- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ---&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK — &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; DAGs parsed with 0 import errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dag_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dags&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  • &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# airflow/tests/operators/test_reverse_etl_operator.py — unit test example
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;unittest.mock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MagicMock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.utils.context&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow_plugins.operators.reverse_etl&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ReverseEtlOperator&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;op&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ReverseEtlOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_reverse_etl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;source_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analytics.customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;target_system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hubspot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;target_object&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contacts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_operator_builds_correct_payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Given a source row, the operator should build the expected API payload.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ada&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_build_payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firstname&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ada&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nd"&gt;@patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow_plugins.operators.reverse_etl.SnowflakeHook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow_plugins.operators.reverse_etl.HubspotHook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_operator_execute_calls_hubspot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mock_hs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mock_sf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Execute should read from Snowflake and POST to Hubspot.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;mock_sf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ada&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MagicMock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mock_hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upsert_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_count&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mock_hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upsert_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_args&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;dag-parse&lt;/code&gt; job is the fastest possible fail. It installs Airflow, migrates a SQLite metadata DB (fast), and runs &lt;code&gt;dag_parse_gate.py&lt;/code&gt; which instantiates a &lt;code&gt;DagBag&lt;/code&gt; and asserts &lt;code&gt;import_errors&lt;/code&gt; is empty. Under 30 seconds; catches the highest-impact class of DAG bug.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AIRFLOW__CORE__UNIT_TEST_MODE=True&lt;/code&gt; tells Airflow to use in-memory connections and skip heavy initialisation; &lt;code&gt;AIRFLOW__CORE__EXECUTOR=SequentialExecutor&lt;/code&gt; avoids requiring a real executor. Both are CI-only environment tricks.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;operator-unit&lt;/code&gt; job runs pytest against &lt;code&gt;airflow/tests/operators&lt;/code&gt; and &lt;code&gt;airflow/tests/hooks&lt;/code&gt;. Custom operators get mocked hooks (&lt;code&gt;@patch("...SnowflakeHook")&lt;/code&gt;); the test asserts on &lt;code&gt;mock_hs.return_value.upsert_contact.call_args&lt;/code&gt;. Coverage bar: every custom operator has at least one happy-path and one error-path test.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;dag-integration&lt;/code&gt; job runs only if a DAG file changed. It spins up Postgres as a GitHub Actions service, migrates the Airflow metadata DB against Postgres, extracts the changed DAG IDs, and runs &lt;code&gt;airflow dags test &amp;lt;dag_id&amp;gt; &amp;lt;date&amp;gt;&lt;/code&gt; on each. This runs the DAG end-to-end without the scheduler.&lt;/li&gt;
&lt;li&gt;The AST-based extraction of &lt;code&gt;DAG_ID&lt;/code&gt; from changed DAG files is a pragmatic way to run only the changed DAGs. Alternatives: parse the whole DagBag and match by file path; require every DAG to be tagged with the file it's in. Both work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dag-parse&lt;/td&gt;
&lt;td&gt;15-30 s&lt;/td&gt;
&lt;td&gt;import errors, syntax errors, missing imports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;operator-unit&lt;/td&gt;
&lt;td&gt;2-3 min&lt;/td&gt;
&lt;td&gt;logic errors in custom operators / hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dag-integration&lt;/td&gt;
&lt;td&gt;4-6 min&lt;/td&gt;
&lt;td&gt;wiring errors, DAG-level task interactions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For every Airflow CI setup, run parse first (fastest fail), unit second (broad coverage), integration third (deep coverage on high-value DAGs). Never skip the parse gate; it is 30 seconds and catches the single most common Airflow bug.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — full-DAG integration test with the LocalExecutor
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The DAG integration test catches wiring bugs the unit tests cannot: task A returns X but task B expects Y, dynamic task mapping produces the wrong fan-out, or a &lt;code&gt;TaskGroup&lt;/code&gt; fails to render under a specific execution date. Walk through a concrete integration test for a DAG that dedupes an S3 landing zone into a Snowflake table.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DAG under test.&lt;/strong&gt; &lt;code&gt;s3_to_snowflake_dedupe&lt;/code&gt; — 4 tasks: sensor (waits for S3 file) → download → dedupe → merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixtures needed.&lt;/strong&gt; LocalStack S3 (via moto), Snowflake mock (via &lt;code&gt;snowflake.connector&lt;/code&gt; mock).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution date.&lt;/strong&gt; A fixed date so the test is deterministic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write an integration test that runs the whole DAG end-to-end and asserts the target Snowflake table receives the expected rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DAG under test&lt;/td&gt;
&lt;td&gt;s3_to_snowflake_dedupe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 backend&lt;/td&gt;
&lt;td&gt;moto (in-process)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake backend&lt;/td&gt;
&lt;td&gt;monkey-patched connector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution date&lt;/td&gt;
&lt;td&gt;2026-07-30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixture rows&lt;/td&gt;
&lt;td&gt;3 rows, one duplicate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# airflow/tests/dags/test_s3_to_snowflake_dedupe.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;unittest.mock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MagicMock&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;moto&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mock_aws&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.models.dagbag&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DagBag&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.utils.state&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;module&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DagBag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag_folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow/dags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;include_examples&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;s3_landing&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;mock_aws&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;landing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;landing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders/2026/07/30/orders.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:1,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:100}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
                 &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:2,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:200}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
                 &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:1,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:100}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# duplicate
&lt;/span&gt;        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;snowflake_stub&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Replace snowflake.connector.connect with a MagicMock that captures MERGE calls.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake.connector.connect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mock_connect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;mock_conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MagicMock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;mock_connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mock_conn&lt;/span&gt;
        &lt;span class="n"&gt;mock_cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MagicMock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;mock_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__enter__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mock_cur&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;mock_cur&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_s3_to_snowflake_dedupe_end_to_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s3_landing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snowflake_stub&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run the DAG end-to-end and assert the MERGE receives 2 deduped rows.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;dag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3_to_snowflake_dedupe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DAG not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;execution_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# airflow dags test — synchronous, no scheduler needed
&lt;/span&gt;    &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Verify the merge was executed with the deduped payload
&lt;/span&gt;    &lt;span class="n"&gt;merge_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;snowflake_stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_args_list&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MERGE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merge_calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected 1 MERGE call, got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merge_calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;merge_sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;merge_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders_dedupe_stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;merge_sql&lt;/span&gt;
    &lt;span class="c1"&gt;# Row count sanity
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ti&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_task_instances&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;ti&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SKIPPED&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ti&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ended in state &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ti&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;dagbag&lt;/code&gt; fixture is module-scoped so parsing runs once for the whole test file. Repeated &lt;code&gt;DagBag()&lt;/code&gt; calls per test would inflate CI time by 10-30 seconds per test.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;moto&lt;/code&gt; provides an in-process S3 mock that behaves like real S3 (bucket ops, put_object, list_objects_v2) without any network calls. &lt;code&gt;mock_aws()&lt;/code&gt; is the modern moto 5.x context manager.&lt;/li&gt;
&lt;li&gt;The Snowflake stub replaces &lt;code&gt;snowflake.connector.connect&lt;/code&gt; at import time, so any operator code that calls &lt;code&gt;snowflake.connector.connect()&lt;/code&gt; receives a MagicMock instead of a real connection. All &lt;code&gt;execute&lt;/code&gt; calls are captured in &lt;code&gt;mock_cur.execute.call_args_list&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dag.test(execution_date=...)&lt;/code&gt; (added in Airflow 2.5) runs the entire DAG synchronously in-process. No scheduler, no executor, no workers — just a straight-through execution. This is the CI-friendly way to run a DAG; &lt;code&gt;airflow dags test&lt;/code&gt; from the CLI is the equivalent.&lt;/li&gt;
&lt;li&gt;The assertions walk the captured Snowflake calls, find the MERGE statement, and verify the deduped row count. Alternative assertion styles: parse the MERGE SQL and check the row count; assert on the task's XCom returns; check the mock's total call count.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;wait_for_s3&lt;/td&gt;
&lt;td&gt;SUCCESS&lt;/td&gt;
&lt;td&gt;poked once; file present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;download_landing&lt;/td&gt;
&lt;td&gt;SUCCESS&lt;/td&gt;
&lt;td&gt;3 rows from S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dedupe_orders&lt;/td&gt;
&lt;td&gt;SUCCESS&lt;/td&gt;
&lt;td&gt;2 unique order_ids&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;merge_to_snowflake&lt;/td&gt;
&lt;td&gt;SUCCESS&lt;/td&gt;
&lt;td&gt;MERGE call captured with 2 rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For DAGs that touch high-value shared resources (a warehouse table, a critical topic), invest in a &lt;code&gt;dag.test()&lt;/code&gt;-based integration test with moto/monkey-patch stubs. For simpler DAGs, the parse gate + operator unit tests are sufficient. Don't try to integration-test every DAG — the maintenance cost outstrips the value.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — build and version a deployable Airflow image
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; After CI passes, the last CI job builds the deployable artifact. For self-hosted Kubernetes Airflow, this is an OCI image; for MWAA, it's a &lt;code&gt;dags.zip&lt;/code&gt; + &lt;code&gt;requirements.txt&lt;/code&gt; uploaded to S3; for Astronomer, it's &lt;code&gt;astro deploy&lt;/code&gt;. Walk through the OCI image path — the most versatile.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base.&lt;/strong&gt; &lt;code&gt;apache/airflow:2.10.3-python3.11&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlay.&lt;/strong&gt; Copy &lt;code&gt;airflow/dags&lt;/code&gt;, &lt;code&gt;airflow/plugins&lt;/code&gt;, and install &lt;code&gt;airflow/requirements-prod.txt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version labels.&lt;/strong&gt; git SHA, branch, CI run ID, Airflow version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registry.&lt;/strong&gt; &lt;code&gt;ghcr.io/acme/airflow&lt;/code&gt; tagged with &lt;code&gt;sha-&amp;lt;short&amp;gt;&lt;/code&gt; and &lt;code&gt;pr-&amp;lt;PR_NUMBER&amp;gt;&lt;/code&gt; (for PRs) or &lt;code&gt;main&lt;/code&gt; (for merges).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the Dockerfile and the GitHub Actions job that builds, tags, and pushes the versioned image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base image&lt;/td&gt;
&lt;td&gt;apache/airflow:2.10.3-python3.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry&lt;/td&gt;
&lt;td&gt;ghcr.io/acme/airflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tag scheme&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sha-&amp;lt;short&amp;gt;&lt;/code&gt;, &lt;code&gt;pr-&amp;lt;PR&amp;gt;&lt;/code&gt;, &lt;code&gt;main&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Labels&lt;/td&gt;
&lt;td&gt;git SHA, branch, ci run id, airflow version&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# airflow/Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; apache/airflow:2.10.3-python3.11&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; GIT_SHA=unknown&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; GIT_BRANCH=unknown&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; CI_RUN_ID=unknown&lt;/span&gt;

&lt;span class="k"&gt;LABEL&lt;/span&gt;&lt;span class="s"&gt; org.opencontainers.image.source="https://github.com/acme/data-monorepo"&lt;/span&gt;
&lt;span class="k"&gt;LABEL&lt;/span&gt;&lt;span class="s"&gt; org.opencontainers.image.revision="${GIT_SHA}"&lt;/span&gt;
&lt;span class="k"&gt;LABEL&lt;/span&gt;&lt;span class="s"&gt; org.opencontainers.image.ref.name="${GIT_BRANCH}"&lt;/span&gt;
&lt;span class="k"&gt;LABEL&lt;/span&gt;&lt;span class="s"&gt; ai.pipecode.ci.run.id="${CI_RUN_ID}"&lt;/span&gt;
&lt;span class="k"&gt;LABEL&lt;/span&gt;&lt;span class="s"&gt; ai.pipecode.airflow.version="2.10.3"&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; root&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; git &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; airflow&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=airflow:root requirements-prod.txt /requirements.txt&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; /requirements.txt

&lt;span class="c"&gt;# DAGs and plugins baked into the image&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=airflow:root dags     /opt/airflow/dags&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=airflow:root plugins  /opt/airflow/plugins&lt;/span&gt;

&lt;span class="c"&gt;# Sanity: parse gate at build time&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from airflow.models.dagbag import DagBag; &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s2"&gt;               dagbag = DagBag('/opt/airflow/dags', include_examples=False); &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s2"&gt;               assert not dagbag.import_errors, dagbag.import_errors; &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s2"&gt;               print(f'{len(dagbag.dags)} DAGs, 0 errors')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/airflow-build.yml — build + push deployable image&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow-build&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.actor }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compute tags&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tags&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;short_sha=$(git rev-parse --short HEAD)&lt;/span&gt;
          &lt;span class="s"&gt;echo "sha_tag=ghcr.io/acme/airflow:sha-${short_sha}" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
          &lt;span class="s"&gt;if [ "${{ github.event_name }}" = "pull_request" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "extra_tag=ghcr.io/acme/airflow:pr-${{ github.event.pull_request.number }}" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;echo "extra_tag=ghcr.io/acme/airflow:main" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow&lt;/span&gt;
          &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ steps.tags.outputs.sha_tag }}&lt;/span&gt;
            &lt;span class="s"&gt;${{ steps.tags.outputs.extra_tag }}&lt;/span&gt;
          &lt;span class="na"&gt;build-args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;GIT_SHA=${{ github.sha }}&lt;/span&gt;
            &lt;span class="s"&gt;GIT_BRANCH=${{ github.ref_name }}&lt;/span&gt;
            &lt;span class="s"&gt;CI_RUN_ID=${{ github.run_id }}&lt;/span&gt;
          &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
          &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;type=gha,mode=max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Dockerfile takes &lt;code&gt;apache/airflow:2.10.3-python3.11&lt;/code&gt; as its base — pinning the patch version prevents accidental upgrades. Use the same tag as prod for tightest parity.&lt;/li&gt;
&lt;li&gt;Build args flow into &lt;code&gt;LABEL&lt;/code&gt;s so the running image self-describes: &lt;code&gt;docker inspect ghcr.io/acme/airflow:sha-abc123 | jq '.[0].Config.Labels'&lt;/code&gt; reveals git SHA, branch, CI run ID, and Airflow version. Any deployed image can be traced back to a specific commit and CI run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requirements-prod.txt&lt;/code&gt; is installed before the DAGs are copied so Docker's layer cache re-uses the pip layer across DAG-only changes — typical CI build drops from 90 s to 15 s on a DAG-only PR.&lt;/li&gt;
&lt;li&gt;The RUN parse-gate at build time is a belt-and-braces check: even if the CI job's parse gate somehow passed a broken DAG, the image build will fail. This is the last chance to catch an import error before the image goes to the registry.&lt;/li&gt;
&lt;li&gt;Tags: &lt;code&gt;sha-&amp;lt;short&amp;gt;&lt;/code&gt; for every commit (immutable, provenance-preserving) plus &lt;code&gt;pr-&amp;lt;PR_NUMBER&amp;gt;&lt;/code&gt; (mutable, points at latest CI build for the PR) or &lt;code&gt;main&lt;/code&gt; (mutable, points at latest merge). The immutable sha tag is what deployments actually reference.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Tags pushed&lt;/th&gt;
&lt;th&gt;Traceable to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PR open / update&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sha-&amp;lt;short&amp;gt;&lt;/code&gt;, &lt;code&gt;pr-&amp;lt;PR&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;commit SHA + CI run ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge to main&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sha-&amp;lt;short&amp;gt;&lt;/code&gt;, &lt;code&gt;main&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;commit SHA + CI run ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release tag&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sha-&amp;lt;short&amp;gt;&lt;/code&gt;, &lt;code&gt;v&amp;lt;VERSION&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;commit SHA + git tag&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For every Airflow deployment, bake DAGs into the image, parse-gate at build time as a safety net, and use immutable &lt;code&gt;sha-&amp;lt;short&amp;gt;&lt;/code&gt; tags for actual deployments. The mutable &lt;code&gt;main&lt;/code&gt; / &lt;code&gt;pr-&amp;lt;PR&amp;gt;&lt;/code&gt; tags are for humans; deployments reference the immutable ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on Airflow CI
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your Airflow deployment (MWAA on Airflow 2.10) has 180 DAGs. Once a month, a DAG merges that breaks at import time and the scheduler pauses for the whole DagBag. Design the CI workflow that catches this before merge — the parse gate, the operator unit-test coverage bar, the DAG integration test policy for high-value DAGs, and the deployable-artifact contract including version labels."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a parse gate + unit matrix + integration matrix + labelled OCI image
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/airflow-full.yml — production-grade Airflow CI&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow-ci&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# 1. Parse gate — fastest fail&lt;/span&gt;
  &lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW_HOME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                       &lt;span class="s"&gt;/tmp/airflow&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__LOAD_EXAMPLES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;False'&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__UNIT_TEST_MODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;True'&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r airflow/requirements-ci.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow db migrate&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/dag_parse_gate.py&lt;/span&gt;

  &lt;span class="c1"&gt;# 2. Operator/hook unit tests — parallel&lt;/span&gt;
  &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;suite&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sensors&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;utils&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW_HOME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                       &lt;span class="s"&gt;/tmp/airflow&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__LOAD_EXAMPLES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;False'&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__UNIT_TEST_MODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;True'&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r airflow/requirements-ci.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest ${{ matrix.suite }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest airflow/tests/${{ matrix.suite }} -v --tb=short&lt;/span&gt;

  &lt;span class="c1"&gt;# 3. Integration tests for changed DAGs&lt;/span&gt;
  &lt;span class="na"&gt;integration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;parse&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;contains(join(github.event.pull_request.changed_files, ','), 'airflow/dags/')&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;airflow&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;airflow&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5432:5432'&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__DATABASE__SQL_ALCHEMY_CONN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://airflow:airflow@localhost/airflow&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__EXECUTOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;             &lt;span class="s"&gt;LocalExecutor&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r airflow/requirements-ci.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow db migrate&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest airflow/tests/integration -v --tb=short&lt;/span&gt;

  &lt;span class="c1"&gt;# 4. Build labelled deployable OCI image&lt;/span&gt;
  &lt;span class="na"&gt;build-image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;parse&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;unit&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;integration&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always() &amp;amp;&amp;amp; needs.parse.result == 'success' &amp;amp;&amp;amp; needs.unit.result == 'success' &amp;amp;&amp;amp; (needs.integration.result == 'success' || needs.integration.result == 'skipped')&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.actor }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build + push&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow&lt;/span&gt;
          &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;ghcr.io/acme/airflow:sha-${{ github.sha }}&lt;/span&gt;
            &lt;span class="s"&gt;ghcr.io/acme/airflow:pr-${{ github.event.pull_request.number }}&lt;/span&gt;
          &lt;span class="na"&gt;build-args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;GIT_SHA=${{ github.sha }}&lt;/span&gt;
            &lt;span class="s"&gt;GIT_BRANCH=${{ github.ref_name }}&lt;/span&gt;
            &lt;span class="s"&gt;CI_RUN_ID=${{ github.run_id }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Airflow CI budget contract (per gate)
Gate           p95 latency   Coverage target
─────────────────────────────────────────────
parse          30 s          100% of DAG files
unit-operators 3 min         95% line coverage on custom operators
unit-hooks     2 min         95% line coverage on custom hooks
integration    6 min         100% of DAGs touching shared warehouse tables
build-image    2 min         every merge; every PR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;parse&lt;/td&gt;
&lt;td&gt;DagBag(include_examples=False) + assert not import_errors&lt;/td&gt;
&lt;td&gt;catches syntax + import errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unit (matrix)&lt;/td&gt;
&lt;td&gt;pytest airflow/tests/{operators,hooks,sensors,utils}&lt;/td&gt;
&lt;td&gt;catches operator logic errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;integration&lt;/td&gt;
&lt;td&gt;LocalExecutor + Postgres service + pytest airflow/tests/integration&lt;/td&gt;
&lt;td&gt;catches DAG wiring errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;build-image&lt;/td&gt;
&lt;td&gt;apache/airflow:2.10.3 + copy DAGs + build-time parse check + labels&lt;/td&gt;
&lt;td&gt;produces immutable sha-tagged artifact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version labels&lt;/td&gt;
&lt;td&gt;GIT_SHA / GIT_BRANCH / CI_RUN_ID as OCI LABELs&lt;/td&gt;
&lt;td&gt;every deployment traceable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, the once-a-month "broken DAG paused the scheduler" incident drops to zero — the parse gate catches every broken import before merge. The p95 CI time for an Airflow-only PR is about 5 minutes; the p95 cost is about $0.15. The operator unit-test coverage bar (95%) is enforced by pytest-cov + &lt;code&gt;--cov-fail-under=95&lt;/code&gt;. Every deployed image has git SHA + branch + CI run ID + Airflow version in its labels, so any prod incident can be traced back to a specific commit in seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Broken-DAG incidents&lt;/td&gt;
&lt;td&gt;~1/month&lt;/td&gt;
&lt;td&gt;~0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 CI time (Airflow-only PR)&lt;/td&gt;
&lt;td&gt;12 min&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 CI cost (Airflow-only PR)&lt;/td&gt;
&lt;td&gt;$0.60&lt;/td&gt;
&lt;td&gt;$0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operator unit-test coverage&lt;/td&gt;
&lt;td&gt;~40%&lt;/td&gt;
&lt;td&gt;95% (enforced)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image provenance&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;git SHA + branch + run ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAG integration coverage&lt;/td&gt;
&lt;td&gt;0 DAGs&lt;/td&gt;
&lt;td&gt;~30 high-value DAGs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Parse gate first&lt;/strong&gt;&lt;/strong&gt; — the single highest-ROI CI check. 30 seconds, one assertion, catches the highest-impact class of Airflow bug (import-time errors that pause the whole DagBag). Every Airflow project should have this on day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Unit-test matrix&lt;/strong&gt;&lt;/strong&gt; — pytest matrix parallelises the operator / hook / sensor / util suites. Coverage bar (95%) enforced in CI via &lt;code&gt;--cov-fail-under&lt;/code&gt;. Regressions in custom operator logic caught before merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Integration test for shared-resource DAGs&lt;/strong&gt;&lt;/strong&gt; — LocalExecutor + Postgres service + &lt;code&gt;dag.test()&lt;/code&gt; runs the whole DAG end-to-end in ~1 minute per DAG. Scoped to DAGs that touch shared warehouse tables so the CI time doesn't explode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Labelled OCI image&lt;/strong&gt;&lt;/strong&gt; — every image carries git SHA, branch, CI run ID, and Airflow version as OCI labels. &lt;code&gt;docker inspect&lt;/code&gt; on any deployed image reveals its provenance in one command. Immutable &lt;code&gt;sha-&amp;lt;short&amp;gt;&lt;/code&gt; tag is the actual deployment target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Build-time parse gate&lt;/strong&gt;&lt;/strong&gt; — a belt-and-braces &lt;code&gt;RUN python -c "DagBag(...)"&lt;/code&gt; inside the Dockerfile. If the CI parse-gate somehow passed a broken DAG (e.g. Airflow version mismatch), the image build fails before push. Defence in depth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one parse gate ($0.02), one unit matrix ($0.05), one conditional integration ($0.08), one image build ($0.02). Total ~$0.15 per PR. Compared to the "broken DAG paused scheduler for 4 hours" incident cost (~$800 in delayed data), this is essentially free. Net O(changed) per PR versus O(catalog) impact of every prod incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Python&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — python&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Python operator and DAG testing problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/python" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;API&lt;/span&gt;
&lt;span&gt;Topic — api-integration&lt;/span&gt;
&lt;strong&gt;API integration problems on Airflow hooks&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/api-integration" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Spark CI — packaged jobs, unit + integration tests, dry-run submits
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;spark-submit --deploy-mode client --num-executors 0&lt;/code&gt; is the dry-run gate every Spark CI needs before the real cluster is billed
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;spark ci cd&lt;/code&gt; is the pattern where every PR that touches &lt;code&gt;spark/&lt;/code&gt; runs three gates — packaged JAR / wheel that carries the job entry point + its Python / Scala dependencies, local unit tests against a &lt;code&gt;SparkSession.builder.master("local[2]")&lt;/code&gt; fixture that catches logic and schema bugs, and a dry-run &lt;code&gt;spark-submit&lt;/code&gt; against a small dev cluster (or a docker-compose Spark mini-cluster) that validates cluster reachability, JAR resolution, and config before executors are ever provisioned — because a Spark job that fails after 40 minutes of shuffle costs orders of magnitude more than a job that fails in 30 seconds of local pytest&lt;/strong&gt;. Every senior Spark deployment has migrated to this pattern once; skipping the local-unit + dry-run gates turns "one bad UDF" into "40-minute cluster time wasted plus a 3 AM page."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtcu8l5al1egvjiyufp0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtcu8l5al1egvjiyufp0.jpeg" alt="Iconographic Spark CI diagram — packaged JAR card, local unit tests card, dry-run submit arrow, and cluster gate with checkmark badge." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for Spark CI.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correctness.&lt;/strong&gt; Three levels: unit (does the transformation logic produce the right output for known input?), schema (does the job's output schema match downstream expectations?), integration (does &lt;code&gt;spark-submit&lt;/code&gt; actually accept the config in the target cluster?). Each layer catches a different class of bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Local &lt;code&gt;SparkSession&lt;/code&gt; is free (runs on the GHA runner). Docker-compose mini-cluster is ~$0.05 per run. Dry-run submit against a dev cluster is ~$0.02. The cost trap is running the &lt;em&gt;real&lt;/em&gt; job on the &lt;em&gt;real&lt;/em&gt; cluster from CI — that's how a $500 CI run happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency-to-merge.&lt;/strong&gt; Local pytest in 3-4 minutes. Dry-run in 30-60 seconds. A Spark-only PR should complete CI in under 6 minutes p95.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius.&lt;/strong&gt; Local Spark writes to a tempdir. Docker-compose Spark writes to a container-scoped volume. Dry-run submit does not write at all. The real cluster is only touched at deploy time, never at CI time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The packaged Spark job — one artifact, one entry point.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PySpark.&lt;/strong&gt; &lt;code&gt;pip wheel&lt;/code&gt; producing &lt;code&gt;job-1.2.3-py3-none-any.whl&lt;/code&gt; with &lt;code&gt;entry_points = {"console_scripts": ["run-job = mypkg.entry:main"]}&lt;/code&gt;. &lt;code&gt;spark-submit --py-files job-1.2.3.whl --archives venv.tar.gz#env script.py&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scala/Java.&lt;/strong&gt; &lt;code&gt;sbt assembly&lt;/code&gt; producing a fat JAR &lt;code&gt;job-1.2.3-assembly.jar&lt;/code&gt;. &lt;code&gt;spark-submit --class com.acme.Job job.jar&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config.&lt;/strong&gt; Job-specific YAML (&lt;code&gt;config/prod.yaml&lt;/code&gt;, &lt;code&gt;config/dev.yaml&lt;/code&gt;) selected by env var; never baked into the artifact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version.&lt;/strong&gt; Semver tag + git SHA baked into a &lt;code&gt;--version&lt;/code&gt; command that prints the SHA. Enables prod-side triage — "which commit is this?" is one flag away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The local &lt;code&gt;SparkSession&lt;/code&gt; fixture — the unit-test workhorse.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The fixture.&lt;/strong&gt; &lt;code&gt;pytest&lt;/code&gt; session-scoped fixture creating &lt;code&gt;SparkSession.builder.master("local[2]").appName("ci").getOrCreate()&lt;/code&gt;. Two cores is enough for logic tests; more is wasted CI time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What to test.&lt;/strong&gt; Pure transformations (&lt;code&gt;def dedupe(df: DataFrame) -&amp;gt; DataFrame:&lt;/code&gt;) against known-input DataFrames; UDFs against small Python lists; schema-enforcement (&lt;code&gt;assertSchemaEqual&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What not to test.&lt;/strong&gt; Anything requiring shuffle at scale — leave that to the integration test. Anything requiring cluster-specific config (Kryo serializer settings, dynamic allocation) — same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test data.&lt;/strong&gt; Small in-memory DataFrames (&lt;code&gt;spark.createDataFrame([(1, "a"), (2, "b")], ["id", "name"])&lt;/code&gt;) or small JSON fixtures. Never load prod data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The docker-compose mini-cluster — one CI gate for shuffle behaviour.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The setup.&lt;/strong&gt; &lt;code&gt;bitnami/spark:3.5&lt;/code&gt; for master + 2 workers in docker-compose. About 90 seconds to spin up on GHA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use.&lt;/strong&gt; Jobs that exercise shuffle, partitioning, or broadcast joins. Local mode does not test these accurately at CI scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to skip.&lt;/strong&gt; Pure UDF / column-arithmetic jobs — local mode is enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The dry-run &lt;code&gt;spark-submit&lt;/code&gt; — the cluster-config gate.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it does.&lt;/strong&gt; Submits the job with &lt;code&gt;--num-executors 0&lt;/code&gt; (or &lt;code&gt;--conf spark.dynamicAllocation.enabled=false --conf spark.executor.instances=0&lt;/code&gt;), so the driver starts, resolves the JAR, checks Kerberos / IAM, but no executors spin up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it catches.&lt;/strong&gt; Missing JAR dependencies, mistyped config keys, cluster-reachability, auth failures, &lt;code&gt;--files&lt;/code&gt; resolution errors. Everything that would fail in the first 30 seconds of a real submit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it does not catch.&lt;/strong&gt; Actual data processing bugs — leave those to the unit tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Databricks equivalent.&lt;/strong&gt; POST to &lt;code&gt;/api/2.1/jobs/runs/submit-and-wait&lt;/code&gt; with &lt;code&gt;--dry-run=true&lt;/code&gt; (or preview via the CLI); validates the job config against the workspace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on Spark CI.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you unit-test a PySpark job?" — required answer: pytest fixture with local SparkSession.&lt;/li&gt;
&lt;li&gt;"How do you validate cluster config in CI without spinning up a cluster?" — dry-run submit with &lt;code&gt;--num-executors 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"What's the packaging story for PySpark vs Scala?" — wheel vs fat JAR.&lt;/li&gt;
&lt;li&gt;"How do you test a UDF?" — pytest with the UDF's Python function directly, plus a Spark-side sanity test.&lt;/li&gt;
&lt;li&gt;"How do you avoid running the real job on the real cluster in CI?" — packaging + local mode + dry-run. The real cluster is never touched by CI.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — pytest against a local SparkSession
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical PySpark unit test: a session-scoped SparkSession fixture that constructs a &lt;code&gt;local[2]&lt;/code&gt; session once per pytest run, plus per-test DataFrames built inline. Walk through a full test module for a dedupe transformation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixture.&lt;/strong&gt; &lt;code&gt;spark_session&lt;/code&gt; fixture, session scope, &lt;code&gt;local[2]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transformation under test.&lt;/strong&gt; &lt;code&gt;dedupe_orders(df: DataFrame) -&amp;gt; DataFrame&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assertions.&lt;/strong&gt; Row count, schema, specific column values via &lt;code&gt;.collect()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the pytest module for a dedupe function with three test cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Input rows&lt;/th&gt;
&lt;th&gt;Expected rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;test_basic_dedupe&lt;/td&gt;
&lt;td&gt;3 rows (1 duplicate)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_all_duplicate&lt;/td&gt;
&lt;td&gt;5 identical rows&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_no_duplicates&lt;/td&gt;
&lt;td&gt;5 distinct rows&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spark/tests/conftest.py — the shared SparkSession fixture
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A local Spark session shared across the pytest session.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;master&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local[2]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ci&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.shuffle.partitions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.ui.enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.session.timeZone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;
    &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spark/tests/test_dedupe_orders.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Row&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mypkg.transforms&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dedupe_orders&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_basic_dedupe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;df_in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataFrame&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;df_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dedupe_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df_in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;df_out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df_out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_all_duplicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;df_in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataFrame&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;df_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dedupe_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df_in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;df_out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_no_duplicates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;df_in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataFrame&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;df_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dedupe_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df_in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;df_out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_schema_preserved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;df_in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataFrame&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;df_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dedupe_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df_in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;df_out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;df_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# mypkg/transforms.py — the transformation under test
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DataFrame&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dedupe_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the DataFrame with duplicate rows removed (all-column key).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropDuplicates&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;spark&lt;/code&gt; fixture is session-scoped, so the JVM starts up once for the whole test run — not once per test. This is critical: &lt;code&gt;SparkSession.builder.getOrCreate()&lt;/code&gt; is ~2-3 seconds of JVM startup, and per-test creation would inflate a 50-test suite by 2-3 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spark.sql.shuffle.partitions=2&lt;/code&gt; overrides the default 200 for the local session; with 2 cores, 200 partitions creates enormous scheduling overhead that dominates the test time. &lt;code&gt;spark.ui.enabled=false&lt;/code&gt; skips the Spark UI web server (unneeded in CI).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spark.createDataFrame(rows, schema)&lt;/code&gt; builds an in-memory DataFrame directly. No file IO, no serialization overhead — the fastest way to construct test data. &lt;code&gt;Row&lt;/code&gt; gives typed named-tuple-like construction.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.count()&lt;/code&gt; and &lt;code&gt;.collect()&lt;/code&gt; are the assertion primitives. For larger expected outputs, use &lt;code&gt;.toPandas()&lt;/code&gt; + pandas assertions; for schema, use &lt;code&gt;.schema&lt;/code&gt; equality or &lt;code&gt;assertSchemaEqual&lt;/code&gt; from &lt;code&gt;pyspark.testing&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;test_schema_preserved&lt;/code&gt; case is a common trap: transformations often silently drop columns or change types. Asserting schema equality catches this regression class.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Fixture cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First test (JVM startup)&lt;/td&gt;
&lt;td&gt;~3 s&lt;/td&gt;
&lt;td&gt;one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subsequent tests&lt;/td&gt;
&lt;td&gt;~50-200 ms each&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full 50-test suite&lt;/td&gt;
&lt;td&gt;~30-40 s total&lt;/td&gt;
&lt;td&gt;amortised&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For every PySpark project, ship a session-scoped SparkSession fixture with &lt;code&gt;local[2]&lt;/code&gt; + shuffle=2 + ui=false + UTC timezone. Then every unit test is one createDataFrame + one transformation call + one assertion. Under 200 ms per test after the JVM warms.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — docker-compose Spark mini-cluster
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; For jobs that exercise shuffle, broadcast joins, or specific &lt;code&gt;spark-defaults.conf&lt;/code&gt; behaviour, local mode is insufficient — you need a real cluster with executor JVMs. Docker-compose is the CI-friendly way to spin one up in about 90 seconds. Walk through the compose file and a pytest that submits a job against it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compose services.&lt;/strong&gt; &lt;code&gt;spark-master&lt;/code&gt; + 2 × &lt;code&gt;spark-worker&lt;/code&gt; + optional &lt;code&gt;spark-history&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base image.&lt;/strong&gt; &lt;code&gt;bitnami/spark:3.5.1&lt;/code&gt; (matches prod Spark version).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job submission.&lt;/strong&gt; &lt;code&gt;docker exec spark-master spark-submit --master spark://spark-master:7077 …&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the docker-compose file and a pytest that spins the cluster, submits a job, and asserts the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spark version&lt;/td&gt;
&lt;td&gt;3.5.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Master + workers&lt;/td&gt;
&lt;td&gt;1 + 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Master port&lt;/td&gt;
&lt;td&gt;7077&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job type&lt;/td&gt;
&lt;td&gt;JAR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assertion&lt;/td&gt;
&lt;td&gt;output S3 prefix has expected file count&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spark/tests/integration/docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;spark-master&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bitnami/spark:3.5.1&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SPARK_MODE=master&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SPARK_LOCAL_IP=spark-master&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7077:7077"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./workdir:/workdir&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;../../dist:/opt/jobs&lt;/span&gt;

  &lt;span class="na"&gt;spark-worker-1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bitnami/spark:3.5.1&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SPARK_MODE=worker&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SPARK_MASTER_URL=spark://spark-master:7077&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SPARK_WORKER_MEMORY=2g&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SPARK_WORKER_CORES=2&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;spark-master&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;../../dist:/opt/jobs'&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;spark-worker-2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bitnami/spark:3.5.1&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SPARK_MODE=worker&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SPARK_MASTER_URL=spark://spark-master:7077&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SPARK_WORKER_MEMORY=2g&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SPARK_WORKER_CORES=2&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;spark-master&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;../../dist:/opt/jobs'&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spark/tests/integration/test_dedupe_cluster.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;


&lt;span class="n"&gt;COMPOSE_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;module&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COMPOSE_FILE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="c1"&gt;# Wait for master to be ready
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COMPOSE_FILE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-T&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark-master&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-fsS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CalledProcessError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Spark master did not come up in 60 s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COMPOSE_FILE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_dedupe_job_end_to_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Submit the dedupe job against the mini-cluster and assert output.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;input_path&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;output_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;input_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:1,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:100}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:2,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:200}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:1,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:100}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COMPOSE_FILE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-T&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark-master&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark-submit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--master&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark://spark-master:7077&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;com.acme.DedupeJob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/opt/jobs/dedupe-job-1.0.0.jar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workdir/in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workdir/out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# Assert output exists and has 2 rows
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;files&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no output produced&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected 2 output rows, got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The compose file defines master + 2 workers using &lt;code&gt;bitnami/spark:3.5.1&lt;/code&gt;. Ports 7077 (master) and 8080 (UI) are published so the pytest can healthcheck via HTTP. Volume mounts share the JAR and the working directory between host and containers.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;cluster&lt;/code&gt; fixture is module-scoped so the cluster spins up once per test module. &lt;code&gt;docker compose up -d&lt;/code&gt; runs detached; the polling loop hits the master UI on 8080 until it responds — typically 60-90 seconds.&lt;/li&gt;
&lt;li&gt;Job submission runs &lt;code&gt;spark-submit&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; the master container via &lt;code&gt;docker compose exec&lt;/code&gt;. This uses the master's classpath and network — no host-side Spark install needed on the GHA runner.&lt;/li&gt;
&lt;li&gt;The mounted &lt;code&gt;/workdir&lt;/code&gt; shares the input JSON and receives the output — no S3 dependency; everything stays local to the compose stack. This keeps the integration test hermetic and fast.&lt;/li&gt;
&lt;li&gt;The test asserts on output-file existence and row count. For richer assertions, load the output with pandas or run a validation Spark job. The teardown (&lt;code&gt;down -v&lt;/code&gt;) removes volumes so nothing persists.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;docker compose up&lt;/td&gt;
&lt;td&gt;45-60 s (first run); 20-30 s (cached image)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Master healthcheck&lt;/td&gt;
&lt;td&gt;15-30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job submit + run&lt;/td&gt;
&lt;td&gt;30-60 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker compose down&lt;/td&gt;
&lt;td&gt;5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total (first run)&lt;/td&gt;
&lt;td&gt;~2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Reserve the docker-compose Spark cluster for jobs that need real shuffle / broadcast / partitioning behaviour. For pure-transformation jobs, local mode is enough and finishes in 3-5 minutes total. The compose cluster adds 1-2 minutes for the environment; only pay that cost when the test needs it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — dry-run submit that validates cluster config
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The dry-run submit is the cheapest cluster-level gate: submit the job to a real dev cluster with &lt;code&gt;--num-executors 0&lt;/code&gt;, so the driver starts, JAR resolves, config validates, but no executors spin up. If the submit is accepted, the cluster will accept the real submit; if it fails, you catch the config bug in 30 seconds instead of after a 40-minute allocation wait.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The invocation.&lt;/strong&gt; &lt;code&gt;spark-submit --master yarn --deploy-mode client --num-executors 0 --class com.acme.Job job.jar --dry-run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What passes.&lt;/strong&gt; JAR resolution, &lt;code&gt;--files&lt;/code&gt; resolution, Kerberos ticket, HDFS access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What fails.&lt;/strong&gt; Missing JAR, mistyped &lt;code&gt;--conf&lt;/code&gt;, wrong &lt;code&gt;--class&lt;/code&gt;, invalid YARN queue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the GHA step and a wrapper script that runs the dry-run submit against a dev EMR cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Target cluster&lt;/td&gt;
&lt;td&gt;dev EMR (via IAM role)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executors&lt;/td&gt;
&lt;td&gt;0 (dry run)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exit code contract&lt;/td&gt;
&lt;td&gt;0 = accepted; non-zero = config bug&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# scripts/spark_dry_run.sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;JAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;                &lt;span class="c"&gt;# path to fat JAR&lt;/span&gt;
&lt;span class="nv"&gt;CLASS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;              &lt;span class="c"&gt;# main class&lt;/span&gt;
&lt;span class="nv"&gt;CLUSTER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEV_EMR_CLUSTER_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Submit via EMR CLI with --dry-run-esque args&lt;/span&gt;
&lt;span class="nv"&gt;step_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
[
  {
    "Type": "Spark",
    "Name": "ci-dry-run-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--short&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;",
    "ActionOnFailure": "CONTINUE",
    "Args": [
      "spark-submit",
      "--deploy-mode", "cluster",
      "--num-executors", "0",
      "--conf", "spark.dynamicAllocation.enabled=false",
      "--conf", "spark.executor.instances=0",
      "--conf", "spark.driver.memory=512m",
      "--conf", "spark.app.name=ci-dry-run",
      "--class", "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CLASS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;",
      "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JAR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;",
      "--dry-run"
    ]
  }
]
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;step_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws emr add-steps &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CLUSTER_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--steps&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;step_config&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'StepIds[0]'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Submitted step &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;step_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; polling…"&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;_ &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 20&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws emr describe-step &lt;span class="nt"&gt;--cluster-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CLUSTER_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--step-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;step_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
          &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Step.Status.State'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"state=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;COMPLETED&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt;
    FAILED|CANCELLED&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="nb"&gt;sleep &lt;/span&gt;3 &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"timeout waiting for dry-run to complete"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GHA step&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Spark dry-run against dev cluster&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;AWS_REGION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
    &lt;span class="na"&gt;DEV_EMR_CLUSTER_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEV_EMR_CLUSTER_ID }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;bash scripts/spark_dry_run.sh \&lt;/span&gt;
      &lt;span class="s"&gt;s3://acme-artifacts/spark/dedupe-job-${{ github.sha }}.jar \&lt;/span&gt;
      &lt;span class="s"&gt;com.acme.DedupeJob&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="c1"&gt;// spark/src/main/scala/com/acme/DedupeJob.scala — dry-run branch&lt;/span&gt;
&lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;DedupeJob&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Unit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;cliArgs&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseArgs&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cliArgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;dryRun&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DRY RUN — validating config only"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;spark&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;SparkSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;appName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dedupe-dry-run"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;getOrCreate&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
      &lt;span class="c1"&gt;// Touch config to force resolution&lt;/span&gt;
      &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"master=${spark.conf.get("&lt;/span&gt;&lt;span class="nv"&gt;spark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;master&lt;/span&gt;&lt;span class="s"&gt;")}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"appName=${spark.conf.get("&lt;/span&gt;&lt;span class="nv"&gt;spark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="s"&gt;")}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;spark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
      &lt;span class="nv"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// real job body …&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;--num-executors 0&lt;/code&gt; + &lt;code&gt;spark.dynamicAllocation.enabled=false&lt;/code&gt; tells the cluster manager "start the driver, but don't allocate any executors." The driver comes up, resolves the JAR, checks config, then exits immediately — 30-60 seconds end to end.&lt;/li&gt;
&lt;li&gt;Submitting via &lt;code&gt;aws emr add-steps&lt;/code&gt; uses the EMR steps API rather than SSHing to the master node. The step ID is returned; the polling loop calls &lt;code&gt;describe-step&lt;/code&gt; until the state is terminal.&lt;/li&gt;
&lt;li&gt;The job's &lt;code&gt;--dry-run&lt;/code&gt; flag is recognised in the main class; if set, the job constructs a SparkSession (which forces config resolution) and exits &lt;code&gt;0&lt;/code&gt;. This catches bad &lt;code&gt;--conf&lt;/code&gt; values that would otherwise only surface once the real job started reading data.&lt;/li&gt;
&lt;li&gt;The exit-code contract: 0 = accepted, non-zero = config bug. The GHA step fails fast on non-zero, blocking the PR merge until the config is fixed.&lt;/li&gt;
&lt;li&gt;The dev cluster is deliberately small (single m5.xlarge master) and shared across CI jobs — it's OK because dry-runs are 30 seconds each. Concurrent CI runs queue up, but no one waits long.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;Caught by dry-run?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing JAR at S3&lt;/td&gt;
&lt;td&gt;yes (JAR resolution fails)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistyped &lt;code&gt;--conf&lt;/code&gt; key&lt;/td&gt;
&lt;td&gt;partially (some keys validated at driver start)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong &lt;code&gt;--class&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes (ClassNotFoundException at driver init)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid YARN queue&lt;/td&gt;
&lt;td&gt;yes (rejected before executors requested)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data-processing bug&lt;/td&gt;
&lt;td&gt;no (need unit tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shuffle-size bug&lt;/td&gt;
&lt;td&gt;no (need integration test)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For every Spark job, ship a &lt;code&gt;--dry-run&lt;/code&gt; code path in the main class that constructs a SparkSession and exits &lt;code&gt;0&lt;/code&gt;. Wire the dry-run into CI as the last gate before merge. This is the cheapest possible "will this run in prod?" check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on Spark CI
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You inherit a Spark job pipeline where CI submits every PR to the prod cluster to test it, burning ~$400/day in wasted compute. Design the CI redesign: packaging, local unit tests, docker-compose integration for high-value jobs, and a dry-run gate. Show me the CI budget, the coverage targets, and the on-call runbook when the dry-run fails on a working PR."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using packaged JAR + local pytest + docker-compose mini-cluster + dry-run submit
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/spark-full.yml — production-grade Spark CI&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spark-ci&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# 1. Package the job — one artifact, reused downstream&lt;/span&gt;
  &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;jar_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.upload.outputs.key }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;temurin&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sbt assembly&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spark&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sbt assembly&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload JAR&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upload&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spark/dedupe-job-${{ github.sha }}.jar&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;aws s3 cp spark/target/scala-2.13/dedupe-job-assembly-*.jar \&lt;/span&gt;
                    &lt;span class="s"&gt;s3://acme-artifacts/${KEY}&lt;/span&gt;
          &lt;span class="s"&gt;echo "key=${KEY}" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;

  &lt;span class="c1"&gt;# 2. Local unit tests — fast, cheap&lt;/span&gt;
  &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;temurin&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install PySpark for tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r spark/requirements-ci.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PySpark unit tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest spark/tests/unit --cov=mypkg --cov-fail-under=90&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Scala unit tests&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spark&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sbt test&lt;/span&gt;

  &lt;span class="c1"&gt;# 3. Integration — only for shuffle-heavy jobs&lt;/span&gt;
  &lt;span class="na"&gt;integration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;contains(join(github.event.pull_request.changed_files, ','), 'spark/jobs/')&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;package&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker-compose spark cluster + integration test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cd spark/tests/integration&lt;/span&gt;
          &lt;span class="s"&gt;docker compose up -d&lt;/span&gt;
          &lt;span class="s"&gt;for i in $(seq 1 30); do&lt;/span&gt;
            &lt;span class="s"&gt;docker compose exec -T spark-master curl -fsS http://localhost:8080 &amp;amp;&amp;amp; break&lt;/span&gt;
            &lt;span class="s"&gt;sleep 2&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;
          &lt;span class="s"&gt;pip install pytest pyspark&lt;/span&gt;
          &lt;span class="s"&gt;pytest .&lt;/span&gt;
          &lt;span class="s"&gt;docker compose down -v&lt;/span&gt;

  &lt;span class="c1"&gt;# 4. Dry-run submit — cluster gate&lt;/span&gt;
  &lt;span class="na"&gt;dry-run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.unit.result == 'success' &amp;amp;&amp;amp; needs.package.result == 'success'&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;unit&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::123456789012:role/gha-spark-dry-run&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DEV_EMR_CLUSTER_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEV_EMR_CLUSTER_ID }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;bash scripts/spark_dry_run.sh \&lt;/span&gt;
            &lt;span class="s"&gt;s3://acme-artifacts/${{ needs.package.outputs.jar_key }} \&lt;/span&gt;
            &lt;span class="s"&gt;com.acme.DedupeJob&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Spark CI budget contract
Gate           p95 latency   Cost budget   Coverage
─────────────────────────────────────────────────────
package         60 s         $0.02         100% jobs
unit            4 min        $0.03         90% line coverage
integration     6 min        $0.05         high-value jobs only
dry-run         60 s         $0.02         100% jobs before merge
Total (typical) 6 min        $0.12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;package&lt;/td&gt;
&lt;td&gt;sbt assembly + upload to s3&lt;/td&gt;
&lt;td&gt;one immutable JAR per commit SHA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unit&lt;/td&gt;
&lt;td&gt;pytest + sbt test, 90% coverage floor&lt;/td&gt;
&lt;td&gt;catches logic + schema bugs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;integration&lt;/td&gt;
&lt;td&gt;docker-compose spark-3.5.1 mini-cluster&lt;/td&gt;
&lt;td&gt;catches shuffle + join bugs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dry-run&lt;/td&gt;
&lt;td&gt;EMR add-steps with --num-executors 0&lt;/td&gt;
&lt;td&gt;catches cluster-config bugs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prod cluster touched?&lt;/td&gt;
&lt;td&gt;never in CI&lt;/td&gt;
&lt;td&gt;cost isolated to deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, CI submits zero real jobs to the prod cluster — the daily $400 wasted-compute bill goes to zero. The p95 CI time for a Spark-only PR is about 6 minutes; the p95 cost is about $0.12. Unit test coverage on the transformation package is enforced at 90% via &lt;code&gt;--cov-fail-under=90&lt;/code&gt;. Every merged commit has a JAR at &lt;code&gt;s3://acme-artifacts/spark/dedupe-job-&amp;lt;sha&amp;gt;.jar&lt;/code&gt;, so prod rollouts and rollbacks are one S3 pointer change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wasted-compute bill (CI)&lt;/td&gt;
&lt;td&gt;~$400/day&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 CI time (Spark-only PR)&lt;/td&gt;
&lt;td&gt;42 min&lt;/td&gt;
&lt;td&gt;6 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 CI cost per PR&lt;/td&gt;
&lt;td&gt;$8&lt;/td&gt;
&lt;td&gt;$0.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit-test coverage&lt;/td&gt;
&lt;td&gt;~30%&lt;/td&gt;
&lt;td&gt;90% (enforced)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job-versioning story&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;immutable JAR per SHA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broken-config incidents&lt;/td&gt;
&lt;td&gt;~2/week&lt;/td&gt;
&lt;td&gt;~0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Packaged JAR / wheel per commit&lt;/strong&gt;&lt;/strong&gt; — one immutable artifact per SHA, keyed by git SHA. Every downstream test uses the same artifact; prod deploys reference it by SHA. This kills the "which version is in prod?" question at its root.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Local SparkSession fixture&lt;/strong&gt;&lt;/strong&gt; — fastest possible unit-test loop. &lt;code&gt;local[2]&lt;/code&gt; + shuffle=2 + ui=false keeps the fixture 3 seconds to warm and 100-200 ms per test after. The 90% coverage floor is a &lt;em&gt;contract&lt;/em&gt;, not a hope; &lt;code&gt;--cov-fail-under=90&lt;/code&gt; enforces it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Docker-compose mini-cluster&lt;/strong&gt;&lt;/strong&gt; — scoped to jobs that actually need real shuffle. Adds ~2 minutes to CI when it runs, but stays gated on &lt;code&gt;spark/jobs/**&lt;/code&gt; path filter so most PRs skip it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Dry-run submit&lt;/strong&gt;&lt;/strong&gt; — the cheapest cluster-level gate. &lt;code&gt;--num-executors 0&lt;/code&gt; starts the driver, resolves everything, exits — 30-60 seconds. Catches config, JAR, class, queue bugs before the real submit would.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Never touch the prod cluster from CI&lt;/strong&gt;&lt;/strong&gt; — the invariant. All gates run on the GHA runner, a local docker-compose stack, or a small dev EMR cluster. Prod compute is only billed for prod work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — $0.02 package + $0.03 unit + optional $0.05 integration + $0.02 dry-run = $0.12 per PR typical. Compared to the $8/PR baseline (mostly wasted prod-cluster time), that's a 60× cost reduction. Net O(local) per PR versus O(cluster) per PR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Python&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — python&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Python data-transformation testing problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/python" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Streaming&lt;/span&gt;
&lt;span&gt;Topic — streaming&lt;/span&gt;
&lt;strong&gt;Streaming and Spark job problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/streaming" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Preview environments — one branch, one warehouse schema
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; schema on Snowflake / Databricks UC — created on PR open, dropped on PR close
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;dbt preview environment&lt;/code&gt; is the pattern where every open pull request maps to its own throwaway warehouse schema named &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; — created on PR open via &lt;code&gt;CREATE SCHEMA ... CLONE prod&lt;/code&gt; (Snowflake / Databricks UC zero-copy), targeted by the slim-CI dbt build, exposed to analysts as a preview URL, and auto-dropped by a cleanup workflow when the PR closes — so every PR has an isolated, near-prod dataset for review without ever touching the prod schema and without paying for full-copy storage&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpkim5lqfcqsqosrpjw8e.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpkim5lqfcqsqosrpjw8e.jpeg" alt="Iconographic preview environments diagram — one PR = one schema, a branch glyph pointing to an ephemeral warehouse schema chip, cleanup arrow on PR close." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for preview environments.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correctness.&lt;/strong&gt; The preview schema is a near-prod dataset — clones of every unmodified table, freshly-built copies of changed models. Analysts querying the preview see exactly what production will look like after merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Snowflake and Databricks UC zero-copy clones are metadata operations — no data is physically duplicated, no storage is billed. XS warehouse with AUTO_SUSPEND=60 keeps compute cost under $0.10 per preview session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency-to-preview.&lt;/strong&gt; Slim build (~90 s) + clone (~5 s) + link posted to PR (~2 s) = analyst has the preview URL within 100 seconds of PR open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius.&lt;/strong&gt; Each preview lives in its own schema; concurrent PRs never collide. Cleanup on PR close reclaims the catalog entry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Schema naming — deterministic and cleanable.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Format.&lt;/strong&gt; &lt;code&gt;pr_&amp;lt;PR_NUMBER&amp;gt;_&amp;lt;BRANCH_SLUG&amp;gt;&lt;/code&gt; — e.g. &lt;code&gt;pr_4712_add_orders_v2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branch slug rules.&lt;/strong&gt; Lowercase, strip non-alphanumeric, cap at 40 characters. Snowflake identifier limit is 255; capping at 40 leaves room for the &lt;code&gt;pr_&amp;lt;num&amp;gt;_&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case.&lt;/strong&gt; Lowercase everywhere — Snowflake folds unquoted identifiers to uppercase; dbt writes lowercase by default. Consistency across CI + prod avoids "table not found" surprises.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup.&lt;/strong&gt; A cleanup workflow triggers on &lt;code&gt;pull_request&lt;/code&gt; &lt;code&gt;closed&lt;/code&gt; (both merge and reject) and runs &lt;code&gt;DROP SCHEMA IF EXISTS pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt; CASCADE&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Snowflake zero-copy CLONE.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Command.&lt;/strong&gt; &lt;code&gt;CREATE OR REPLACE SCHEMA analytics.pr_4712_add_orders_v2 CLONE analytics.prod;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantics.&lt;/strong&gt; All tables, views, and pipes in the source schema are cloned as metadata pointers. Reads work immediately; writes copy-on-write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; $0 storage until a write happens. A read-only preview costs storage $0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Databricks Unity Catalog SCHEMA CLONE.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Command.&lt;/strong&gt; &lt;code&gt;CREATE SCHEMA &amp;lt;catalog&amp;gt;.pr_4712_add_orders_v2 DEEP CLONE &amp;lt;catalog&amp;gt;.prod;&lt;/code&gt; for deep, or &lt;code&gt;SHALLOW CLONE&lt;/code&gt; for zero-copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantics.&lt;/strong&gt; Shallow clone is metadata-only (like Snowflake); deep clone physically copies. Prefer shallow for previews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Shallow clone = $0. Deep clone = full storage cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ephemeral compute — AUTO_SUSPEND=60 is the invariant.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake.&lt;/strong&gt; &lt;code&gt;WAREHOUSE_SIZE=XSMALL, AUTO_SUSPEND=60, AUTO_RESUME=TRUE&lt;/code&gt;. XS warehouse is $1/hour; suspending after 60 s of idle keeps a typical CI session under 5 minutes of billed compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databricks.&lt;/strong&gt; Interactive clusters with &lt;code&gt;autotermination_minutes: 10&lt;/code&gt;; SQL warehouses with &lt;code&gt;auto_stop_mins: 5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery.&lt;/strong&gt; On-demand billing means no explicit suspend — every query is priced independently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cleanup on PR close — the non-negotiable half of the pattern.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The trigger.&lt;/strong&gt; &lt;code&gt;on: pull_request: types: [closed]&lt;/code&gt; in GitHub Actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The action.&lt;/strong&gt; &lt;code&gt;DROP SCHEMA IF EXISTS pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt; CASCADE&lt;/code&gt; — idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The safety net.&lt;/strong&gt; Nightly cron that drops any &lt;code&gt;pr_%&lt;/code&gt; schema older than 14 days — catches PRs abandoned without close.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on preview environments.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you avoid full-copy storage cost for previews?" — zero-copy clone (Snowflake CREATE SCHEMA CLONE / Databricks SHALLOW CLONE).&lt;/li&gt;
&lt;li&gt;"How do you name preview schemas?" — &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch_slug&amp;gt;&lt;/code&gt;, deterministic, cleanable.&lt;/li&gt;
&lt;li&gt;"What if a PR is abandoned?" — cleanup workflow on &lt;code&gt;closed&lt;/code&gt; + nightly cron for &lt;code&gt;pr_%&lt;/code&gt; older than 14 days.&lt;/li&gt;
&lt;li&gt;"How do analysts access the preview?" — preview URL posted to PR comment; Snowsight / Databricks SQL link with the schema pre-selected.&lt;/li&gt;
&lt;li&gt;"What's the cost per preview?" — $0 storage (zero-copy), $0.05-$0.30 compute for a typical review session.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — create-preview-schema GitHub Action
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The create-preview workflow runs on PR open (and on every push to the PR) and produces the &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; schema. It's idempotent — CREATE OR REPLACE — so a re-push refreshes the preview against the latest changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger.&lt;/strong&gt; &lt;code&gt;pull_request&lt;/code&gt; types: [opened, synchronize, reopened].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action.&lt;/strong&gt; Snowflake &lt;code&gt;CREATE OR REPLACE SCHEMA ... CLONE prod&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grant.&lt;/strong&gt; Read-only access to the schema for the analyst role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the workflow and the SQL that creates a preview schema for a PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;pull_request opened / synchronize / reopened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse&lt;/td&gt;
&lt;td&gt;DBT_CI_WH_XS (AUTO_SUSPEND=60)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source schema&lt;/td&gt;
&lt;td&gt;ANALYTICS.PROD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target schema&lt;/td&gt;
&lt;td&gt;ANALYTICS.pr__&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyst grant&lt;/td&gt;
&lt;td&gt;SELECT to ANALYST_ROLE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/preview-schema-create.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;preview-schema-create&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;create-preview&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PR_NUMBER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;${{ github.event.pull_request.number }}&lt;/span&gt;
      &lt;span class="na"&gt;BRANCH_SLUG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;${{ github.head_ref }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install snowflake-connector-python&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compute schema name&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;schema&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;slug=$(echo "$BRANCH_SLUG" | tr '/' '_' | tr -cd '[:alnum:]_' | tr '[:upper:]' '[:lower:]' | cut -c1-40)&lt;/span&gt;
          &lt;span class="s"&gt;echo "name=pr_${PR_NUMBER}_${slug}" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create preview schema&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_ACCOUNT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;${{ secrets.SNOWFLAKE_ACCOUNT }}&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;dbt_ci&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_PRIVATE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SNOWFLAKE_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;SCHEMA_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;           &lt;span class="s"&gt;${{ steps.schema.outputs.name }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/create_preview_schema.py&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post preview URL to PR&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SCHEMA_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.schema.outputs.name }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const url = `https://acme.snowflakecomputing.com/console#/data/databases/ANALYTICS/schemas/${process.env.SCHEMA_NAME.toUpperCase()}`;&lt;/span&gt;
            &lt;span class="s"&gt;const body = `Preview schema created: \`ANALYTICS.${process.env.SCHEMA_NAME}\`\n\n[Open in Snowsight](${url})`;&lt;/span&gt;
            &lt;span class="s"&gt;await github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo:  context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;body:  body,&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/create_preview_schema.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCHEMA_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snowflake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_ACCOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;private_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_load_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_PRIVATE_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBT_CI_ROLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBT_CI_WH_XS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANALYTICS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE OR REPLACE SCHEMA ANALYTICS.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; CLONE ANALYTICS.PROD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GRANT USAGE  ON SCHEMA ANALYTICS.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; TO ROLE ANALYST_ROLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GRANT SELECT ON ALL TABLES IN SCHEMA ANALYTICS.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; TO ROLE ANALYST_ROLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GRANT SELECT ON ALL VIEWS  IN SCHEMA ANALYTICS.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; TO ROLE ANALYST_ROLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK — created &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives.serialization&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_pem_private_key&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;load_pem_private_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;private_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cryptography&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;hazmat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primitives&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cryptography&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;hazmat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primitives&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PrivateFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PKCS8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;encryption_algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cryptography&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;hazmat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primitives&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NoEncryption&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The workflow triggers on PR open + every synchronize (push). &lt;code&gt;CREATE OR REPLACE&lt;/code&gt; is idempotent — re-running refreshes the clone against the current prod state.&lt;/li&gt;
&lt;li&gt;Schema name construction sanitises the branch (lowercase, alphanumeric + underscore only, capped at 40 chars). Snowflake identifiers are case-folded to uppercase unless quoted; sticking to lowercase in code + uppercase at the SQL layer keeps everything predictable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CREATE OR REPLACE SCHEMA ... CLONE ANALYTICS.PROD&lt;/code&gt; is the zero-copy clone. All tables, views, sequences, pipes in &lt;code&gt;PROD&lt;/code&gt; become pointers in the new schema. No data copied; no storage billed until write.&lt;/li&gt;
&lt;li&gt;The GRANT statements give the analyst role read access to the preview schema — Snowsight browses will work, dashboards can be repointed, and stakeholders can &lt;code&gt;SELECT&lt;/code&gt;. No write access, so an analyst cannot accidentally mutate the preview.&lt;/li&gt;
&lt;li&gt;The PR comment includes a Snowsight deep-link with the schema pre-selected, so reviewers click through to the preview in one hop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake connect&lt;/td&gt;
&lt;td&gt;~2 s&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CREATE OR REPLACE SCHEMA ... CLONE&lt;/td&gt;
&lt;td&gt;~3 s&lt;/td&gt;
&lt;td&gt;$0 (metadata op)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GRANT statements&lt;/td&gt;
&lt;td&gt;~1 s&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post PR comment&lt;/td&gt;
&lt;td&gt;~1 s&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;~10 s&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Create the preview schema via zero-copy clone on PR open; make it idempotent (&lt;code&gt;CREATE OR REPLACE&lt;/code&gt;); post the schema name and a Snowsight link to the PR. Analysts get the preview URL as fast as CI can post a comment.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — destroy-preview-schema on PR close
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The cleanup workflow is the non-negotiable other half of the pattern. Without it, preview schemas accumulate indefinitely and eventually clutter the catalog. With it, catalog footprint is bounded to the count of open PRs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger.&lt;/strong&gt; &lt;code&gt;pull_request&lt;/code&gt; type: closed (fires on both merge and reject).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action.&lt;/strong&gt; &lt;code&gt;DROP SCHEMA IF EXISTS ... CASCADE&lt;/code&gt; — idempotent; safe even if the schema was already dropped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the cleanup workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;pull_request closed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action&lt;/td&gt;
&lt;td&gt;DROP SCHEMA IF EXISTS pr__ CASCADE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleanup guarantee&lt;/td&gt;
&lt;td&gt;fires whether PR merged or rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/preview-schema-drop.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;preview-schema-drop&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;closed&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;drop-preview&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PR_NUMBER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;${{ github.event.pull_request.number }}&lt;/span&gt;
      &lt;span class="na"&gt;BRANCH_SLUG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.head_ref }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install snowflake-connector-python&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compute schema name&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;schema&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;slug=$(echo "$BRANCH_SLUG" | tr '/' '_' | tr -cd '[:alnum:]_' | tr '[:upper:]' '[:lower:]' | cut -c1-40)&lt;/span&gt;
          &lt;span class="s"&gt;echo "name=pr_${PR_NUMBER}_${slug}" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Drop preview schema&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_ACCOUNT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;${{ secrets.SNOWFLAKE_ACCOUNT }}&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;dbt_ci&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_PRIVATE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SNOWFLAKE_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;SCHEMA_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;           &lt;span class="s"&gt;${{ steps.schema.outputs.name }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/drop_preview_schema.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/drop_preview_schema.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCHEMA_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snowflake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_ACCOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;private_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_load_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_PRIVATE_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBT_CI_ROLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBT_CI_WH_XS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANALYTICS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DROP SCHEMA IF EXISTS ANALYTICS.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; CASCADE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK — dropped &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (or it did not exist)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives.serialization&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;load_pem_private_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PrivateFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NoEncryption&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;load_pem_private_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;private_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PrivateFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PKCS8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;encryption_algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;NoEncryption&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Nightly safety-net cron — drop pr_% schemas older than 14 days&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
  &lt;span class="n"&gt;schemas_to_drop&lt;/span&gt; &lt;span class="n"&gt;VARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ARRAY_AGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;schemas_to_drop&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;ANALYTICS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFORMATION_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SCHEMATA&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="k"&gt;SCHEMA_NAME&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'PR_%'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;CREATED&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;DATEADD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ARRAY_SIZE&lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;schemas_to_drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt;
    &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;IMMEDIATE&lt;/span&gt; &lt;span class="s1"&gt;'DROP SCHEMA IF EXISTS ANALYTICS.'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;schemas_to_drop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' CASCADE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;pull_request: closed&lt;/code&gt; trigger fires whether the PR is merged or rejected. GitHub does not distinguish between merged and abandoned closes in the trigger itself — both should trigger cleanup because both leave the preview schema orphaned.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DROP SCHEMA IF EXISTS ... CASCADE&lt;/code&gt; is idempotent: &lt;code&gt;IF EXISTS&lt;/code&gt; prevents an error if the schema was already dropped (concurrent runs, manual cleanup, etc.). &lt;code&gt;CASCADE&lt;/code&gt; drops all objects in the schema; for zero-copy clones there's no storage impact.&lt;/li&gt;
&lt;li&gt;The Snowflake schema name is uppercased at the SQL layer because Snowflake case-folds unquoted identifiers. Consistency: lowercase in Python + workflow logic, uppercase at SQL.&lt;/li&gt;
&lt;li&gt;The nightly safety-net cron catches schemas that escaped the pull_request trigger — e.g. PRs closed while GitHub Actions was down, PRs deleted rather than closed, PRs from forks that bypass the trigger. Bounded staleness = 14 days.&lt;/li&gt;
&lt;li&gt;The DROP is essentially free — the source schema (PROD) is untouched; only the metadata pointer schema is removed. Runtime is under 5 seconds even for schemas with thousands of tables.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Cleanup path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PR merged&lt;/td&gt;
&lt;td&gt;pull_request-closed trigger → DROP within 30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR rejected&lt;/td&gt;
&lt;td&gt;pull_request-closed trigger → DROP within 30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR abandoned (never closed)&lt;/td&gt;
&lt;td&gt;nightly cron catches at day 14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fork PR (limited trigger)&lt;/td&gt;
&lt;td&gt;nightly cron catches at day 14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions outage&lt;/td&gt;
&lt;td&gt;nightly cron catches at day 14&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Ship the cleanup workflow the same day as the create workflow. Add the nightly cron the same week. A preview-environment system without cleanup accumulates schemas until someone screams; a system with both cleanup layers stays bounded forever.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — seeding a preview from a sampled production dataset
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; For sensitive workloads (PII, revenue data) where the preview should not contain full prod data, seed the preview from a sampled subset instead of a zero-copy clone. This trades preview fidelity for privacy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The sampling.&lt;/strong&gt; &lt;code&gt;CREATE TABLE ... AS SELECT * FROM prod.orders SAMPLE (5)&lt;/code&gt; — Snowflake's 5% random sample.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The PII handling.&lt;/strong&gt; Redact or hash sensitive columns during the sample.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the sampling script that seeds a preview schema with 5% of prod data, redacting emails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;ANALYTICS.PROD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample rate&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redacted columns&lt;/td&gt;
&lt;td&gt;orders.customer_email → sha256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;ANALYTICS.pr__&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/seed_sampled_preview.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt;

&lt;span class="n"&gt;TABLES_TO_SAMPLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ORDERS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# columns to redact
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CUSTOMERS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SHIPMENTS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCHEMA_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snowflake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_ACCOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;private_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_load_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_PRIVATE_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBT_CI_ROLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBT_CI_WH_XS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANALYTICS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE SCHEMA IF NOT EXISTS ANALYTICS.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redact_cols&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TABLES_TO_SAMPLE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;col_expr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_column_expressions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redact_cols&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE OR REPLACE TABLE ANALYTICS.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; AS &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_expr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; FROM ANALYTICS.PROD.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; SAMPLE (5)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK — seeded &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_column_expressions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redact_cols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT COLUMN_NAME FROM ANALYTICS.INFORMATION_SCHEMA.COLUMNS
        WHERE  TABLE_SCHEMA = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PROD&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND TABLE_NAME = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
        ORDER  BY ORDINAL_POSITION
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;redact_cols&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
            &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SHA2(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, 256) AS &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives.serialization&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;load_pem_private_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PrivateFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NoEncryption&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;load_pem_private_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;private_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PrivateFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PKCS8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;encryption_algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;NoEncryption&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The sampling script iterates a table → redact-cols dict, so adding a new table is a one-line change. Column names are looked up dynamically from &lt;code&gt;INFORMATION_SCHEMA.COLUMNS&lt;/code&gt; to avoid schema drift.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SAMPLE (5)&lt;/code&gt; is Snowflake's Bernoulli row sample: each row has a 5% independent chance of inclusion. For a 100M-row table this yields ~5M rows; runtime is roughly proportional to the sample size, not the source size.&lt;/li&gt;
&lt;li&gt;Redacted columns are SHA-256 hashed rather than dropped, so downstream JOIN semantics survive. &lt;code&gt;SHA2(email, 256)&lt;/code&gt; keeps the column at its original width and preserves uniqueness for de-dupe logic.&lt;/li&gt;
&lt;li&gt;CREATE OR REPLACE TABLE (not zero-copy clone) is used because the redaction requires actual data materialisation. Storage cost applies — a 5% sample of a 1 TB table costs ~50 GB. Retention is the same 14-day nightly cron.&lt;/li&gt;
&lt;li&gt;This pattern is the exception, not the rule — most previews use zero-copy clones. Reserve the sampled-preview path for PII-sensitive schemas, and document which tables are redacted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Rows in prod&lt;/th&gt;
&lt;th&gt;Rows in preview&lt;/th&gt;
&lt;th&gt;Redacted cols&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ORDERS&lt;/td&gt;
&lt;td&gt;100M&lt;/td&gt;
&lt;td&gt;~5M&lt;/td&gt;
&lt;td&gt;customer_email → sha256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUSTOMERS&lt;/td&gt;
&lt;td&gt;10M&lt;/td&gt;
&lt;td&gt;~500K&lt;/td&gt;
&lt;td&gt;email, phone → sha256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHIPMENTS&lt;/td&gt;
&lt;td&gt;50M&lt;/td&gt;
&lt;td&gt;~2.5M&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Default to zero-copy clone; use sampled + redacted only for PII-sensitive schemas. Document the redaction list in a versioned config. Analysts querying the preview must know which columns are hashes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on preview environments
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your dbt project has 800 models and 30 open PRs at any time. Design the preview-environment system: schema naming, creation, cleanup, per-PR cost bound, and the plan for PII-sensitive schemas. Include the runbook when the cleanup workflow fails."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using create + drop workflows + nightly safety-net + optional sampled seed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/preview-envs.yml — full preview lifecycle&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;preview-envs&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;closed&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;   &lt;span class="c1"&gt;# nightly safety-net at 06:00 UTC&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event.action != 'closed' &amp;amp;&amp;amp; github.event_name == 'pull_request'&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/preview-schema-create.yml&lt;/span&gt;

  &lt;span class="na"&gt;drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event.action == 'closed'&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/workflows/preview-schema-drop.yml&lt;/span&gt;

  &lt;span class="na"&gt;nightly-safety-net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'schedule'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install snowflake-connector-python&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Drop pr_% schemas older than 14 days&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_ACCOUNT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;${{ secrets.SNOWFLAKE_ACCOUNT }}&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;dbt_ci&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_PRIVATE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SNOWFLAKE_PRIVATE_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/preview_safety_net.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/preview_safety_net.py — bounds preview-schema staleness
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snowflake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_ACCOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;private_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_load_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNOWFLAKE_PRIVATE_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBT_CI_ROLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBT_CI_WH_XS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANALYTICS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT SCHEMA_NAME
            FROM   ANALYTICS.INFORMATION_SCHEMA.SCHEMATA
            WHERE  SCHEMA_NAME LIKE &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PR_%&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
              AND  CREATED &amp;lt; DATEADD(day, -14, CURRENT_TIMESTAMP())
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;stale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DROP SCHEMA IF EXISTS ANALYTICS.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; CASCADE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;safety-net dropped &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK — &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; schemas cleaned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives.serialization&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;load_pem_private_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PrivateFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NoEncryption&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;load_pem_private_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;private_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PrivateFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PKCS8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;encryption_algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;NoEncryption&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PR open / sync&lt;/td&gt;
&lt;td&gt;preview-schema-create&lt;/td&gt;
&lt;td&gt;CREATE OR REPLACE SCHEMA … CLONE PROD; post URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR merged&lt;/td&gt;
&lt;td&gt;preview-schema-drop&lt;/td&gt;
&lt;td&gt;DROP SCHEMA IF EXISTS … CASCADE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR rejected&lt;/td&gt;
&lt;td&gt;preview-schema-drop&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR abandoned&lt;/td&gt;
&lt;td&gt;nightly cron at day 14&lt;/td&gt;
&lt;td&gt;DROP SCHEMA IF EXISTS … CASCADE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions outage&lt;/td&gt;
&lt;td&gt;nightly cron&lt;/td&gt;
&lt;td&gt;catches later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyst review&lt;/td&gt;
&lt;td&gt;Snowsight URL from PR comment&lt;/td&gt;
&lt;td&gt;reads preview schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PII schema&lt;/td&gt;
&lt;td&gt;scripts/seed_sampled_preview.py&lt;/td&gt;
&lt;td&gt;5% sample + SHA-256 redaction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, the catalog holds exactly one schema per open PR plus prod; the November catalog audit shows zero orphaned &lt;code&gt;pr_%&lt;/code&gt; schemas older than 14 days. Median preview creation time is 10 seconds; median compute cost per PR is $0.05 (analyst review session on the XS warehouse). Analysts open the preview URL in one click from the PR description.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Preview creation latency&lt;/td&gt;
&lt;td&gt;~10 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview compute cost per PR&lt;/td&gt;
&lt;td&gt;~$0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview storage cost&lt;/td&gt;
&lt;td&gt;$0 (zero-copy)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orphaned schemas (14-day window)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleanup coverage&lt;/td&gt;
&lt;td&gt;100% (trigger + cron)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PII redaction&lt;/td&gt;
&lt;td&gt;opt-in per schema&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Deterministic schema naming&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch_slug&amp;gt;&lt;/code&gt; is generated from GitHub context. Any script that knows the PR number can construct the name. This makes create + drop trivially symmetric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Zero-copy CLONE&lt;/strong&gt;&lt;/strong&gt; — Snowflake and Databricks UC implement clone as metadata. Creation is a 3-second op; storage is $0 until write. The preview is a full mirror without a copy cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Idempotent CREATE OR REPLACE + DROP IF EXISTS&lt;/strong&gt;&lt;/strong&gt; — every re-run is safe. Race conditions between create + push don't corrupt state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;pull_request:closed trigger&lt;/strong&gt;&lt;/strong&gt; — fires on both merge and reject. Reclaims the schema within 30 seconds of PR close.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Nightly safety-net cron&lt;/strong&gt;&lt;/strong&gt; — bounds staleness at 14 days regardless of failure mode. Catches abandoned PRs, GHA outages, fork PRs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — $0 storage (zero-copy), ~$0.05 compute per PR review session (XS warehouse, AUTO_SUSPEND=60), $0.01 workflow overhead. Net cost per PR under $0.10. Compared to the "no preview + wait for merge" baseline the analyst-hour savings alone pay for the system in one week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL warehouse-preview and clone problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on ephemeral environments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — CI/CD recipes for dbt + Airflow + Spark
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Which gate when.&lt;/strong&gt; dbt: &lt;code&gt;dbt build --select state:modified+ --defer --state prod-manifest/ --target ci&lt;/code&gt;. Airflow: &lt;code&gt;DagBag(dag_folder='airflow/dags', include_examples=False).import_errors == {}&lt;/code&gt; first; then operator pytest; then optional &lt;code&gt;dag.test()&lt;/code&gt; on LocalExecutor + Postgres service. Spark: local &lt;code&gt;SparkSession.builder.master("local[2]")&lt;/code&gt; pytest first, then optional docker-compose &lt;code&gt;bitnami/spark:3.5.1&lt;/code&gt; mini-cluster, then &lt;code&gt;spark-submit --num-executors 0&lt;/code&gt; dry-run against a dev cluster. Preview: &lt;code&gt;CREATE OR REPLACE SCHEMA ANALYTICS.pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt; CLONE ANALYTICS.PROD&lt;/code&gt; on PR open; &lt;code&gt;DROP SCHEMA IF EXISTS ... CASCADE&lt;/code&gt; on PR close; nightly cron drops &lt;code&gt;pr_%&lt;/code&gt; older than 14 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slim CI command template.&lt;/strong&gt; &lt;code&gt;dbt build --select state:modified+ --defer --state prod-manifest/ --target ci --fail-fast&lt;/code&gt; + &lt;code&gt;dbt clone --state prod-manifest/ --resource-type model --resource-type seed --resource-type snapshot&lt;/code&gt;. Prod side uploads &lt;code&gt;manifest.json&lt;/code&gt; + &lt;code&gt;run_results.json&lt;/code&gt; + &lt;code&gt;generated_at.txt&lt;/code&gt; to &lt;code&gt;s3://dbt-artifacts/prod/latest/&lt;/code&gt; at the end of every successful run. CI downloads all three; the &lt;code&gt;generated_at.txt&lt;/code&gt; freshness guard warns if manifest is &amp;gt;48 h old.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path-based short-circuit.&lt;/strong&gt; Use &lt;code&gt;dorny/paths-filter@v3&lt;/code&gt; at the top of the workflow with &lt;code&gt;dbt: ['dbt/**']&lt;/code&gt;, &lt;code&gt;airflow: ['airflow/**']&lt;/code&gt;, &lt;code&gt;spark: ['spark/**']&lt;/code&gt;. Never run a system's gates on a PR that did not touch that system. The cheapest CI optimisation ever invented.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DAG parse gate snippet.&lt;/strong&gt; &lt;code&gt;python -c "from airflow.models.dagbag import DagBag; b=DagBag('airflow/dags', include_examples=False); assert not b.import_errors, b.import_errors"&lt;/code&gt; with &lt;code&gt;AIRFLOW_HOME=/tmp/airflow&lt;/code&gt;, &lt;code&gt;AIRFLOW__CORE__LOAD_EXAMPLES=False&lt;/code&gt;, &lt;code&gt;AIRFLOW__CORE__UNIT_TEST_MODE=True&lt;/code&gt;. Runs in 15-30 s; catches every import-time break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pytest SparkSession fixture template.&lt;/strong&gt; &lt;code&gt;@pytest.fixture(scope="session")&lt;/code&gt; → &lt;code&gt;SparkSession.builder.master("local[2]").appName("ci").config("spark.sql.shuffle.partitions", "2").config("spark.ui.enabled", "false").config("spark.sql.session.timeZone", "UTC").getOrCreate()&lt;/code&gt;. Session-scoped so JVM starts once. Under 200 ms per test after warm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview schema naming + cleanup.&lt;/strong&gt; Format &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;slug&amp;gt;&lt;/code&gt; where &lt;code&gt;slug = branch|tr '/' '_' | tr -cd '[:alnum:]_' | tr '[:upper:]' '[:lower:]' | cut -c1-40&lt;/code&gt;. Create with &lt;code&gt;CREATE OR REPLACE SCHEMA ... CLONE prod&lt;/code&gt;; drop with &lt;code&gt;DROP SCHEMA IF EXISTS ... CASCADE&lt;/code&gt;. Nightly cron: &lt;code&gt;SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME LIKE 'PR_%' AND CREATED &amp;lt; DATEADD(day, -14, CURRENT_TIMESTAMP())&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deferral vs clone decision matrix.&lt;/strong&gt; &lt;code&gt;--defer&lt;/code&gt; alone: reads unbuilt refs from prod schema; preview schema contains only rebuilt models. Use when the CI job is the only consumer. &lt;code&gt;--defer&lt;/code&gt; + &lt;code&gt;dbt clone&lt;/code&gt;: preview schema is a full mirror of prod overlaid with rebuilt models. Use when humans (analysts, PMs) query the preview. Cost: identical (both are zero-copy on Snowflake / Databricks UC).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost guardrails.&lt;/strong&gt; Snowflake: &lt;code&gt;WAREHOUSE_SIZE=XSMALL&lt;/code&gt;, &lt;code&gt;AUTO_SUSPEND=60&lt;/code&gt;, &lt;code&gt;AUTO_RESUME=TRUE&lt;/code&gt; — an XS is $1/hr; 60-s suspend keeps typical CI session under $0.10. Databricks: SQL warehouse &lt;code&gt;auto_stop_mins: 5&lt;/code&gt;; interactive cluster &lt;code&gt;autotermination_minutes: 10&lt;/code&gt;. BigQuery: on-demand; every query priced independently. Publish per-gate cost budget as a contract (e.g. dbt-slim-ci ≤ $0.30, airflow-parse ≤ $0.02).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airflow OCI image versioning contract.&lt;/strong&gt; Build args → OCI labels: &lt;code&gt;GIT_SHA&lt;/code&gt; → &lt;code&gt;org.opencontainers.image.revision&lt;/code&gt;, &lt;code&gt;GIT_BRANCH&lt;/code&gt; → &lt;code&gt;org.opencontainers.image.ref.name&lt;/code&gt;, &lt;code&gt;CI_RUN_ID&lt;/code&gt; → &lt;code&gt;ai.pipecode.ci.run.id&lt;/code&gt;, &lt;code&gt;AIRFLOW_VERSION&lt;/code&gt; → &lt;code&gt;ai.pipecode.airflow.version&lt;/code&gt;. Immutable tag &lt;code&gt;sha-&amp;lt;short&amp;gt;&lt;/code&gt; for deploys; mutable &lt;code&gt;pr-&amp;lt;PR&amp;gt;&lt;/code&gt; / &lt;code&gt;main&lt;/code&gt; for humans. &lt;code&gt;docker inspect&lt;/code&gt; reveals provenance in one command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spark packaging contract.&lt;/strong&gt; PySpark: &lt;code&gt;pip wheel&lt;/code&gt; → &lt;code&gt;job-&amp;lt;version&amp;gt;-py3-none-any.whl&lt;/code&gt; + &lt;code&gt;entry_points = {"console_scripts": [...]}&lt;/code&gt;. Scala: &lt;code&gt;sbt assembly&lt;/code&gt; → &lt;code&gt;job-&amp;lt;version&amp;gt;-assembly.jar&lt;/code&gt;. Config never baked into artifact; selected by env var at submit time. &lt;code&gt;--version&lt;/code&gt; flag prints git SHA. &lt;code&gt;--dry-run&lt;/code&gt; flag constructs SparkSession + exits 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State-artifact freshness guard.&lt;/strong&gt; dbt CI downloads &lt;code&gt;generated_at.txt&lt;/code&gt; alongside &lt;code&gt;manifest.json&lt;/code&gt;; if &lt;code&gt;age_hours &amp;gt; 48&lt;/code&gt; emit a &lt;code&gt;::warning::&lt;/code&gt; in GitHub Actions. Stale manifest = slim CI over-selects (marks unchanged as modified) or under-selects (misses real changes). Warning surfaces the problem before it silently distorts CI output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-build fallback triggers.&lt;/strong&gt; dbt slim CI does not detect all impact classes. Fall back to full build when the PR touches: &lt;code&gt;macros/**&lt;/code&gt;, &lt;code&gt;dbt_project.yml&lt;/code&gt;, &lt;code&gt;packages.yml&lt;/code&gt;, &lt;code&gt;requirements.txt&lt;/code&gt;, or &lt;code&gt;profiles.yml&lt;/code&gt;. Detect via &lt;code&gt;git diff --name-only origin/main | grep -qE '(macros/|dbt_project\.yml|packages\.yml)'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII-sensitive preview pattern.&lt;/strong&gt; Don't zero-copy clone PII schemas; instead materialise a 5% &lt;code&gt;SAMPLE&lt;/code&gt; with SHA-256 redaction on sensitive columns (&lt;code&gt;SHA2(email, 256) AS email&lt;/code&gt;). Storage cost applies (~5% of source); worth it for GDPR / SOC2. Document redaction list in versioned config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-call runbook order.&lt;/strong&gt; (1) CI red on prod-blocking PR → check gate that failed; (2) dbt slim-CI failed on state comparison → verify &lt;code&gt;s3://dbt-artifacts/prod/latest/manifest.json&lt;/code&gt; exists and is &amp;lt;48 h old; (3) Airflow parse failed → open the &lt;code&gt;import_errors&lt;/code&gt; traceback; (4) Spark dry-run failed → check dev-EMR cluster health; (5) preview schema missing → check create workflow logs, re-run manually; (6) preview schema orphaned → nightly cron logs at 06:00 UTC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is slim CI in dbt?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;slim ci dbt&lt;/code&gt; is the pattern of running &lt;code&gt;dbt build --select state:modified+ --defer --state prod-manifest/&lt;/code&gt; in CI instead of a full &lt;code&gt;dbt build&lt;/code&gt;. The prod &lt;code&gt;manifest.json&lt;/code&gt; from the last successful production run is downloaded as an artifact; &lt;code&gt;state:modified+&lt;/code&gt; picks only the models whose SQL, config, or upstream refs have changed since that manifest, plus their descendants; &lt;code&gt;--defer&lt;/code&gt; routes any unbuilt parent refs to the prod schema rather than re-materialising them in the CI schema. The result is that a CI run rebuilds 3-15 models on a typical PR instead of 200-1500, cutting the p95 CI time from about 45 minutes to about 2 minutes and the compute cost from $50-$500 per run to about $0.30. Every senior analytics engineer names slim CI in the first minute of any dbt CI/CD interview because it is &lt;em&gt;the&lt;/em&gt; load-bearing CI optimisation for dbt projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I set up state comparison for dbt CI?
&lt;/h3&gt;

&lt;p&gt;State comparison requires an artifact contract between production and CI: the prod dbt job uploads &lt;code&gt;target/manifest.json&lt;/code&gt; (plus &lt;code&gt;run_results.json&lt;/code&gt; and a &lt;code&gt;generated_at.txt&lt;/code&gt; freshness marker) to a known object-store location (typically &lt;code&gt;s3://dbt-artifacts/prod/latest/&lt;/code&gt;) at the end of every successful run. The CI workflow downloads those files into a local &lt;code&gt;prod-manifest/&lt;/code&gt; directory before running &lt;code&gt;dbt build&lt;/code&gt;; the &lt;code&gt;--state prod-manifest/&lt;/code&gt; flag tells dbt where to look. Under the hood, dbt fingerprints every model (SHA-256 of SQL text + config + upstream refs + sources + contract), compares the CI manifest against the prod manifest, and marks any model with a differing fingerprint as &lt;code&gt;state:modified&lt;/code&gt;. Ship the freshness guard alongside — warn if the prod manifest is more than 48 hours old, because stale prod state distorts the diff (over-selects or under-selects). Bootstrap by running one manual full build against the target-prod schema and uploading its manifest; subsequent runs incrementally refresh the artifact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need a full Spark cluster in CI?
&lt;/h3&gt;

&lt;p&gt;No, and running one is the single most common CI-cost mistake in Spark shops. The CI matrix for Spark is three gates in ascending cost: (a) local &lt;code&gt;SparkSession.builder.master("local[2]")&lt;/code&gt; pytest fixture, which runs in the GHA runner with zero cluster cost and covers pure transformations, UDFs, and schema assertions — this catches roughly 80% of Spark bugs; (b) docker-compose &lt;code&gt;bitnami/spark:3.5.1&lt;/code&gt; mini-cluster for jobs that exercise real shuffle, broadcast joins, or partitioning behaviour — adds ~2 minutes to CI when it runs but stays gated on &lt;code&gt;spark/jobs/**&lt;/code&gt; path filter; (c) a &lt;code&gt;spark-submit --num-executors 0 --dry-run&lt;/code&gt; submit against a small dev EMR cluster to validate config, JAR resolution, and cluster reachability without ever provisioning executors. The real prod cluster is &lt;em&gt;never&lt;/em&gt; touched by CI. If you find your CI job submitting to the prod Spark cluster, you have a cost bug — refactor to the three-gate pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I test Airflow DAGs in CI?
&lt;/h3&gt;

&lt;p&gt;Three layers of gates in ascending cost. First, the DAG parse gate: instantiate &lt;code&gt;DagBag(dag_folder='airflow/dags', include_examples=False)&lt;/code&gt; in CI and assert &lt;code&gt;dagbag.import_errors == {}&lt;/code&gt;. Runs in 15-30 seconds and catches every import-time break — the single highest-impact class of Airflow bug because import failures pause the whole DagBag, not just the broken DAG. Second, pytest against custom operators and hooks with mocked backends: instantiate the operator, provide a mock context, assert on return values and mock call args. Coverage bar: 95% line coverage on everything in &lt;code&gt;airflow/plugins/&lt;/code&gt;. Third, an optional integration test using &lt;code&gt;dag.test(execution_date=...)&lt;/code&gt; (Airflow 2.5+) with a &lt;code&gt;LocalExecutor&lt;/code&gt; and a Postgres service — runs the entire DAG synchronously without a scheduler. Reserve the integration layer for high-value DAGs (ones touching shared warehouse tables, dynamic task mapping, etc.) so the CI matrix stays under 6 minutes. Wrap all three in a labelled OCI image built with &lt;code&gt;apache/airflow:2.10.x&lt;/code&gt; as the deployable artifact.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do preview environments work with Snowflake?
&lt;/h3&gt;

&lt;p&gt;Every open pull request maps to a throwaway schema named &lt;code&gt;pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt;&lt;/code&gt; in Snowflake, created on PR open via zero-copy clone and dropped on PR close. The create workflow runs &lt;code&gt;CREATE OR REPLACE SCHEMA ANALYTICS.pr_4712_add_orders_v2 CLONE ANALYTICS.PROD&lt;/code&gt; — Snowflake implements clone as a metadata operation, so a 10 TB source schema clones in about 3 seconds and costs $0 in storage until a write happens. The slim-CI &lt;code&gt;dbt build&lt;/code&gt; runs against the preview schema; &lt;code&gt;dbt clone&lt;/code&gt; fills in any unmodified tables so the preview is a full mirror. A comment is posted to the PR with a Snowsight deep-link that pre-selects the schema, so reviewers open the preview in one click. The drop workflow triggers on &lt;code&gt;pull_request&lt;/code&gt; type &lt;code&gt;closed&lt;/code&gt; (both merge and reject) and runs &lt;code&gt;DROP SCHEMA IF EXISTS pr_&amp;lt;PR&amp;gt;_&amp;lt;branch&amp;gt; CASCADE&lt;/code&gt;. A nightly cron at 06:00 UTC drops any &lt;code&gt;pr_%&lt;/code&gt; schema older than 14 days as a safety net for abandoned PRs or GHA outages. Compute uses an XS warehouse (&lt;code&gt;WAREHOUSE_SIZE=XSMALL, AUTO_SUSPEND=60&lt;/code&gt;), keeping typical per-PR compute cost under $0.10.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is dbt deferral vs dbt clone?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;dbt deferral&lt;/code&gt; (&lt;code&gt;--defer --state prod-manifest/&lt;/code&gt;) is a &lt;em&gt;read-side&lt;/em&gt; optimisation: when a CI-built model runs &lt;code&gt;{{ ref('unmodified_upstream') }}&lt;/code&gt;, dbt sees the ref was not rebuilt in this run, consults the state manifest, and rewrites the ref to point at the &lt;em&gt;prod&lt;/em&gt; schema instead of the CI schema. No table is materialised for the deferred parent in the preview schema; the read is redirected. &lt;code&gt;dbt clone&lt;/code&gt; (&lt;code&gt;dbt clone --state prod-manifest/ --resource-type model --resource-type seed --resource-type snapshot&lt;/code&gt;) is a &lt;em&gt;write-side&lt;/em&gt; operation: for every unmodified model in the state manifest, dbt executes &lt;code&gt;CREATE OR REPLACE &amp;lt;preview&amp;gt;.&amp;lt;model&amp;gt; CLONE prod.&amp;lt;model&amp;gt;&lt;/code&gt;, producing a zero-copy metadata pointer in the preview schema. Use &lt;code&gt;--defer&lt;/code&gt; alone when the CI job is the only consumer (fast, no schema footprint). Use &lt;code&gt;--defer&lt;/code&gt; + &lt;code&gt;dbt clone&lt;/code&gt; when humans (analysts, PMs) query the preview and expect all the joined tables to be there. Cost is identical (both are zero-copy on Snowflake and Databricks UC); the difference is whether the preview schema is a full mirror or just a delta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt; for the pipeline-CI, slim-build, and state-comparison problems senior interviewers love.&lt;/li&gt;
&lt;li&gt;Rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; for warehouse-side clone semantics, incremental modeling, and preview-schema query patterns.&lt;/li&gt;
&lt;li&gt;Sharpen the systems axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt; for CI/CD architecture, ephemeral environment design, and deployable-artifact provenance scenarios.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the four-axis CI decision matrix against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lock in pipeline-CI muscle memory&lt;/h3&gt;

&lt;p&gt;Docs explain slim CI. PipeCode drills explain the decision — when `state:modified+` is enough versus when to fall back to a full build, when the DAG parse gate saves the scheduler versus when a `dag.test()` integration is worth the six minutes, when a docker-compose Spark cluster is worth the setup cost versus when `local[2]` pytest is the honest answer, when a zero-copy preview schema replaces "wait for merge" as the analyst review UX. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — pattern-first practice tuned for the CI/CD trade-offs senior analytics engineers and data platform engineers actually face.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice ETL problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice design problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Data SRE Playbook: SLO-Driven Pipelines, Error Budgets &amp; On-Call Rotations for DE Teams</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 31 Jul 2026 14:38:09 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/data-sre-playbook-slo-driven-pipelines-error-budgets-on-call-rotations-for-de-teams-54pk</link>
      <guid>https://dev.to/gowthampotureddi/data-sre-playbook-slo-driven-pipelines-error-budgets-on-call-rotations-for-de-teams-54pk</guid>
      <description>&lt;p&gt;A &lt;strong&gt;&lt;code&gt;data sre playbook&lt;/code&gt;&lt;/strong&gt; is the operating manual senior data teams reach for the day they stop shipping "the DAG turned green" as a reliability claim and start owning user-visible pipeline reliability the same way SRE teams own web-service reliability — with measurable SLIs, explicit SLOs, monthly error budgets, burn-rate paging, follow-the-sun on-call rotations, linked runbooks, and blameless postmortems that compound into a reliability roadmap. The reason every senior data engineer needs one in 2026 is that the number of downstream consumers of an operational data pipeline — dashboards, ML models, feature stores, reverse-ETL sync targets, customer-facing APIs — has grown to the point where a two-hour freshness gap on the &lt;code&gt;orders&lt;/code&gt; table is a customer-visible outage, and "we'll fix it on the next DAG run" is not an incident response.&lt;/p&gt;

&lt;p&gt;This guide is the senior-DE walkthrough you wished existed the first time an executive asked "what's our reliability commitment on the warehouse?", or an interviewer probed "walk me through the SLIs, SLOs, and error budgets you'd set for a streaming pipeline," or a director cornered you with "why did that data outage take three hours to notice and eleven hours to resolve?" It covers the four failure axes every data platform must instrument — &lt;code&gt;data freshness slo&lt;/code&gt; (max age of the latest record), completeness (fraction of source rows landed), volume (row-count within an expected band), and quality (fraction of rows passing schema and constraint checks) — the &lt;code&gt;error budget data engineering&lt;/code&gt; math that translates a 99.9% freshness target into a 43-minute monthly ceiling, the multi-window multi-burn-rate &lt;code&gt;pipeline sli&lt;/code&gt; alerts modelled on the Google SRE workbook, the &lt;code&gt;data on-call rotation&lt;/code&gt; shape and handoff ritual, the 5-field &lt;code&gt;data runbook&lt;/code&gt; template every alert links to, and the blameless postmortem walk from detect through mitigate, resolve, and review. Every section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzqk2th74ap22sqe3bzrh.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzqk2th74ap22sqe3bzrh.jpeg" alt="PipeCode blog header for the Data SRE playbook — bold white headline 'Data SRE Playbook' over a hero composition of four small glyph medallions (SLO dial, error-budget tank, pager, runbook) arranged around a central purple 'RELIABILITY' wax seal on a dark gradient." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt;, rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt;, and sharpen the reliability axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;data-validation practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why data teams need SRE practices in 2026&lt;/li&gt;
&lt;li&gt;SLIs and SLOs for data pipelines&lt;/li&gt;
&lt;li&gt;Error budgets and burn-rate alerts&lt;/li&gt;
&lt;li&gt;On-call rotations and incident runbooks&lt;/li&gt;
&lt;li&gt;Post-incident review and reliability roadmap&lt;/li&gt;
&lt;li&gt;Cheat sheet — Data SRE recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why data teams need SRE practices in 2026 — the four failure axes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Data pipelines fail differently from web services — the same SRE principles apply, the SLIs do not
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;a data platform is reliable when the datasets its consumers depend on are fresh, complete, correctly-sized, and quality-checked within an explicitly agreed-upon target — and pipeline reliability engineering is the practice of translating those four consumer-visible axes into measurable SLIs, monthly SLOs, error budgets, burn-rate alerts, on-call rotations, and blameless postmortems, exactly like SRE teams do for web services but with pipeline-shaped inputs instead of HTTP-shaped ones&lt;/strong&gt;. Every senior data-engineering interview in 2026 probes this territory because the number of teams claiming "SRE for data" without actually shipping a single measurable SLI has exploded, and the interviewers you'll face have been burned by fluent-sounding candidates whose reliability practice starts and stops at "we monitor DAG status."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four failure axes interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Freshness.&lt;/strong&gt; The maximum age of the latest record in a target table relative to when it should have arrived. If a hourly &lt;code&gt;orders&lt;/code&gt; pipeline runs at :05 past the hour, the freshness target might be "no consumer query should ever see &lt;code&gt;MAX(ingested_at) &amp;lt; now() - 65 min&lt;/code&gt;." Freshness is the axis that hurts business dashboards first; it's the most common SLO to write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completeness.&lt;/strong&gt; The fraction of expected source rows that have landed in the target within the SLO window. If the source produced 10,000 orders in the hour and only 9,850 arrived, completeness is 98.5% for that window. Completeness catches silent drops — filter bugs, schema mismatches, network truncation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume.&lt;/strong&gt; Whether the row count of the target table sits within an expected band (usually &lt;code&gt;min_expected &amp;lt; count &amp;lt; max_expected&lt;/code&gt; from a rolling 7- or 28-day baseline). Volume catches "we ingested but the count is wrong" — a duplicated upstream feed doubling counts, a broken filter halving them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality.&lt;/strong&gt; The fraction of rows that pass schema, constraint, and business-rule checks (nulls in required columns, invalid enums, out-of-range values, orphan foreign keys). Quality catches "we ingested the right count but the data is garbage."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why "did the DAG run green" is not an SLI.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DAG green ≠ data correct.&lt;/strong&gt; An Airflow task can succeed while writing an empty file, writing to the wrong partition, or writing stale data because an upstream watermark stalled. The status of the &lt;em&gt;job&lt;/em&gt; is a proxy for reliability at best; the status of the &lt;em&gt;dataset&lt;/em&gt; is what consumers experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DAG green ignores latency-of-detection.&lt;/strong&gt; A pipeline that fails at 09:00 and is caught at 15:00 has still burned six hours of business value. Job-status monitoring wakes you up when a task raises; SLI monitoring wakes you up when the dataset stops meeting its contract, which is often earlier and sometimes later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DAG green ignores upstream reality.&lt;/strong&gt; If the source system stopped producing rows at 08:00, every downstream DAG will still turn green (they read an empty delta and write an empty file). SLI-based monitoring on volume + freshness catches this class of "silent zero" failure that job monitoring cannot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DAG green ignores partial success.&lt;/strong&gt; A Spark job with &lt;code&gt;spark.sql.files.ignoreCorruptFiles = true&lt;/code&gt; succeeds while silently dropping bad Parquet files. A dbt run with &lt;code&gt;--fail-fast&lt;/code&gt; off finishes with 2 of 200 tests failing but a green run icon. Only per-dataset SLIs make partial-success failures visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The SRE contract: user-visible reliability, not job status.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User-visible metrics only.&lt;/strong&gt; An SLI is measured on the &lt;em&gt;output artifact&lt;/em&gt; (the target table, the served view, the exported file) — not on the process that produced it. This constraint forces the metric to align with what a downstream consumer cares about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target expressed as an SLO.&lt;/strong&gt; "99.9% of hourly windows have freshness &amp;lt; 65 minutes." "99.5% of daily runs have completeness &amp;gt; 99% of expected count." SLOs are always a &lt;em&gt;percentage of measurement windows meeting a threshold&lt;/em&gt;, never "we always meet it."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error budget = 1 − SLO.&lt;/strong&gt; A 99.9% freshness SLO means 0.1% of hourly windows can miss — that's 43 minutes per 30-day month. The budget is the operational contract with the rest of the organisation: within it, releases proceed; outside it, releases halt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting off the SLI, not the pipeline.&lt;/strong&gt; Burn-rate alerts fire when the SLI degrades faster than the budget will tolerate. The alert wakes a human because the &lt;em&gt;user-visible contract&lt;/em&gt; is at risk, not because a job status changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you name &lt;strong&gt;all four axes&lt;/strong&gt; (freshness, completeness, volume, quality) without prompting? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you say &lt;strong&gt;"DAG status is not an SLI"&lt;/strong&gt; when asked what to monitor? — required answer.&lt;/li&gt;
&lt;li&gt;Do you cite the &lt;strong&gt;error-budget number&lt;/strong&gt; (99.9% ≈ 43 min/month) without pulling out a calculator? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you describe a burn-rate alert as &lt;strong&gt;"multi-window multi-rate"&lt;/strong&gt; and cite the Google SRE workbook? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you frame postmortems as &lt;strong&gt;"blameless, focused on contributing factors"&lt;/strong&gt; rather than root-cause five-whys? — senior signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the four-axis reliability table for an orders pipeline
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful artifact for a data SRE interview is a memorised 4×4 axis table. Every senior reliability discussion converges on this table within the first ten minutes; having it in your head is what separates a fluent answer from a stumbling one. Walk through building the table for a hypothetical hourly &lt;code&gt;orders&lt;/code&gt; pipeline that lands into a Snowflake warehouse.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source.&lt;/strong&gt; &lt;code&gt;production.public.orders&lt;/code&gt; (Postgres OLTP).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target.&lt;/strong&gt; &lt;code&gt;analytics.orders&lt;/code&gt; (Snowflake, updated hourly by an Airflow DAG at :05 past the hour).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumers.&lt;/strong&gt; Executive dashboard (Looker), ML fraud model (feature store), reverse-ETL sync to Salesforce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User contract.&lt;/strong&gt; All three consumers depend on data being no more than ~1 hour behind, complete against the source, correctly-sized, and quality-checked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the four-axis SLI/SLO table for the &lt;code&gt;analytics.orders&lt;/code&gt; pipeline and pick a numeric target for each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;SLO target&lt;/th&gt;
&lt;th&gt;Budget / month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Freshness&lt;/td&gt;
&lt;td&gt;max(&lt;code&gt;ingested_at&lt;/code&gt;) age at query time&lt;/td&gt;
&lt;td&gt;99.9% of minutes &amp;lt; 65 min&lt;/td&gt;
&lt;td&gt;~43 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completeness&lt;/td&gt;
&lt;td&gt;target rowcount / source rowcount per hour&lt;/td&gt;
&lt;td&gt;99.5% of hours ≥ 99%&lt;/td&gt;
&lt;td&gt;~3.6 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Volume&lt;/td&gt;
&lt;td&gt;rowcount vs 7-day median band ±30%&lt;/td&gt;
&lt;td&gt;99.5% of hours within band&lt;/td&gt;
&lt;td&gt;~3.6 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality&lt;/td&gt;
&lt;td&gt;rows passing schema + constraint checks&lt;/td&gt;
&lt;td&gt;99.9% of daily runs ≥ 99%&lt;/td&gt;
&lt;td&gt;~1 day/quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Freshness SLI — computed on the target table at query time&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingested_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'65 minutes'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;meets_freshness_slo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingested_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;current_age&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Completeness SLI — computed hourly by comparing target to source&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;source_hour&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_src&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;production&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'24 hours'&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;target_hour&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_tgt&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'24 hours'&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_tgt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_tgt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_tgt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;completeness_ratio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_tgt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;
            &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;                            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;meets_completeness_slo&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;source_hour&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;target_hour&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Volume SLI — computed hourly against a 7-day rolling median band&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hour_of_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;percentile_cont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WITHIN&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hourly_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;median_ct&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hour_of_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hourly_count&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;
        &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hourly_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;median_ct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hourly_count&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;median_ct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;median_ct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
            &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;meets_volume_slo&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hour_of_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hourly_count&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'24 hours'&lt;/span&gt;
        &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;baseline&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour_of_day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The freshness SLI is a single per-query check — &lt;code&gt;MAX(ingested_at)&lt;/code&gt; on the target table. If the answer is younger than the SLO threshold, the minute counts as "meeting"; otherwise it burns budget. Computing this in the warehouse rather than in Airflow means it reflects the &lt;em&gt;dataset&lt;/em&gt; state, not the &lt;em&gt;pipeline&lt;/em&gt; state.&lt;/li&gt;
&lt;li&gt;The completeness SLI joins hourly source counts against hourly target counts. Ratio &lt;code&gt;&amp;lt; 0.99&lt;/code&gt; for the hour means the hour "misses" its SLO. The 5-minute look-back safety window (not shown; add &lt;code&gt;WHERE event_ts &amp;lt; now() - INTERVAL '5 min'&lt;/code&gt;) prevents in-flight rows from being counted as missing.&lt;/li&gt;
&lt;li&gt;The volume SLI compares each hour's target count to the median of the same hour-of-day across the last 7 days. The ±30% band absorbs normal weekly variance while catching the "3x volume" and "0.1x volume" outliers that indicate upstream duplication or a broken filter.&lt;/li&gt;
&lt;li&gt;The quality SLI (not shown in code — computed via Great Expectations or dbt tests) is the fraction of rows passing a battery of expectations: &lt;code&gt;orders.order_id NOT NULL&lt;/code&gt;, &lt;code&gt;orders.status IN ('pending','shipped','delivered','cancelled')&lt;/code&gt;, &lt;code&gt;orders.total_cents &amp;gt; 0&lt;/code&gt;, &lt;code&gt;orders.customer_id REFERENCES customers.id&lt;/code&gt;. Aggregate to "% of rows passing all checks" for the day.&lt;/li&gt;
&lt;li&gt;All four SLIs feed into a single reliability dashboard and a single error-budget accounting table. When &lt;em&gt;any&lt;/em&gt; one SLI's budget approaches exhaustion, the on-call receives a burn-rate page. When &lt;em&gt;any&lt;/em&gt; one SLI's budget is exhausted, feature releases against the pipeline are halted until budget replenishes at the start of the next month.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;th&gt;Axis they care most about&lt;/th&gt;
&lt;th&gt;SLO target&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Executive dashboard&lt;/td&gt;
&lt;td&gt;Freshness&lt;/td&gt;
&lt;td&gt;99.9% &amp;lt; 65 min&lt;/td&gt;
&lt;td&gt;dashboards are read at any moment; stale = wrong decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML fraud model&lt;/td&gt;
&lt;td&gt;Completeness&lt;/td&gt;
&lt;td&gt;99.5% ≥ 99%&lt;/td&gt;
&lt;td&gt;model retrained hourly; missing rows = biased scoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reverse-ETL to Salesforce&lt;/td&gt;
&lt;td&gt;Volume&lt;/td&gt;
&lt;td&gt;99.5% within band&lt;/td&gt;
&lt;td&gt;wrong volume = wrong sales pipeline forecast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;Quality&lt;/td&gt;
&lt;td&gt;99.9% daily ≥ 99%&lt;/td&gt;
&lt;td&gt;broken schema = downstream crashes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never pick an SLI based on "what the pipeline emits." Pick it based on (freshness × completeness × volume × quality) — the four axes a downstream consumer actually experiences. Write the table on a whiteboard first; the SLI selection falls out of the consumer contract.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — what interviewers actually probe
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The senior data-SRE interview has a predictable structure: the interviewer opens with an ambiguous question ("how do you monitor a data pipeline?"), then progressively narrows to test whether you know the axes. The candidates who name the SLIs in sentence one score highest; the candidates who describe "Airflow alerts on failure" score lowest. Walk through the interview grading rubric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous opener.&lt;/strong&gt; "How would you monitor an hourly ETL pipeline?" — invites you to name SLIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 1.&lt;/strong&gt; "What's an SLI vs an SLO?" — probes definitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 2.&lt;/strong&gt; "How much downtime does 99.9% freshness allow?" — probes error-budget math.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 3.&lt;/strong&gt; "When do you page vs ticket?" — probes burn-rate alerting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 4.&lt;/strong&gt; "Walk me through your last data incident postmortem." — probes maturity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draft a 5-minute senior data-SRE answer that covers all four axes without waiting to be asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interview signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;Senior answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SLI named&lt;/td&gt;
&lt;td&gt;"we alert on Airflow failures"&lt;/td&gt;
&lt;td&gt;"freshness, completeness, volume, quality — each with an SLO"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO stated&lt;/td&gt;
&lt;td&gt;"we aim for 99% uptime"&lt;/td&gt;
&lt;td&gt;"99.9% freshness &amp;lt; 65 min = 43 min/month error budget"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alert semantics&lt;/td&gt;
&lt;td&gt;"we page on task failure"&lt;/td&gt;
&lt;td&gt;"multi-window multi-burn-rate on the freshness SLI"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-call shape&lt;/td&gt;
&lt;td&gt;"the person who wrote the DAG gets paged"&lt;/td&gt;
&lt;td&gt;"primary + secondary weekly rotation with runbook links"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postmortem&lt;/td&gt;
&lt;td&gt;"we write a Slack thread"&lt;/td&gt;
&lt;td&gt;"blameless review, contributing factors, tracked action items"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Senior data-SRE answer template (5 minutes)
============================================

Minute 1 — name the SLIs up front
  "I'd instrument four SLIs per pipeline: freshness (max age of latest row),
   completeness (target/source rowcount), volume (rowcount vs baseline band),
   and quality (rows passing schema + constraint checks). Each gets an SLO
   like 99.9% of hourly windows meeting the threshold."

Minute 2 — error-budget math
  "99.9% freshness means 0.1% of the month can miss, which is 43 minutes.
   That's the operational contract with the rest of the org: within the
   budget, releases proceed; outside it, releases halt until the budget
   resets at month start."

Minute 3 — burn-rate alerts
  "Instead of alerting on any missed minute, I use multi-window
   multi-burn-rate: a fast-burn window (1 h) at 14x normal spend pages
   immediately, a slow-burn window (6 h) at 6x tickets the team. This
   catches both cliffs and creeping degradation without false pages."

Minute 4 — on-call + runbook
  "Weekly primary + secondary rotation with follow-the-sun handoff for
   global teams. Every alert links to a 5-field runbook: symptom,
   detection, mitigation, escalation, owner. No runbook link = alert
   isn't allowed to fire."

Minute 5 — postmortem + roadmap
  "Blameless postmortem within 5 business days: timeline, contributing
   factors (not a single root cause), action items with owners and due
   dates. Action items feed the reliability roadmap; SLO trends drive
   the SLI backlog next quarter."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Minute 1 is the crucial framing. Naming the four SLIs immediately — freshness, completeness, volume, quality — signals you've operated a data platform under real reliability targets, not just written DAGs. Weak candidates start with tools ("we use Airflow and Datadog") before naming a metric.&lt;/li&gt;
&lt;li&gt;Minute 2 does the error-budget math out loud. Interviewers listen for whether you can convert an SLO percentage into an operational number without hesitation. "99.9% is 43 minutes per month" is the answer you should have burned into muscle memory.&lt;/li&gt;
&lt;li&gt;Minute 3 introduces multi-window multi-burn-rate. This is the SRE-workbook-canonical alerting pattern; naming it, and citing the fast-vs-slow-burn distinction, is a senior signal that separates you from candidates who alert on task-failure.&lt;/li&gt;
&lt;li&gt;Minute 4 addresses on-call operationally. The 5-field runbook (symptom, detection, mitigation, escalation, owner) is the concrete template; refusing to allow alerts without runbook links is the enforcement mechanism. Both are things you'd expect a mature platform to have.&lt;/li&gt;
&lt;li&gt;Minute 5 covers postmortems and the reliability roadmap. "Blameless, contributing factors, tracked action items, feed the roadmap" is the four-part cycle that turns incidents into reliability improvements over quarters. This is the axis mid-level candidates skip entirely.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading criterion&lt;/th&gt;
&lt;th&gt;Weak score&lt;/th&gt;
&lt;th&gt;Senior score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Names SLIs in minute 1&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;States error-budget number&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names burn-rate alerting&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names runbook fields&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names postmortem cycle&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The senior data-SRE answer is a 5-minute monologue that covers SLIs, error budgets, burn-rate alerts, on-call rotation, and postmortems without waiting for the follow-ups. Rehearse it once; deploy it every time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the "SRE maturity" decision tree for a data team
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Given a data team, the senior architect runs a 4-question maturity check in their head. Codifying the check makes the interview answer reproducible: any interviewer can hand you a team description and you can walk the tree out loud. Walk through the tree with three canonical scenarios: a startup with one DE and no SLIs, a mid-market team with green DAGs only, and a mature platform team with per-dataset SLOs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Q1.&lt;/strong&gt; Do the pipelines have measurable SLIs on the four axes (freshness, completeness, volume, quality)? → no = start there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q2.&lt;/strong&gt; Are the SLOs written down and agreed with downstream consumers? → no = do the SLO-negotiation exercise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q3.&lt;/strong&gt; Are burn-rate alerts wired to a rotation with runbook links? → no = the SLIs are decorative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q4.&lt;/strong&gt; Are postmortems blameless and tracked as action items on a reliability roadmap? → no = incidents don't compound into improvements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Walk the maturity tree for the three scenarios and record the recommended next step for each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Q1 SLIs?&lt;/th&gt;
&lt;th&gt;Q2 SLOs?&lt;/th&gt;
&lt;th&gt;Q3 alerts + runbook?&lt;/th&gt;
&lt;th&gt;Q4 postmortems?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Startup, 1 DE, no SLIs&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid-market, green DAGs only&lt;/td&gt;
&lt;td&gt;partial (freshness)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mature platform&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Data-SRE maturity classifier (illustrative)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;data_sre_maturity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;has_slis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;has_slos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;has_burn_rate_alerts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;has_blameless_postmortems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the recommended next step on the SRE maturity ladder.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;has_slis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Level 0 — instrument SLIs on all four axes first&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;has_slos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Level 1 — negotiate SLO targets with downstream consumers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;has_burn_rate_alerts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Level 2 — wire multi-window burn-rate alerts + runbook links&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;has_blameless_postmortems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Level 3 — adopt blameless postmortems and a reliability roadmap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Level 4 — mature; iterate on SLI selection and chaos drills&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="c1"&gt;# Walk the three scenarios
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;data_sre_maturity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# → 'Level 0 — instrument SLIs on all four axes first'
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;data_sre_maturity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# → 'Level 1 — negotiate SLO targets with downstream consumers'
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;data_sre_maturity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# → 'Level 4 — mature; iterate on SLI selection and chaos drills'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scenario 1 — the one-DE startup has no SLIs, no SLOs, no alerts, no postmortems. The next step is not "adopt SRE" but "instrument the SLIs" — you cannot manage what you don't measure. Start by adding freshness on the top three most-consumed tables; expand from there.&lt;/li&gt;
&lt;li&gt;Scenario 2 — the mid-market team has partial SLIs (freshness only, computed as a metric but never with an agreed target). The next step is negotiating SLO numbers with consumers: sit with the dashboard owner and agree "99.9% &amp;lt; 65 min," write it in a doc, and only then wire alerts.&lt;/li&gt;
&lt;li&gt;Scenario 3 — the mature platform team is at Level 4 and iterating. The next step is not "climb the ladder" but "expand SLI coverage" (from 20 tables to 200 tables), "reduce false-positive alert rate," and "run chaos drills quarterly to verify the runbooks."&lt;/li&gt;
&lt;li&gt;The maturity ladder is &lt;em&gt;strict&lt;/em&gt; — you cannot jump levels. A team that adds burn-rate alerts without agreed SLOs will burn out on false pages. A team that writes blameless postmortems without burn-rate alerts will find nothing to write about because incidents are silently ignored.&lt;/li&gt;
&lt;li&gt;If none of Q1-Q4 pass, the correct answer to "how do we adopt data SRE?" is "start with SLIs on your three most-consumed tables and iterate." Refuse to endorse tools ("buy Monte Carlo," "adopt Datadog") until Q1 is passing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Startup, no SLIs&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Instrument freshness on top 3 tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid-market, green DAGs&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Negotiate SLO targets with consumers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mature platform&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Expand SLI coverage; chaos drills&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The four-level maturity ladder is a whiteboard-friendly answer. Practice walking it end-to-end so an interviewer can hand you any team description and get a level + next step in under 60 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on data SRE adoption
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You join a 40-engineer data platform team that has 200 Airflow DAGs, no SLIs, and monitors reliability by 'did the DAG turn green.' Design the 90-day plan to introduce SRE practices without breaking the team's ability to ship. Cover the SLI you'd start with, the SLO negotiation, the alerting rollout, the on-call bootstrapping, and the metric you'd use to measure your own success."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a 90-day SLI-first rollout with staged SLO negotiation and phased alert cutover
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 90-day data-SRE rollout plan (illustrative)&lt;/span&gt;
&lt;span class="na"&gt;rollout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;weeks_0_2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;instrument SLIs on top 10 tables&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;identify top 10 tables by downstream query count (via warehouse audit logs)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;implement freshness SLI (SELECT MAX(ingested_at) FROM t)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;implement completeness SLI (source rowcount vs target rowcount per hour)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;export both as Prometheus gauges (label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;table_name)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build a "Data reliability" Grafana dashboard (no alerts yet)&lt;/span&gt;

  &lt;span class="na"&gt;weeks_2_6&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;negotiate SLO targets with downstream consumers&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;schedule 30-min meetings with owner of each top-10 dashboard/service&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;propose default SLOs (99.9% freshness &amp;lt; 2x pipeline cadence)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;agree per-table SLO in writing (Confluence doc)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;publish SLO catalogue link from every table in the data catalogue&lt;/span&gt;

  &lt;span class="na"&gt;weeks_6_10&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wire burn-rate alerts + runbooks&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;draft 5-field runbook per SLO (symptom, detection, mitigation, escalation, owner)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;configure multi-window multi-burn-rate alert rules in Prometheus/Alertmanager&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;route to Slack (warning) and PagerDuty (critical) with runbook link&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;run a game-day simulation before enabling paging&lt;/span&gt;

  &lt;span class="na"&gt;weeks_10_13&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;on-call rotation + first postmortems&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;set up weekly primary + secondary rotation in PagerDuty&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;write handoff-doc template (open incidents, budget status, changes)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;schedule blameless postmortem for every SLO breach within 5 business days&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;track action items in Jira with "reliability" label&lt;/span&gt;

&lt;span class="na"&gt;success_metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fraction of top-10 tables with a live SLI + agreed SLO + alerting = 10/10 by day &lt;/span&gt;&lt;span class="m"&gt;90&lt;/span&gt;
  &lt;span class="na"&gt;secondary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mean time to detect (MTTD) reduced from ~4 h to &amp;lt; 15 min on top-10 tables&lt;/span&gt;
  &lt;span class="na"&gt;guardrail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="s"&gt;-positive alert rate &amp;lt; 20% of total pages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Freshness SLI exporter — Python + prometheus_client
# Run every 60 s as a sidecar or scheduled task
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;prometheus_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;start_http_server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Gauge&lt;/span&gt;

&lt;span class="n"&gt;g_age_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Gauge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data_freshness_age_seconds&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;age in seconds of newest row&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;table_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.orders&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.customers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.shipments&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.payments&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.sessions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.pageviews&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.line_items&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.refunds&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.reviews&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scrape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT extract(epoch FROM (now() - MAX(ingested_at))) FROM &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;g_age_seconds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;start_http_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;host=warehouse port=5432 dbname=analytics user=slo_reader&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;autocommit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;scrape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Week&lt;/th&gt;
&lt;th&gt;Milestone&lt;/th&gt;
&lt;th&gt;Deliverable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0–2&lt;/td&gt;
&lt;td&gt;SLIs live on top 10 tables&lt;/td&gt;
&lt;td&gt;freshness + completeness gauges + dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2–6&lt;/td&gt;
&lt;td&gt;SLO targets agreed&lt;/td&gt;
&lt;td&gt;10 SLO docs signed by consumer + owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6–10&lt;/td&gt;
&lt;td&gt;Burn-rate alerts wired&lt;/td&gt;
&lt;td&gt;Prometheus rules + runbook links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10–13&lt;/td&gt;
&lt;td&gt;On-call rotation + first postmortems&lt;/td&gt;
&lt;td&gt;rotation live; first postmortem doc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day 90&lt;/td&gt;
&lt;td&gt;Success metrics reported&lt;/td&gt;
&lt;td&gt;MTTD &amp;lt; 15 min; false-page &amp;lt; 20%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the rollout, the team has 10 tables under measurable reliability contracts, a weekly on-call rotation with runbook-linked alerts, and a blameless postmortem cadence. The next 90 days extend SLI coverage to the top 50 tables and start iterating on false-positive alert reduction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;th&gt;Day 90&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tables with live SLI&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tables with agreed SLO&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean time to detect&lt;/td&gt;
&lt;td&gt;~4 h&lt;/td&gt;
&lt;td&gt;&amp;lt; 15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blameless postmortems / month&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1–3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False-positive alert rate&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&amp;lt; 20%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;SLI-first rollout&lt;/strong&gt;&lt;/strong&gt; — measurement before alerting. Instrumenting the metric first, running a dashboard for a few weeks, then adding alerts on the same metric is the SRE-canonical sequence. It gives you baseline data to set realistic SLO targets and avoids the "alerts firing before we knew normal" trap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;SLO negotiation&lt;/strong&gt;&lt;/strong&gt; — the SLO is a contract, not a target unilaterally set by the platform team. Sitting with the consumer of each dataset and agreeing the number in writing turns "we aim for 99.9%" into "the fraud team accepted 99.5% freshness with 65-minute recovery." Writing it down is what makes it enforceable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Multi-window multi-burn-rate alerts&lt;/strong&gt;&lt;/strong&gt; — the standard SRE-workbook pattern for turning "we burnt some budget in the last window" into a paging decision. Fast-burn wakes you at cliff-fall; slow-burn wakes you at creep. Both fire on the same SLI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;5-field runbook link per alert&lt;/strong&gt;&lt;/strong&gt; — no runbook, no alert. Enforcing this at the alert-config level is the strongest lever for reducing "what do I do now?" MTTR. Every alert that pages a human must link to a document that tells them what to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one platform engineer week 0-2 to build the exporter, half an engineer weeks 2-6 for SLO negotiation meetings, one engineer weeks 6-10 to wire alerts and runbooks, half an engineer weeks 10-13 to bootstrap on-call. Net ~2.5 engineer-months for a 90-day rollout that puts 10 tables under reliability contracts. Compared to a $200k/year "data observability" tool that doesn't do SLO negotiation, this is 3–4x more valuable in interviewer eyes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on data platform reliability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on pipeline observability&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. SLIs and SLOs for data pipelines — freshness, completeness, volume, quality
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The four SLIs every data platform must instrument — one per axis of user-visible reliability
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;an SLI for a data pipeline is a numeric measurement of a single user-visible reliability axis — freshness, completeness, volume, or quality — computed on the &lt;em&gt;output artifact&lt;/em&gt; (the target table, the served view, the exported file) rather than on the &lt;em&gt;process&lt;/em&gt; that produced it, and an SLO is the target percentage of measurement windows in which the SLI meets its threshold&lt;/strong&gt;. Every senior data engineer must know how to derive an SLI from a consumer contract, express it as a rolling percentage, and translate it into a monthly error budget without hesitation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq3q68i1q1q5vq0u3ohmt.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq3q68i1q1q5vq0u3ohmt.jpeg" alt="Iconographic SLI/SLO diagram — four axis dials for freshness, completeness, volume, and quality each showing a 99.9% target ring with an arrow needle inside a light diagram card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes as concrete SLIs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Freshness SLI.&lt;/strong&gt; Measurement: &lt;code&gt;now() - MAX(ingested_at)&lt;/code&gt; on the target table, or &lt;code&gt;MAX(ingested_at) - MAX(source.event_ts)&lt;/code&gt; for end-to-end lag. SLO: e.g. 99.9% of one-minute observation windows have &lt;code&gt;age &amp;lt; 65 min&lt;/code&gt; for an hourly pipeline. Freshness is the axis every dashboard user notices first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completeness SLI.&lt;/strong&gt; Measurement: target rowcount / source rowcount, computed per hour or per day (or per micro-batch for streaming). SLO: e.g. 99.5% of hours have &lt;code&gt;completeness_ratio ≥ 0.99&lt;/code&gt;. Completeness catches silent drops from schema mismatches, filter bugs, or truncated network transfers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume SLI.&lt;/strong&gt; Measurement: target rowcount vs a rolling 7-day median band. SLO: e.g. 99.5% of hours have &lt;code&gt;count ∈ [0.7 × median, 1.3 × median]&lt;/code&gt;. Volume catches duplication (2× median) and silent zero (0.1× median) failures that completeness alone misses when both source and target are wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality SLI.&lt;/strong&gt; Measurement: fraction of rows passing a battery of schema and business-rule checks (nulls in required columns, invalid enums, out-of-range values, orphan foreign keys). SLO: e.g. 99.9% of daily runs have &lt;code&gt;pass_rate ≥ 0.99&lt;/code&gt;. Quality catches "we ingested the right count but the data is garbage."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why each axis is separate.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Freshness alone is insufficient.&lt;/strong&gt; A pipeline that runs on time but writes an empty file has freshness = 0 (age of latest row = 0 seconds… because there is no latest row). Volume + completeness catch this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completeness alone is insufficient.&lt;/strong&gt; A pipeline that mirrors the source 1:1 but the source itself is broken (upstream feed cut in half) will pass completeness. Volume catches this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume alone is insufficient.&lt;/strong&gt; A pipeline whose count matches the baseline exactly but whose rows are semantically wrong (all &lt;code&gt;total_cents = 0&lt;/code&gt;) will pass volume. Quality catches this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality alone is insufficient.&lt;/strong&gt; A pipeline that passes every quality check but arrives 4 hours late fails freshness. Freshness catches this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The measurement window — the second axis of every SLI.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observation cadence.&lt;/strong&gt; How often the SLI is measured. Freshness is typically measured per minute (cheap query). Completeness is per hour. Volume per hour. Quality per daily run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLO window.&lt;/strong&gt; The period over which the percentage is computed. Standard: rolling 28 days ("SLO calculated over the trailing 28-day window").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error budget window.&lt;/strong&gt; Same as SLO window. 99.9% over 28 days = 40.32 minutes of budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert window.&lt;/strong&gt; Shorter periods used for burn-rate detection: 1 hour (fast burn), 6 hours (slow burn).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to derive an SLI target from a consumer contract.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ask the consumer.&lt;/strong&gt; "How stale is too stale?" The dashboard owner says "I check it every 5 minutes; if it's more than an hour behind, my meeting is derailed." That's your freshness target: 65 minutes (add a small buffer above the natural 60-minute cadence).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanity-check against current performance.&lt;/strong&gt; Compute the SLI over the last 28 days. If current performance is 99.7%, target 99.9% requires a 3x improvement in bad-minute count. If current is 99%, target 99.9% requires a 10x improvement — that's a project, not a target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade cost against reliability.&lt;/strong&gt; Higher SLOs cost more (extra headroom in compute, faster recovery, more on-call attention). Explicit trade-off: "we could hit 99.99% freshness by adding a hot-standby DAG, at a cost of 2x compute." The business decides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish and version the SLO.&lt;/strong&gt; Confluence doc + Git-tracked SLO catalogue. Every SLO has an owner (platform team) and a consumer (dashboard or service owner). Reviewed quarterly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on SLIs and SLOs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What's the difference between an SLI and an SLO?" — required answer: SLI is the measurement, SLO is the target.&lt;/li&gt;
&lt;li&gt;"How do you pick a freshness target?" — required answer: derive from consumer contract; sanity-check against current performance.&lt;/li&gt;
&lt;li&gt;"What's an SLA?" — required answer: SLA is the &lt;em&gt;external customer-facing&lt;/em&gt; commitment (with penalties); SLO is the &lt;em&gt;internal&lt;/em&gt; target (typically stricter).&lt;/li&gt;
&lt;li&gt;"How do you handle multiple consumers with different needs?" — required answer: pick the strictest requirement; publish that as the SLO; slower consumers benefit for free.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — freshness SLI over a Snowflake table
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical freshness SLI setup: a per-minute query that compares &lt;code&gt;MAX(ingested_at)&lt;/code&gt; against the SLO threshold, exported as a Prometheus gauge and rolled up into a 28-day SLO percentage. Walk through the pieces.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metric definition.&lt;/strong&gt; &lt;code&gt;data_freshness_meets_slo{table="analytics.orders"}&lt;/code&gt; = 1 if &lt;code&gt;now() - MAX(ingested_at) &amp;lt; 65 min&lt;/code&gt;, else 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence.&lt;/strong&gt; Every 60 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLO percentage.&lt;/strong&gt; &lt;code&gt;sum_over_time(data_freshness_meets_slo[28d]) / count_over_time(data_freshness_meets_slo[28d])&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target.&lt;/strong&gt; 99.9% (= 40.32 min budget over 28 days).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement the freshness SLI query, the Prometheus exporter, and the SLO rollup rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Target table&lt;/td&gt;
&lt;td&gt;analytics.orders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threshold&lt;/td&gt;
&lt;td&gt;65 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cadence&lt;/td&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO window&lt;/td&gt;
&lt;td&gt;28 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO target&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Freshness check query — runs every 60 s from an exporter&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="s1"&gt;'analytics.orders'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epoch&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingested_at&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;age_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingested_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'65 minutes'&lt;/span&gt;
         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;meets_slo&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Freshness exporter — psycopg2 + prometheus_client
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;prometheus_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;start_http_server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Gauge&lt;/span&gt;

&lt;span class="n"&gt;g_meets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Gauge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data_freshness_meets_slo&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1 if within threshold&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;table_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;g_age&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Gauge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data_freshness_age_seconds&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;age of newest row&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;table_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;CFG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# (table_name, threshold_seconds)
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.orders&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;65&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.customers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analytics.events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scrape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CFG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT extract(epoch FROM (now() - MAX(ingested_at))) FROM &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;g_age&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;g_meets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;start_http_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;host=warehouse dbname=analytics user=slo_reader&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;autocommit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;scrape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Prometheus recording rule — roll up to a 28-day SLO percentage&lt;/span&gt;
&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data_slo_rollups&lt;/span&gt;
  &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slo:freshness:28d&lt;/span&gt;
      &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;sum_over_time(data_freshness_meets_slo[28d])&lt;/span&gt;
          &lt;span class="s"&gt;/ count_over_time(data_freshness_meets_slo[28d])&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slo:freshness:budget_remaining_28d&lt;/span&gt;
      &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;(slo:freshness:28d - 0.999) / (1 - 0.999)&lt;/span&gt;
      &lt;span class="c1"&gt;# value &amp;gt; 0 means budget remaining; 0 means exhausted; &amp;lt; 0 means overspent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The SLI query runs on the warehouse and computes &lt;code&gt;now() - MAX(ingested_at)&lt;/code&gt; — the age of the newest row. This is a &lt;em&gt;cheap&lt;/em&gt; query when &lt;code&gt;ingested_at&lt;/code&gt; is indexed; sub-100ms on any modern warehouse. Running it every 60 seconds is negligible cost.&lt;/li&gt;
&lt;li&gt;The exporter converts the raw age into two Prometheus gauges: &lt;code&gt;data_freshness_age_seconds&lt;/code&gt; (the continuous measurement) and &lt;code&gt;data_freshness_meets_slo&lt;/code&gt; (the 0/1 binary check against the threshold). Both are labelled by table name.&lt;/li&gt;
&lt;li&gt;The recording rule &lt;code&gt;slo:freshness:28d&lt;/code&gt; averages the binary &lt;code&gt;meets_slo&lt;/code&gt; gauge over the trailing 28 days. If 99.9% of one-minute observations were "1", the SLO percentage is 0.999. Recording it as a rule (rather than computing at query time) makes dashboards and alerts cheap.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;budget_remaining_28d&lt;/code&gt; rule normalises the SLO into a "how much budget is left" number: &lt;code&gt;(current − target) / (1 − target)&lt;/code&gt;. A value of 0.5 means half the budget remains; 0 means exhausted; negative means overspent. This is the number you show in a big-bold-number Grafana panel.&lt;/li&gt;
&lt;li&gt;The 28-day window is a rolling window — new minutes enter, old minutes fall off. A team that recovers reliability today sees the budget replenish gradually over the next 28 days, not resurrect instantly. This aligns incentives correctly: no "we'll fix it Monday and pretend nothing happened."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;age_seconds&lt;/th&gt;
&lt;th&gt;meets_slo&lt;/th&gt;
&lt;th&gt;28-day SLO&lt;/th&gt;
&lt;th&gt;Budget remaining&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;analytics.orders&lt;/td&gt;
&lt;td&gt;1247&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.9994&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.customers&lt;/td&gt;
&lt;td&gt;43200&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.events&lt;/td&gt;
&lt;td&gt;923&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.9987&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every table under a freshness SLO gets two gauges (&lt;code&gt;age_seconds&lt;/code&gt;, &lt;code&gt;meets_slo&lt;/code&gt;), one recording rule (&lt;code&gt;slo:freshness:28d&lt;/code&gt;), and one big-bold-number Grafana panel showing budget remaining. This is the minimum viable freshness SLO stack.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — completeness SLI via source-target reconcile
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The completeness SLI compares source and target row counts per hour and computes a ratio. It requires read access to both source and target and a shared time-column for windowing. Walk through the pattern for the &lt;code&gt;orders&lt;/code&gt; pipeline.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metric definition.&lt;/strong&gt; &lt;code&gt;completeness_ratio{table="orders", hour="…"} = target_rowcount / source_rowcount&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLO.&lt;/strong&gt; 99.5% of hours have ratio ≥ 0.99.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence.&lt;/strong&gt; Compute at the top of each hour, once the safety window has passed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage.&lt;/strong&gt; Persist per-hour to a &lt;code&gt;slo_measurements&lt;/code&gt; table for the 28-day rolling calculation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the completeness reconcile SQL and the daily rollup that produces the SLO percentage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;production.orders (event_ts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;analytics.orders (event_ts, ingested_at)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safety window&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO threshold&lt;/td&gt;
&lt;td&gt;ratio ≥ 0.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO target&lt;/td&gt;
&lt;td&gt;99.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Completeness reconcile — computed hourly, one row per (table, hour)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;slo_measurements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sli_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value_num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meets_slo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;windows&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'2 hours'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;window_start&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_src&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;windows&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;production&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
      &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt;
     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;tgt&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_tgt&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;windows&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
      &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt;
     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="s1"&gt;'completeness'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;                             &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sli_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'analytics.orders'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tgt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_tgt&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value_num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;tgt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_tgt&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;
         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;                           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;meets_slo&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;tgt&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Rollup — the 28-day completeness SLO percentage&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meets_slo&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;slo_28d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;meets_slo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;bad_hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meets_slo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;995&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;995&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;budget_remaining_frac&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;slo_measurements&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sli_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completeness'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;window_start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'28 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The reconcile query uses &lt;code&gt;generate_series&lt;/code&gt; to enumerate the specific hour windows to check — we deliberately go 1–2 hours back so the safety window (5 min) has already passed. This prevents counting in-flight rows as missing.&lt;/li&gt;
&lt;li&gt;Left-joining source and target per hour gives us &lt;code&gt;n_src&lt;/code&gt; and &lt;code&gt;n_tgt&lt;/code&gt; even when a table is empty for the window. &lt;code&gt;NULLIF(n_src, 0)&lt;/code&gt; avoids division-by-zero when the source itself was silent.&lt;/li&gt;
&lt;li&gt;The ratio &lt;code&gt;n_tgt::numeric / n_src&lt;/code&gt; is stored in &lt;code&gt;value_num&lt;/code&gt;; the binary &lt;code&gt;meets_slo&lt;/code&gt; records whether the ratio met the 0.99 threshold. Persisting both lets you back-fill dashboards and audit historical breaches.&lt;/li&gt;
&lt;li&gt;The rollup computes &lt;code&gt;AVG(meets_slo)&lt;/code&gt; over the trailing 28 days — that's the SLO percentage. &lt;code&gt;bad_hours / total_hours&lt;/code&gt; gives you the human-readable version ("42 bad hours out of 672 = 93.75%").&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;budget_remaining_frac&lt;/code&gt; column normalises the SLO into a "how much room is left" number. Positive = budget remaining; zero = exhausted; negative = you've overspent (and if this is a paying-customer SLA, credits are owed). Publish this to the Grafana dashboard next to the freshness budget.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;table_name&lt;/th&gt;
&lt;th&gt;slo_28d&lt;/th&gt;
&lt;th&gt;bad_hours&lt;/th&gt;
&lt;th&gt;total_hours&lt;/th&gt;
&lt;th&gt;budget_remaining&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;analytics.orders&lt;/td&gt;
&lt;td&gt;0.99702&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;672&lt;/td&gt;
&lt;td&gt;40.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.customers&lt;/td&gt;
&lt;td&gt;0.99851&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;672&lt;/td&gt;
&lt;td&gt;70.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.events&lt;/td&gt;
&lt;td&gt;0.99405&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;672&lt;/td&gt;
&lt;td&gt;-19.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For every completeness SLO, persist the per-window measurement to a &lt;code&gt;slo_measurements&lt;/code&gt; table — never recompute the SLO from raw source/target counts at query time. This gives you an auditable, backfill-tolerant, dashboard-cheap SLO history.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — quality SLI via dbt tests / Great Expectations
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The quality SLI is the fraction of rows passing a battery of schema and business-rule checks. Modern data teams express these as dbt tests, Great Expectations expectations, or SQL data-diff assertions. Walk through wiring dbt tests to a quality SLO.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test suite.&lt;/strong&gt; &lt;code&gt;unique(order_id)&lt;/code&gt;, &lt;code&gt;not_null(order_id, customer_id, total_cents, status)&lt;/code&gt;, &lt;code&gt;accepted_values(status)&lt;/code&gt;, &lt;code&gt;relationships(customer_id) → customers.id&lt;/code&gt;, &lt;code&gt;range(total_cents, 0, 10_000_000)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLI definition.&lt;/strong&gt; &lt;code&gt;quality_pass_rate = rows_passing_all / total_rows&lt;/code&gt;, computed per daily dbt run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLO.&lt;/strong&gt; 99.9% of daily runs have &lt;code&gt;pass_rate ≥ 0.99&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Configure the dbt tests, compute the quality pass rate, and roll it up to a monthly SLO.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Test framework&lt;/td&gt;
&lt;td&gt;dbt tests (Postgres/Snowflake adapter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test batch&lt;/td&gt;
&lt;td&gt;daily post-build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLI pass threshold&lt;/td&gt;
&lt;td&gt;≥ 0.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO target&lt;/td&gt;
&lt;td&gt;99.9% of daily runs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dbt schema.yml — quality tests on the orders model&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
    &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_id&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;relationships&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('customers')&lt;/span&gt;
              &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;id&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_cents&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;dbt_utils.accepted_range&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;min_value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
              &lt;span class="na"&gt;max_value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;accepted_values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shipped'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;delivered'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cancelled'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;refunded'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Post-run macro — compute pass rate + insert into slo_measurements&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;macro&lt;/span&gt; &lt;span class="n"&gt;record_quality_sli&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;slo_measurements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sli_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value_num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meets_slo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pass'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;row_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;dbt_run_results&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;run_started_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;model_name&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'quality'&lt;/span&gt;                                 &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sli_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s1"&gt;'analytics.'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;model_name&lt;/span&gt;                &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;window_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value_num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;
                &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;meets_slo&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endmacro&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Monthly rollup&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meets_slo&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;quality_slo_30d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;meets_slo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;bad_days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_days&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;slo_measurements&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sli_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'quality'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;window_start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The dbt &lt;code&gt;schema.yml&lt;/code&gt; declares the tests: uniqueness, null-ness, range, accepted values, and referential integrity. Each is a first-class dbt test that emits a &lt;code&gt;pass&lt;/code&gt; or &lt;code&gt;fail&lt;/code&gt; result with a row count of failing records.&lt;/li&gt;
&lt;li&gt;The post-run macro (invoked via &lt;code&gt;on-run-end&lt;/code&gt; in &lt;code&gt;dbt_project.yml&lt;/code&gt;) aggregates the results into a per-model pass rate. Passed rows / total rows, computed per model, per day. This becomes the quality SLI.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;meets_slo&lt;/code&gt; binary records whether the day's pass rate cleared 99%. Persisting both the ratio (&lt;code&gt;value_num&lt;/code&gt;) and the binary (&lt;code&gt;meets_slo&lt;/code&gt;) lets you show both trend charts and SLO percentages.&lt;/li&gt;
&lt;li&gt;The monthly rollup — same shape as freshness and completeness — computes &lt;code&gt;AVG(meets_slo)&lt;/code&gt; over the trailing 30 days. A 99.9% SLO with 30 daily runs allows exactly 0.03 bad days per month — effectively "never miss." A 99% SLO allows ~1 bad day per quarter.&lt;/li&gt;
&lt;li&gt;The reason quality SLOs are typically stricter (99.9% vs freshness's 99.9% of &lt;em&gt;minutes&lt;/em&gt;) is that a daily run has far fewer observation windows than a per-minute freshness check. The same SLO percentage translates to very different bad-window counts based on cadence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;table_name&lt;/th&gt;
&lt;th&gt;quality_slo_30d&lt;/th&gt;
&lt;th&gt;bad_days&lt;/th&gt;
&lt;th&gt;total_days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;analytics.orders&lt;/td&gt;
&lt;td&gt;1.00000&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.customers&lt;/td&gt;
&lt;td&gt;0.96667&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.events&lt;/td&gt;
&lt;td&gt;1.00000&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For every table with a dbt or Great Expectations test suite, wire the pass-rate metric into the same &lt;code&gt;slo_measurements&lt;/code&gt; table as freshness and completeness. Unified table + unified rollup rule = one dashboard for all four SLIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on SLIs and SLOs
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You inherit a 500-table warehouse where 'monitoring' means Airflow email alerts on task failure. Design the SLI/SLO framework — how you'd pick the top tables to instrument, what SLIs you'd add per table, how you'd store measurements, and how you'd derive per-consumer SLO targets."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a top-N table-prioritisation approach with a unified slo_measurements table and consumer-driven SLO derivation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Prioritise tables by downstream query volume (warehouse audit log)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;slo_candidate_tables&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distinct_users_7d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;queries_7d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;snowflake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;access_history&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;query_start_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- The top 20 tables cover ~80% of downstream reads (Pareto)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;slo_candidate_tables&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Unified SLO measurement table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;slo_measurements&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sli_name&lt;/span&gt;         &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- 'freshness' | 'completeness' | 'volume' | 'quality'&lt;/span&gt;
    &lt;span class="k"&gt;table_name&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;window_start&lt;/span&gt;     &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value_num&lt;/span&gt;        &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;-- raw measurement&lt;/span&gt;
    &lt;span class="n"&gt;meets_slo&lt;/span&gt;        &lt;span class="nb"&gt;SMALLINT&lt;/span&gt;    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- 0 or 1&lt;/span&gt;
    &lt;span class="n"&gt;recorded_at&lt;/span&gt;      &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sli_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_slo_meas_rollup&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;slo_measurements&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sli_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_start&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. SLO catalogue — one row per (table, sli) with the agreed target&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;slo_catalogue&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;table_name&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sli_name&lt;/span&gt;         &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;slo_target&lt;/span&gt;       &lt;span class="nb"&gt;NUMERIC&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- e.g. 0.999&lt;/span&gt;
    &lt;span class="n"&gt;consumer_team&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- who signed the SLO&lt;/span&gt;
    &lt;span class="n"&gt;threshold_json&lt;/span&gt;   &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- SLI-specific parameters&lt;/span&gt;
    &lt;span class="n"&gt;approved_at&lt;/span&gt;      &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;approved_by&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sli_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 4. SLO rollup job — runs hourly; recomputes 28-day percentages
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rollup_slos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT c.table_name, c.sli_name, c.slo_target,
                   AVG(m.meets_slo)::numeric(6, 5) AS current_slo_28d
            FROM   slo_catalogue c
            LEFT JOIN slo_measurements m
              ON m.table_name = c.table_name
             AND m.sli_name   = c.sli_name
             AND m.window_start &amp;gt;= now() - INTERVAL &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;28 days&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
            GROUP BY c.table_name, c.sli_name, c.slo_target
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;budget_remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="c1"&gt;# Write to Prometheus pushgateway (or expose as Grafana table)
&lt;/span&gt;        &lt;span class="nf"&gt;push_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;slo_current_28d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sli&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sli&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;push_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;slo_budget_remaining&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget_remaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sli&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sli&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Ticket if any SLO's budget has dropped below 20%
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;budget_remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;open_ticket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLO warning: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sli&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;budget_remaining&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prioritisation&lt;/td&gt;
&lt;td&gt;top-20 tables by 7-day query count&lt;/td&gt;
&lt;td&gt;Pareto — 20 tables = 80% of consumption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unified table&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;slo_measurements&lt;/code&gt; with (sli_name, table_name, window) PK&lt;/td&gt;
&lt;td&gt;one query for all four SLIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO catalogue&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;slo_catalogue&lt;/code&gt; with per-(table, sli) target + consumer&lt;/td&gt;
&lt;td&gt;contract, not target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollup cadence&lt;/td&gt;
&lt;td&gt;hourly&lt;/td&gt;
&lt;td&gt;recompute 28-day percentages cheaply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ticket trigger&lt;/td&gt;
&lt;td&gt;budget remaining &amp;lt; 20%&lt;/td&gt;
&lt;td&gt;early warning before breach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alert wiring&lt;/td&gt;
&lt;td&gt;separate — burn-rate on the per-SLI meets_slo gauge&lt;/td&gt;
&lt;td&gt;see next H2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the framework is in place, adding an SLI for a new table is a two-step operation: add a row to &lt;code&gt;slo_catalogue&lt;/code&gt; (with the agreed target and consumer), and wire the SLI-specific measurement into &lt;code&gt;slo_measurements&lt;/code&gt;. All dashboards, rollups, and burn-rate alerts pick up the new SLI automatically because they iterate the catalogue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tables covered by any SLI&lt;/td&gt;
&lt;td&gt;top 20 (rising to 100 by Q2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLIs per table (target)&lt;/td&gt;
&lt;td&gt;4 (freshness, completeness, volume, quality)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollup query cost&lt;/td&gt;
&lt;td&gt;&amp;lt; 500 ms per hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Budget-warning ticket latency&lt;/td&gt;
&lt;td&gt;~1 hour of degradation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO catalogue reviewed&lt;/td&gt;
&lt;td&gt;quarterly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Top-N prioritisation&lt;/strong&gt;&lt;/strong&gt; — you cannot instrument 500 tables on day one. Ranking by downstream query volume (from the warehouse's own audit log) gives you the 20 tables that carry 80% of the reliability impact. Start there; extend as budget allows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Unified slo_measurements table&lt;/strong&gt;&lt;/strong&gt; — one wide-format table with &lt;code&gt;sli_name&lt;/code&gt; as a column beats one narrow-format table per SLI. It means one dashboard, one rollup query, one alerting scheme covers all four axes. Adding a new SLI type is a data change, not a schema change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;slo_catalogue as a contract&lt;/strong&gt;&lt;/strong&gt; — the catalogue is a Git-tracked, code-reviewed source of truth: which tables have which SLOs, what the targets are, who agreed to them, and when. An SLO that doesn't appear in the catalogue does not exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Consumer-signed targets&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;consumer_team&lt;/code&gt; and &lt;code&gt;approved_by&lt;/code&gt; columns enforce the contract-not-target discipline. The platform team cannot unilaterally set a 99.9% SLO on &lt;code&gt;orders&lt;/code&gt;; the fraud model owner (or whoever depends on it) must agree in writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one 20-column &lt;code&gt;slo_measurements&lt;/code&gt; row per (table, sli, window) — for 100 tables × 4 SLIs × per-hour cadence over 28 days, that's ~270k rows/month. Sub-gigabyte at rest, sub-second at query. The Prometheus/Grafana stack costs ~$50/month at this scale. Compared to $200k/year "data observability" tools that don't do SLO negotiation, this is 200x cheaper and 3x more valuable in interview answers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Data validation&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — data-validation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Data validation and SLI problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;
&lt;strong&gt;SQL reconcile and freshness problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Error budgets and burn-rate alerts
&lt;/h2&gt;
&lt;h3&gt;
  
  
  99.9% freshness = 43 minutes per month — spend it deliberately
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;an error budget is the operational contract that turns a percentage SLO into a concrete quantity of "bad time" the team is permitted to incur before feature releases must halt — 99.9% over a 30-day month is exactly 43 minutes and 12 seconds, and every SRE-style alerting decision (page now, ticket later, ignore) is grounded in how fast the team is spending against that budget, not on whether an individual event happened&lt;/strong&gt;. Every senior data engineer must be able to derive the budget from the SLO percentage in their head, describe the multi-window multi-burn-rate alerting pattern, and defend the policy that halts releases when the budget is exhausted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyx5cq5ga0c8bmhtzksaq.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyx5cq5ga0c8bmhtzksaq.jpeg" alt="Iconographic error-budget diagram — a burn-rate curve rising steeply on the left, a horizontal SLO threshold line, and a fuel-tank glyph on the right showing the monthly error budget draining." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The budget math every senior DE knows without hesitation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;99% over 30 days&lt;/strong&gt; = 30 × 24 × 60 × 0.01 = &lt;strong&gt;432 minutes&lt;/strong&gt; ≈ 7.2 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;99.5%&lt;/strong&gt; = &lt;strong&gt;216 minutes&lt;/strong&gt; ≈ 3.6 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;99.9%&lt;/strong&gt; = &lt;strong&gt;43.2 minutes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;99.95%&lt;/strong&gt; = &lt;strong&gt;21.6 minutes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;99.99%&lt;/strong&gt; = &lt;strong&gt;4.32 minutes&lt;/strong&gt; — "four nines" is aggressive for a data pipeline; usually reserved for the streaming layer where the source-of-truth is upstream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Burn rate — the derivative of the budget.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Definition.&lt;/strong&gt; Burn rate = the rate at which the SLI is spending budget divided by the sustainable rate that would exhaust the budget exactly at month-end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sustainable = 1x.&lt;/strong&gt; If you spend budget at 1x, you'll use it all over 30 days — you'll hit exactly 99.9%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;14x = disaster.&lt;/strong&gt; Spending at 14x means the whole month's budget would be gone in ~2 days. This is the fast-burn threshold that pages immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6x = concerning.&lt;/strong&gt; Spending at 6x means the budget is gone in ~5 days. This is the slow-burn threshold that tickets the team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1x = normal.&lt;/strong&gt; Spending at 1x is exactly on-plan — no alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The multi-window multi-burn-rate alerting pattern (Google SRE workbook).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast-burn (short window).&lt;/strong&gt; Compute burn rate over a 1-hour window. If it exceeds 14x, page immediately. Detects cliffs quickly with minimal delay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow-burn (long window).&lt;/strong&gt; Compute burn rate over a 6-hour window. If it exceeds 6x, ticket the team. Detects creeping degradation without false pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both windows use the same SLI.&lt;/strong&gt; The alert doesn't fire on an event; it fires on the rate of budget consumption over a rolling window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why multi-window.&lt;/strong&gt; A single 5-minute burn spike shouldn't page (transient); a 1-hour sustained burn should. A 6-hour slow burn shouldn't page but must be seen (the ticket).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why not alert on every SLI dip?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alert fatigue.&lt;/strong&gt; A hair-trigger alert on any missed minute produces dozens of pages per week, each of which the on-call resolves by acknowledging and going back to sleep. The signal-to-noise ratio drops to zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False-positive cost.&lt;/strong&gt; Every false page has a human cost (interrupted sleep, distraction, morale). The Google SRE book estimates false-page cost at ~$100 per event in engineering productivity; a team with 10 false pages per week costs the org $50k/year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missed-signal cost.&lt;/strong&gt; Under-alerting means the team learns about incidents from the CEO, not the pager. Balance is via the multi-window burn-rate mechanism — page when the &lt;em&gt;budget consumption rate&lt;/em&gt; is genuinely alarming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The release-halt policy.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When the budget is exhausted.&lt;/strong&gt; No feature releases against that pipeline until the budget replenishes at month-start. Only reliability fixes ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why the halt is non-negotiable.&lt;/strong&gt; The alternative — "release anyway" — invalidates every future SLO promise. Consumers cannot trust "99.9% freshness" if the platform team ships changes when the budget is empty and freshness is already at 98%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How the halt is enforced.&lt;/strong&gt; CI check that reads the SLO status; blocks the deploy if budget &amp;lt; 0. Emergency override with director-level approval only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What "release" means.&lt;/strong&gt; Any change to the pipeline code, DAG DSL, transformation SQL, source-schema contract, or infrastructure. Not just "new feature" — includes "innocuous refactor" too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on error budgets.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How many minutes of downtime does 99.9% allow per month?" — required answer: 43.&lt;/li&gt;
&lt;li&gt;"What's a burn-rate alert?" — required answer: multi-window multi-rate on the SLI, from the SRE workbook.&lt;/li&gt;
&lt;li&gt;"Why don't you page on every SLI dip?" — required answer: alert fatigue; false-page cost.&lt;/li&gt;
&lt;li&gt;"What happens when the error budget is exhausted?" — required answer: release halt; only reliability fixes ship.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — freshness burn-rate PromQL against a freshness metric
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical multi-window multi-burn-rate alert setup: two rules, one fast (1-hour window, 14x threshold) and one slow (6-hour window, 6x threshold). Both reference the same &lt;code&gt;data_freshness_meets_slo&lt;/code&gt; gauge. Walk through the PromQL.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SLI gauge.&lt;/strong&gt; &lt;code&gt;data_freshness_meets_slo{table="analytics.orders"}&lt;/code&gt; — 0/1 per minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target.&lt;/strong&gt; 99.9% (bad-minute rate = 0.001).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast alert.&lt;/strong&gt; 1-hour window, 14x burn = 1.4% bad in the hour = page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow alert.&lt;/strong&gt; 6-hour window, 6x burn = 0.6% bad over the 6 hours = ticket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the Prometheus alert rules for the freshness SLI on &lt;code&gt;analytics.orders&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data_freshness_meets_slo&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO target&lt;/td&gt;
&lt;td&gt;0.999&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast window&lt;/td&gt;
&lt;td&gt;1 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow window&lt;/td&gt;
&lt;td&gt;6 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast burn threshold&lt;/td&gt;
&lt;td&gt;14× normal = ≥ 1.4% bad&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow burn threshold&lt;/td&gt;
&lt;td&gt;6× normal = ≥ 0.6% bad&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Prometheus alerting rules — multi-window multi-burn-rate for freshness&lt;/span&gt;
&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;freshness_burn_rate&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Fast burn — page&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FreshnessBudgetFastBurn&lt;/span&gt;
      &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;(&lt;/span&gt;
          &lt;span class="s"&gt;1 - avg_over_time(data_freshness_meets_slo{table="analytics.orders"}[1h])&lt;/span&gt;
        &lt;span class="s"&gt;) &amp;gt; 0.014       # 14 × 0.001&lt;/span&gt;
        &lt;span class="s"&gt;and&lt;/span&gt;
        &lt;span class="s"&gt;(&lt;/span&gt;
          &lt;span class="s"&gt;1 - avg_over_time(data_freshness_meets_slo{table="analytics.orders"}[5m])&lt;/span&gt;
        &lt;span class="s"&gt;) &amp;gt; 0.014&lt;/span&gt;
      &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2m&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
        &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analytics.orders&lt;/span&gt;
        &lt;span class="na"&gt;sli&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;freshness&lt;/span&gt;
      &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Freshness&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;burning&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;14×&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;normal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on-call"&lt;/span&gt;
        &lt;span class="na"&gt;runbook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://runbooks.internal/data/freshness-orders"&lt;/span&gt;
        &lt;span class="na"&gt;current_burn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;humanizePercentage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;

    &lt;span class="c1"&gt;# Slow burn — ticket&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FreshnessBudgetSlowBurn&lt;/span&gt;
      &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;(&lt;/span&gt;
          &lt;span class="s"&gt;1 - avg_over_time(data_freshness_meets_slo{table="analytics.orders"}[6h])&lt;/span&gt;
        &lt;span class="s"&gt;) &amp;gt; 0.006       # 6 × 0.001&lt;/span&gt;
        &lt;span class="s"&gt;and&lt;/span&gt;
        &lt;span class="s"&gt;(&lt;/span&gt;
          &lt;span class="s"&gt;1 - avg_over_time(data_freshness_meets_slo{table="analytics.orders"}[30m])&lt;/span&gt;
        &lt;span class="s"&gt;) &amp;gt; 0.006&lt;/span&gt;
      &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15m&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
        &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analytics.orders&lt;/span&gt;
        &lt;span class="na"&gt;sli&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;freshness&lt;/span&gt;
      &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Freshness&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;burning&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6×&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;normal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6h&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ticket"&lt;/span&gt;
        &lt;span class="na"&gt;runbook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://runbooks.internal/data/freshness-orders"&lt;/span&gt;
        &lt;span class="na"&gt;current_burn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;humanizePercentage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;avg_over_time(...meets_slo[1h])&lt;/code&gt; computes the fraction of minutes in the last hour that met the SLO. Subtracting from 1 gives the &lt;em&gt;bad&lt;/em&gt; fraction. If bad fraction &amp;gt; 1.4%, we're spending at 14x the sustainable rate — the fast-burn threshold.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;and&lt;/code&gt; clause with a 5-minute window is the "double window" guard: the burn rate must be high in &lt;em&gt;both&lt;/em&gt; the 1-hour and the 5-minute view. This prevents an old spike (already resolved) from re-firing the alert as it slowly ages out of the 1-hour window.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;for: 2m&lt;/code&gt; is the Prometheus stability duration — the alert must be true for 2 minutes before firing. This absorbs metric-scrape jitter and prevents flapping. Combined with the burn-rate math, the fast-burn alert fires ~7 minutes after a cliff-fall begins.&lt;/li&gt;
&lt;li&gt;The slow-burn rule mirrors the structure but with a 6-hour window and 6x threshold. &lt;code&gt;for: 15m&lt;/code&gt; is longer because slow burns are inherently slower-changing; the extra stability window prevents ticket noise.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;annotations.runbook&lt;/code&gt; link is &lt;em&gt;mandatory&lt;/em&gt; — no runbook link = alert doesn't ship. The on-call reads the runbook the moment the page arrives; without it, MTTR balloons while they figure out what to do.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Burn rate&lt;/th&gt;
&lt;th&gt;Time-to-fire&lt;/th&gt;
&lt;th&gt;Alert path&lt;/th&gt;
&lt;th&gt;Human action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;14× sustained 7 min&lt;/td&gt;
&lt;td&gt;~7 min after cliff&lt;/td&gt;
&lt;td&gt;fast-burn → PagerDuty → phone&lt;/td&gt;
&lt;td&gt;page; on-call runs runbook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6× sustained 15 min in 6h window&lt;/td&gt;
&lt;td&gt;~15 min into slow burn&lt;/td&gt;
&lt;td&gt;slow-burn → PagerDuty → Slack&lt;/td&gt;
&lt;td&gt;ticket; team triages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 6×&lt;/td&gt;
&lt;td&gt;never&lt;/td&gt;
&lt;td&gt;no alert&lt;/td&gt;
&lt;td&gt;budget dashboard shows draw&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every freshness (and completeness, volume, quality) SLO ships with &lt;em&gt;two&lt;/em&gt; burn-rate alerts: one fast, one slow, sharing the same SLI. Skipping either creates a gap — fast-only misses creeping degradation; slow-only misses cliff-falls.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — monthly budget accounting spreadsheet template
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The team needs a monthly view of every SLO's budget consumption to hold quarterly reviews and decide next-quarter SLO adjustments. Build the accounting template.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Columns.&lt;/strong&gt; Table, SLI, Target, Actual, Budget spent (min), Budget remaining (min), Status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence.&lt;/strong&gt; Refreshed daily; reviewed weekly at the platform team standup; formally reviewed monthly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source.&lt;/strong&gt; The &lt;code&gt;slo_measurements&lt;/code&gt; + &lt;code&gt;slo_catalogue&lt;/code&gt; tables from H2 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the monthly budget-accounting SQL that produces one row per (table, SLI) with budget spent and remaining.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source tables&lt;/td&gt;
&lt;td&gt;slo_measurements, slo_catalogue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Window&lt;/td&gt;
&lt;td&gt;rolling 30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cadence&lt;/td&gt;
&lt;td&gt;daily refresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output columns&lt;/td&gt;
&lt;td&gt;table, sli, target, actual, spent_min, remaining_min, status&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Monthly SLO budget accounting&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sli_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slo_target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meets_slo&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;actual_slo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;observation_windows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meets_slo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;bad_windows&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;slo_catalogue&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;slo_measurements&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
      &lt;span class="k"&gt;ON&lt;/span&gt;   &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
     &lt;span class="k"&gt;AND&lt;/span&gt;   &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sli_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sli_name&lt;/span&gt;
     &lt;span class="k"&gt;AND&lt;/span&gt;   &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sli_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slo_target&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;budget&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sli_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;slo_target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;actual_slo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;observation_windows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bad_windows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;-- convert to minutes assuming per-minute observation windows&lt;/span&gt;
        &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;slo_target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;observation_windows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;budget_min_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bad_windows&lt;/span&gt;                                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;budget_min_spent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;GREATEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;slo_target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;observation_windows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;bad_windows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;budget_min_remaining&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sli_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;slo_target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;actual_slo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;budget_min_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;budget_min_spent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;budget_min_remaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;budget_min_remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;actual_slo&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;slo_target&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'EXHAUSTED — halt releases'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;budget_min_remaining&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;budget_min_total&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'CRITICAL — 20% left'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;budget_min_remaining&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;budget_min_total&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'WARNING — 50% left'&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt;                                                            &lt;span class="s1"&gt;'HEALTHY'&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;budget_min_remaining&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;budget_min_total&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;observed&lt;/code&gt; CTE joins the catalogue against the measurements over the last 30 days, aggregating to per-(table, SLI) actual SLO percentage, count of observation windows, and count of bad windows.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;budget&lt;/code&gt; CTE computes three quantities: &lt;code&gt;budget_min_total&lt;/code&gt; (total minutes of budget the SLO allows), &lt;code&gt;budget_min_spent&lt;/code&gt; (already-consumed bad minutes), &lt;code&gt;budget_min_remaining&lt;/code&gt; (what's left, floored at 0). The math is: total = &lt;code&gt;(1 − target) × windows&lt;/code&gt;; if target is 0.999 and windows is 43200 minutes/month, total = 43.2 minutes.&lt;/li&gt;
&lt;li&gt;The status column translates numeric budget-remaining into an operational label. &lt;code&gt;EXHAUSTED&lt;/code&gt; triggers the release-halt; &lt;code&gt;CRITICAL&lt;/code&gt; and &lt;code&gt;WARNING&lt;/code&gt; are ticket-level heads-ups.&lt;/li&gt;
&lt;li&gt;Ordering by "% budget remaining ASC" puts the most at-risk SLOs at the top of the table — the review meeting focuses on those first.&lt;/li&gt;
&lt;li&gt;The monthly review meeting walks this table row by row: for each &lt;code&gt;CRITICAL&lt;/code&gt; or &lt;code&gt;EXHAUSTED&lt;/code&gt; row, discuss what happened, what action was taken, whether the SLO target is still reasonable. For each &lt;code&gt;HEALTHY&lt;/code&gt; row, discuss whether the target could be tightened next quarter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;table&lt;/th&gt;
&lt;th&gt;sli&lt;/th&gt;
&lt;th&gt;target&lt;/th&gt;
&lt;th&gt;actual&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;th&gt;spent&lt;/th&gt;
&lt;th&gt;remaining&lt;/th&gt;
&lt;th&gt;status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;analytics.orders&lt;/td&gt;
&lt;td&gt;freshness&lt;/td&gt;
&lt;td&gt;0.999&lt;/td&gt;
&lt;td&gt;0.9993&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;CRITICAL — 20% left&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.events&lt;/td&gt;
&lt;td&gt;freshness&lt;/td&gt;
&lt;td&gt;0.999&lt;/td&gt;
&lt;td&gt;0.9987&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;EXHAUSTED — halt releases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.customers&lt;/td&gt;
&lt;td&gt;freshness&lt;/td&gt;
&lt;td&gt;0.999&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;HEALTHY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.orders&lt;/td&gt;
&lt;td&gt;completeness&lt;/td&gt;
&lt;td&gt;0.995&lt;/td&gt;
&lt;td&gt;0.9970&lt;/td&gt;
&lt;td&gt;216&lt;/td&gt;
&lt;td&gt;130&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;td&gt;WARNING — 50% left&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The monthly budget accounting table is the single most useful reliability artifact for a senior data-platform team. Review it every Monday standup; walk it row-by-row every month-end; use it to decide whether to tighten or loosen each SLO next quarter.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — release-halt policy enforced in CI
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; When an SLO's budget is exhausted, releases against the affected pipeline must halt. The most effective enforcement is a CI check that reads the SLO status and fails the deploy if any relevant SLO is &lt;code&gt;EXHAUSTED&lt;/code&gt;. Walk through the check.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check inputs.&lt;/strong&gt; Which tables the PR touches (from &lt;code&gt;git diff&lt;/code&gt; of dbt models / DAG code), current SLO status of each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail condition.&lt;/strong&gt; Any touched table has an exhausted budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emergency override.&lt;/strong&gt; A director-level approver adds the &lt;code&gt;budget-override&lt;/code&gt; label to the PR; the check is skipped for that PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability fixes exempt.&lt;/strong&gt; PRs labelled &lt;code&gt;reliability-fix&lt;/code&gt; are exempt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the CI check script that halts a release when any relevant SLO is exhausted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;PR to main branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data source&lt;/td&gt;
&lt;td&gt;slo_measurements view or Prometheus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail condition&lt;/td&gt;
&lt;td&gt;budget_min_remaining = 0 for touched tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Override&lt;/td&gt;
&lt;td&gt;PR label &lt;code&gt;budget-override&lt;/code&gt; (director sign-off)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exempt&lt;/td&gt;
&lt;td&gt;PR label &lt;code&gt;reliability-fix&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# release_halt_check.py — runs in CI
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;

&lt;span class="n"&gt;DB_DSN&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SLO_DB_DSN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;PR_LABELS&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PR_LABELS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Exempt labels
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reliability-fix&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PR_LABELS&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;budget-override&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PR_LABELS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Skipping SLO check due to label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Identify touched tables from the PR diff
&lt;/span&gt;&lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;diff&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--name-only&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;main...HEAD&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;touched_tables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# dbt models under models/analytics/orders.sql -&amp;gt; analytics.orders
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;models/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;models/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.sql&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;touched_tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Query current SLO status for those tables
&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DB_DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT table_name, sli_name, budget_min_remaining, status
        FROM   v_slo_budget_status
        WHERE  table_name = ANY(%s)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;touched_tables&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Fail if any SLO is exhausted
&lt;/span&gt;&lt;span class="n"&gt;exhausted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;EXHAUSTED — halt releases&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exhausted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SLO release halt triggered:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exhausted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; · &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; · remaining=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; min · &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Add label `reliability-fix` to ship a reliability fix,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;or `budget-override` (director sign-off) for an emergency override.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SLO check passed — all touched tables within budget.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy.yml — invoke the check&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy pipeline changes&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;slo-check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;0&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SLO release-halt gate&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SLO_DB_DSN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SLO_DB_DSN }}&lt;/span&gt;
          &lt;span class="na"&gt;PR_LABELS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;${{ join(github.event.pull_request.labels.*.name, ',') }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3 release_halt_check.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The CI check first inspects PR labels — &lt;code&gt;reliability-fix&lt;/code&gt; and &lt;code&gt;budget-override&lt;/code&gt; are exempt. This is the escape hatch for legitimate reliability work and for director-approved emergencies. The label check is the &lt;em&gt;first&lt;/em&gt; gate because the queries below are unnecessary if we're exempt.&lt;/li&gt;
&lt;li&gt;The script parses the git diff to extract which tables the PR touches. dbt convention is &lt;code&gt;models/&amp;lt;schema&amp;gt;/&amp;lt;table&amp;gt;.sql&lt;/code&gt; → &lt;code&gt;&amp;lt;schema&amp;gt;.&amp;lt;table&amp;gt;&lt;/code&gt;. Extend for other frameworks (Airflow DAGs → touched table names via a &lt;code&gt;dag_to_tables.yaml&lt;/code&gt; mapping).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;v_slo_budget_status&lt;/code&gt; view (built on top of the earlier budget-accounting query) returns the current status per SLO. Only the SLOs on touched tables are relevant to this PR.&lt;/li&gt;
&lt;li&gt;Any &lt;code&gt;EXHAUSTED&lt;/code&gt; SLO fails the check. The error message names the exact SLO(s) and points to the two override labels. This is the &lt;em&gt;contract&lt;/em&gt; the release-halt policy makes with engineers — no surprises, always a documented path forward.&lt;/li&gt;
&lt;li&gt;The workflow file wires the check into every PR. GitHub required-checks makes this gate mandatory for merge; only admin override can bypass. This is the mechanism by which the SLO becomes an operational constraint, not a decorative percentage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PR touches&lt;/th&gt;
&lt;th&gt;Any SLO exhausted?&lt;/th&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;CI result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;analytics.orders (freshness EXHAUSTED)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;fails; blocks merge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.orders (freshness EXHAUSTED)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;reliability-fix&lt;/td&gt;
&lt;td&gt;passes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.orders (freshness EXHAUSTED)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;budget-override&lt;/td&gt;
&lt;td&gt;passes; requires director&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analytics.customers (all HEALTHY)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;passes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The release-halt policy without CI enforcement is a suggestion. With CI enforcement — required check + labelled exemptions + audit log of overrides — it becomes the operational reality that keeps SLO promises credible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on error budgets
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You have a freshness SLO of 99.9% on the &lt;code&gt;orders&lt;/code&gt; warehouse table. Halfway through the month, an upstream schema change caused a 6-hour outage, burning most of the monthly budget. What alerts should have fired earlier, what's the release policy for the rest of the month, and how do you use this incident to negotiate a saner SLO next quarter?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using multi-window burn-rate alerts, immediate release-halt, and a postmortem-driven SLO renegotiation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. Alerts that should have fired (see H2 3 above) — verify these are wired&lt;/span&gt;
&lt;span class="na"&gt;alerts_that_should_fire&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fast_burn_1h&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;14x normal&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PagerDuty (page)&lt;/span&gt;
    &lt;span class="na"&gt;expected_fire_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~15 min into outage&lt;/span&gt;

  &lt;span class="na"&gt;slow_burn_6h&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;6h&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;6x normal&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Slack + Jira ticket&lt;/span&gt;
    &lt;span class="na"&gt;expected_fire_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~1 h into outage&lt;/span&gt;

  &lt;span class="na"&gt;budget_low_warning&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;budget_min_remaining &amp;lt; 20% of total&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ticket to platform-team lead&lt;/span&gt;
    &lt;span class="na"&gt;expected_fire_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;on next hourly rollup after threshold crossed&lt;/span&gt;

  &lt;span class="na"&gt;budget_exhausted&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;budget_min_remaining = &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI release-halt engaged; email leadership&lt;/span&gt;
    &lt;span class="na"&gt;expected_fire_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;on next hourly rollup after breach&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. Post-incident policy runner — activates the release halt automatically
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enforce_release_halt&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SLO_DB_DSN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT table_name, sli_name, budget_min_remaining
            FROM   v_slo_budget_status
            WHERE  budget_min_remaining = 0
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;exhausted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;exhausted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="c1"&gt;# Update the shared release-gate flag consumed by CI
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exhausted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                INSERT INTO release_gate(table_name, sli_name, halted_at, reason)
                VALUES (%s, %s, now(), &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;budget_exhausted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
                ON CONFLICT (table_name, sli_name) DO NOTHING
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sli&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Notify leadership + on-call
&lt;/span&gt;    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Release halt engaged: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;·&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exhausted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SLACK_WEBHOOK&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;enforce_release_halt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- 3. Postmortem-driven SLO renegotiation template --&amp;gt;&lt;/span&gt;
&lt;span class="gh"&gt;# Incident: 6h freshness breach on analytics.orders — 2026-07-14&lt;/span&gt;

&lt;span class="gu"&gt;## SLO status this month&lt;/span&gt;
| SLI | Target | Actual (day 15) | Budget spent | Remaining |
|---|---|---|---|---|
| freshness | 99.9% | 98.7% | 388 min | 0 min (overspent by 345 min) |

&lt;span class="gu"&gt;## Root policy question&lt;/span&gt;
The SLO was set at 99.9% based on the dashboard-owner's stated tolerance
(~1h/month). Actual pipeline reliability over the last 90 days averaged
&lt;span class="p"&gt;99.&lt;/span&gt;85% — the target was already tighter than achievable.

&lt;span class="gu"&gt;## Recommendation&lt;/span&gt;
Renegotiate to 99.5% freshness (216 min/month = 3.6 hours), which:
  • Reflects observed pipeline reliability
  • Still constrains bad hours to &amp;lt; 4/month
  • Allows the team to invest in one reliability project per quarter
    rather than burning weekly cycles on false-alert cleanup

&lt;span class="gu"&gt;## Investments required to sustain 99.9%&lt;/span&gt;
  • Hot-standby DAG on backup schedule (~2 engineer-weeks build, +25% compute)
  • Automatic upstream-schema-drift detection (~1 sprint)
  • Recommend: only invest if executive requires 99.9%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alerts fired&lt;/td&gt;
&lt;td&gt;fast + slow + budget-low&lt;/td&gt;
&lt;td&gt;multi-window catches both cliff and creep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release halt&lt;/td&gt;
&lt;td&gt;automatic on budget = 0&lt;/td&gt;
&lt;td&gt;CI-enforced; policy = credible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid-month spend&lt;/td&gt;
&lt;td&gt;388 min out of 43&lt;/td&gt;
&lt;td&gt;overspent by 8x — SLO not achievable at current reliability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postmortem action&lt;/td&gt;
&lt;td&gt;renegotiate to 99.5% or invest to hit 99.9%&lt;/td&gt;
&lt;td&gt;contract must be sustainable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next quarter&lt;/td&gt;
&lt;td&gt;99.5% freshness or 2 sprints of reliability work&lt;/td&gt;
&lt;td&gt;executive trade-off&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the incident, the release halt is enforced automatically; leadership is notified; the postmortem produces a concrete SLO-renegotiation recommendation grounded in observed reliability, not aspiration. Next quarter's SLO catalogue is updated with the new target and the reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Incident duration&lt;/td&gt;
&lt;td&gt;6 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Budget consumption&lt;/td&gt;
&lt;td&gt;388 min (~9× monthly budget)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO breached&lt;/td&gt;
&lt;td&gt;freshness on analytics.orders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release halt engaged&lt;/td&gt;
&lt;td&gt;yes; auto-lifted at month-start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postmortem recommendation&lt;/td&gt;
&lt;td&gt;renegotiate to 99.5% freshness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO catalogue updated&lt;/td&gt;
&lt;td&gt;next quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Multi-window multi-burn-rate&lt;/strong&gt;&lt;/strong&gt; — the fast-burn alert paged within 15 minutes of the cliff-fall; the slow-burn alert ticketed within an hour. No incident should ever be "found" hours after it started when both alerts are wired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Automatic release halt&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;release_gate&lt;/code&gt; table + CI check enforces the policy without human intervention. This is the mechanism that turns the error budget from an accounting fiction into an operational reality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Postmortem-driven renegotiation&lt;/strong&gt;&lt;/strong&gt; — the incident revealed that 99.9% freshness was not achievable at current reliability. The postmortem is the vehicle for surfacing this and forcing a decision: renegotiate the SLO or invest in reliability. Either is fine; the wrong answer is "keep the target and hope."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Overspend accounting&lt;/strong&gt;&lt;/strong&gt; — the budget-remaining column goes negative when overspent (-345 min in this case). This visible number is the argument in the renegotiation meeting: "we set a target we systematically miss by 8x."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one PagerDuty page, one Jira ticket, one leadership email, one 1-hour postmortem meeting, one SLO catalogue update. Compared to the alternative — silently missing the SLO every month while consumers lose trust — this cycle costs about half an engineer-day per incident and buys credibility with every downstream team.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Time series&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — time-series&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Time-series and burn-rate problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/time-series" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — time-series/sql&lt;/span&gt;
&lt;strong&gt;SQL time-series and window problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/time-series/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. On-call rotations and incident runbooks for data
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Rotation × escalation × runbook — the three anchors of data on-call that scale past 10 pipelines
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a data on-call rotation is the scheduled, documented ownership of who wakes up when an SLI breach pages, structured as a primary + secondary weekly rotation (or follow-the-sun for global teams) with a defined handoff ritual, a three-tier escalation ladder for when the primary cannot mitigate alone, and a 5-field runbook linked from every alert that turns "what do I do now?" into a scripted playbook — the three anchors together are what make data reliability sustainable past 10 pipelines and 3 on-call engineers&lt;/strong&gt;. Every senior data engineer must be able to describe the rotation shape, the handoff protocol, the escalation matrix, and the runbook template in a single interview breath.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjf25qo48c7mflsjjjt6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjf25qo48c7mflsjjjt6.jpeg" alt="Iconographic on-call rotation diagram — a weekly calendar strip on the left with primary/secondary rotation slots, an escalation ladder in the middle, and a runbook page chip on the right." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rotation shape.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary + secondary.&lt;/strong&gt; Two people on rotation each week. Primary gets paged first (5 min); secondary escalated to at 15 min if primary doesn't ack. Both are compensated for on-call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly cadence.&lt;/strong&gt; One-week shifts. Shorter (daily) creates handoff overhead; longer (bi-weekly) creates burnout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-the-sun for global teams.&lt;/strong&gt; A US team + an EU team + an APAC team can each own their business-hours 8-hour block and hand off at the boundary. Sub-daily rotations only in this model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fair rotation length.&lt;/strong&gt; Aim for on-call every 4–8 weeks per engineer. More frequent burns people out; less frequent means they forget the muscle memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The handoff ritual.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When.&lt;/strong&gt; Monday morning at a fixed time (e.g. 10:00 local for the incoming on-call).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration.&lt;/strong&gt; 15–20 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attendees.&lt;/strong&gt; Outgoing primary, incoming primary, incoming secondary. Team lead optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content.&lt;/strong&gt; Open incidents, this-week's SLO/budget status, upcoming known changes (planned deploys, schema migrations), any custom alerts snoozed and why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artefact.&lt;/strong&gt; Handoff doc committed to a &lt;code&gt;handoffs/YYYY-MM-DD.md&lt;/code&gt; folder — permanent record for postmortems and audit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The escalation ladder — three tiers.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 (immediate, 0–5 min).&lt;/strong&gt; Primary on-call. Pager wakes them. They own initial triage and mitigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 (15 min if unresolved).&lt;/strong&gt; Secondary + team lead. Extra hands, additional context. Common when Tier 1 has diagnosed but needs help executing (privileged access, unfamiliar system).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 (30 min if unresolved).&lt;/strong&gt; Engineering manager + platform SRE + downstream consumer contact. When incident is affecting external commitments or requires cross-team coordination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 4 (60 min).&lt;/strong&gt; VP-level. Reserved for outages affecting revenue-generating customer flows or regulatory obligations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The runbook — 5 fields, no more.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom.&lt;/strong&gt; What the alert looks like when it fires. "Freshness burn rate &amp;gt; 14× on analytics.orders for 5+ minutes."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detection.&lt;/strong&gt; Where to look to confirm the symptom is real. "Check the Grafana &lt;code&gt;freshness_age_seconds&lt;/code&gt; panel; check the Airflow DAG status; check upstream Postgres &lt;code&gt;pg_stat_replication&lt;/code&gt;."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mitigation.&lt;/strong&gt; The steps to stop the bleeding. "Restart the DAG. If still failing, disable the upstream connector to prevent backlog. If disk full, page storage team."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalation.&lt;/strong&gt; When and to whom. "If not mitigated in 15 min, escalate to secondary + team lead. If cross-team required, engage Tier 3."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owner.&lt;/strong&gt; Who owns this runbook and reviews it quarterly. "@platform-team, reviewed 2026-Q2."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why the runbook is mandatory.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fresh 3 AM brain.&lt;/strong&gt; On-call brains are 30% as effective at 3 AM. The runbook turns novel problem-solving into script-following.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newer team members.&lt;/strong&gt; A rotation with 6 engineers means each person owns 1/6 of the deep knowledge. The runbook is how the other 5/6 gets used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-incident continuity.&lt;/strong&gt; Runbooks improve after every incident (see H2 5 postmortem). A team that treats runbooks as living documents converges on incident-proof playbooks over quarters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on on-call.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What does your on-call rotation look like?" — required answer: primary + secondary + weekly + handoff ritual.&lt;/li&gt;
&lt;li&gt;"Walk me through your escalation ladder." — required answer: three tiers with times and roles.&lt;/li&gt;
&lt;li&gt;"What's in a runbook?" — required answer: 5 fields (symptom, detection, mitigation, escalation, owner).&lt;/li&gt;
&lt;li&gt;"How do you keep runbooks up to date?" — required answer: reviewed quarterly + updated after every postmortem.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — runbook for "freshness SLO breach on orders table"
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Build a concrete runbook for the freshness SLO breach on &lt;code&gt;analytics.orders&lt;/code&gt;. Every alert on that SLI links to this runbook.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alert.&lt;/strong&gt; &lt;code&gt;FreshnessBudgetFastBurn{table="analytics.orders", sli="freshness"}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-call target.&lt;/strong&gt; Primary DE. Escalate to secondary + platform lead after 15 min.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common causes.&lt;/strong&gt; Upstream Postgres connector stalled, DAG task failure, warehouse merge queue backed up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the 5-field runbook for the freshness breach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Content type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Symptom&lt;/td&gt;
&lt;td&gt;one-line description of the alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection&lt;/td&gt;
&lt;td&gt;3–5 diagnostic commands or dashboard checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mitigation&lt;/td&gt;
&lt;td&gt;ordered list of remediation steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalation&lt;/td&gt;
&lt;td&gt;when and who&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;td&gt;team + last-reviewed date&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Runbook — Freshness SLO breach on analytics.orders&lt;/span&gt;

&lt;span class="gu"&gt;## Symptom&lt;/span&gt;
Alert: &lt;span class="sb"&gt;`FreshnessBudgetFastBurn{table="analytics.orders", sli="freshness"}`&lt;/span&gt;.
Freshness burning at 14× normal for 5+ minutes. Downstream dashboards
(Looker "Sales Ops") will display stale data older than 65 minutes.

&lt;span class="gu"&gt;## Detection&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Grafana**&lt;/span&gt; — open the "Data reliability · analytics.orders" dashboard.
   Confirm &lt;span class="sb"&gt;`freshness_age_seconds`&lt;/span&gt; is rising and above 3900 s (65 min).
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Airflow**&lt;/span&gt; — check &lt;span class="sb"&gt;`dag_orders_hourly`&lt;/span&gt; in Airflow UI. Any red tasks?
   Any tasks stuck in &lt;span class="sb"&gt;`queued`&lt;/span&gt; for &amp;gt; 10 min?
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Upstream Postgres**&lt;/span&gt; — SSH to db-primary and run:
   &lt;span class="sb"&gt;`SELECT * FROM pg_replication_slots WHERE slot_name = 'debezium_orders';`&lt;/span&gt;
   Check &lt;span class="sb"&gt;`active = true`&lt;/span&gt; and &lt;span class="sb"&gt;`lag_bytes &amp;lt; 100MB`&lt;/span&gt;.
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Warehouse**&lt;/span&gt; — check Snowflake &lt;span class="sb"&gt;`WAREHOUSE_LOAD_HISTORY`&lt;/span&gt; for the merge
   warehouse. Is queue depth &amp;gt; 20 for the last 10 minutes?

&lt;span class="gu"&gt;## Mitigation&lt;/span&gt;
Try in order; move to next step if previous doesn't resolve within 5 min:
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**Restart the DAG.**&lt;/span&gt; Airflow → dag_orders_hourly → Clear failed task.
   If succeeds, watch freshness metric for 15 min to confirm recovery.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Restart the Debezium connector**&lt;/span&gt; (if replication slot is stale):
   &lt;span class="sb"&gt;`kubectl -n data rollout restart deploy/debezium-orders`&lt;/span&gt;.
   Watch slot lag drop; freshness should catch up within 5 min.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Scale the merge warehouse**&lt;/span&gt; (if Snowflake queue is deep):
   &lt;span class="sb"&gt;`ALTER WAREHOUSE merge_wh SET WAREHOUSE_SIZE = 'LARGE';`&lt;/span&gt; for 1 h,
   then revert. Cost is +$4/h for one hour.
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Disable the upstream connector to protect backlog**&lt;/span&gt; (last resort):
   &lt;span class="sb"&gt;`kubectl -n data scale deploy/debezium-orders --replicas=0`&lt;/span&gt;.
   This stops new WAL consumption but preserves the slot. Only do this
   after Tier 2 approval — the pipeline will require manual catch-up.

&lt;span class="gu"&gt;## Escalation&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 15 min without resolution → page &lt;span class="gs"&gt;**secondary**&lt;/span&gt; + &lt;span class="gs"&gt;**@platform-lead**&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; 30 min → engage &lt;span class="gs"&gt;**@eng-manager-data**&lt;/span&gt; + notify &lt;span class="gs"&gt;**@sales-ops-owner**&lt;/span&gt;
  (dashboard consumer) via Slack #sales-ops.
&lt;span class="p"&gt;-&lt;/span&gt; 60 min affecting revenue reporting → engage &lt;span class="gs"&gt;**@vp-data**&lt;/span&gt;.

&lt;span class="gu"&gt;## Owner&lt;/span&gt;
&lt;span class="sb"&gt;`@platform-team`&lt;/span&gt;. Last reviewed: 2026-Q2. Next review: 2026-Q3.
Update this runbook after every postmortem involving analytics.orders.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;Symptom&lt;/strong&gt; section names the exact alert label and describes what the reader will see. On-call arrives at the runbook 30 seconds after the page fires; they need to confirm "yes, this is my situation" in one glance.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Detection&lt;/strong&gt; section is a sequential checklist of dashboards and commands to run. Each step is copy-pasteable. Ordered from most-likely-to-be-the-issue (Airflow, upstream Postgres) to least-likely (warehouse queue). This ordering compresses average diagnosis time.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Mitigation&lt;/strong&gt; section is &lt;em&gt;ordered&lt;/em&gt; remediation steps, each with a "stop if this works" guard. The order goes from lowest-risk (restart the DAG) to highest-risk (disable the upstream connector). Each step includes success criteria — "watch freshness metric for 15 min."&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Escalation&lt;/strong&gt; section is &lt;em&gt;time-bounded&lt;/em&gt; — 15 min, 30 min, 60 min. Escalation targets are named with @-handles so the on-call can page them directly from the runbook. The 60-min VP-level escalation is only for revenue-affecting incidents; naming the criterion prevents over-escalation.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Owner&lt;/strong&gt; section records who maintains the runbook and when it was last reviewed. Runbooks that go unreviewed for a year are stale; quarterly review + post-postmortem update keeps them alive. A runbook with no owner is a runbook no-one improves.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Symptom&lt;/td&gt;
&lt;td&gt;2-sentence alert description + consumer impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection&lt;/td&gt;
&lt;td&gt;4-step checklist with copy-paste commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mitigation&lt;/td&gt;
&lt;td&gt;4-step ordered try-list with success criteria&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalation&lt;/td&gt;
&lt;td&gt;3 time-bounded tiers with named targets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;td&gt;team + last-reviewed date + review cadence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every runbook fits on one screen. If the mitigation section runs past 5 numbered steps, split it into sub-runbooks. On-call brains at 3 AM cannot follow a 20-step checklist.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — runbook for "volume SLI &amp;lt; 50% of 7-day median"
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The volume SLI catches "row count is wildly wrong." Build the runbook for the alert that fires when the target table's count drops below 50% of the rolling 7-day median for the hour of day.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alert.&lt;/strong&gt; &lt;code&gt;VolumeBurnRateFastBurn{table="analytics.orders"}&lt;/code&gt; — sustained bad-volume ratio in the 1-hour window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common causes.&lt;/strong&gt; Upstream feed truncated, source query WHERE clause bug, backfill job overwriting live data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the runbook for the volume alert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Content type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Symptom&lt;/td&gt;
&lt;td&gt;volume dropped below 50% of baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection&lt;/td&gt;
&lt;td&gt;compare source vs target counts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mitigation&lt;/td&gt;
&lt;td&gt;quarantine target; investigate upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalation&lt;/td&gt;
&lt;td&gt;product-owner + platform-lead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;td&gt;platform-team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Runbook — Volume SLI breach on analytics.orders&lt;/span&gt;

&lt;span class="gu"&gt;## Symptom&lt;/span&gt;
Alert: &lt;span class="sb"&gt;`VolumeBurnRateFastBurn{table="analytics.orders"}`&lt;/span&gt;.
Hourly row count on analytics.orders is &amp;lt; 50% of the 7-day median for
this hour-of-day. Dashboards showing "orders per hour" will display a
misleading dip; downstream ML models may retrain on biased data.

&lt;span class="gu"&gt;## Detection&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Grafana**&lt;/span&gt; — "Data reliability · analytics.orders volume" panel.
   Confirm actual count is &amp;lt; 50% of expected baseline.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Warehouse count query**&lt;/span&gt; — run:
   &lt;span class="sb"&gt;`SELECT date_trunc('hour', event_ts), COUNT(*) FROM analytics.orders
    WHERE event_ts &amp;gt;= now() - INTERVAL '3 hours' GROUP BY 1 ORDER BY 1;`&lt;/span&gt;
   Confirm the drop is real (not a rendering issue).
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Source count query**&lt;/span&gt; — run against production.orders:
   &lt;span class="sb"&gt;`SELECT date_trunc('hour', event_ts), COUNT(*) FROM production.orders
    WHERE event_ts &amp;gt;= now() - INTERVAL '3 hours' GROUP BY 1 ORDER BY 1;`&lt;/span&gt;
   Is the drop in the source too?
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Recent deploys**&lt;/span&gt; — check the changelog for any deploy in the last
   24 h that touched the orders pipeline or the source query.

&lt;span class="gu"&gt;## Mitigation&lt;/span&gt;

&lt;span class="gu"&gt;### Case A: source is also low (upstream issue)&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Notify &lt;span class="gs"&gt;**@service-orders-owner**&lt;/span&gt; (upstream service team).
&lt;span class="p"&gt;2.&lt;/span&gt; Do NOT modify the pipeline — it's correctly reflecting reality.
&lt;span class="p"&gt;3.&lt;/span&gt; Post to #incidents: "Upstream orders source is producing X% of normal;
   downstream is accurate; investigating with @service-orders-owner."

&lt;span class="gu"&gt;### Case B: source is normal, target is low (pipeline issue)&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Quarantine the affected partitions**&lt;/span&gt;:
   &lt;span class="sb"&gt;`ALTER TABLE analytics.orders RENAME PARTITION FOR (h='2026-07-30 14:00')
    TO analytics.orders_quarantine_20260730_14;`&lt;/span&gt;
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Backfill**&lt;/span&gt; by running:
   &lt;span class="sb"&gt;`airflow tasks run dag_orders_hourly backfill --start-date=... --end-date=...`&lt;/span&gt;
&lt;span class="p"&gt;3.&lt;/span&gt; Verify count matches source before dropping the quarantine partition.

&lt;span class="gu"&gt;## Escalation&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 20 min without root cause → page &lt;span class="gs"&gt;**secondary**&lt;/span&gt; + &lt;span class="gs"&gt;**@platform-lead**&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; If Case A and upstream unresponsive within 30 min → engage
  &lt;span class="gs"&gt;**@eng-manager-orders**&lt;/span&gt; and &lt;span class="gs"&gt;**@product-orders**&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; If Case B backfill takes &amp;gt; 2 h → notify dashboard consumers via
  #sales-ops Slack.

&lt;span class="gu"&gt;## Owner&lt;/span&gt;
&lt;span class="sb"&gt;`@platform-team`&lt;/span&gt;. Last reviewed: 2026-Q2. Next review: 2026-Q3.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The symptom section again names the exact alert and calls out the &lt;em&gt;consumer impact&lt;/em&gt; — the "misleading dip" language forces the on-call to understand what dashboards will show, not just what a metric says. This impact framing drives urgency.&lt;/li&gt;
&lt;li&gt;The detection section is Case-A-vs-Case-B in disguise: comparing source and target counts is the diagnostic that branches the mitigation. This is a common pattern for volume runbooks — is it a data issue or a pipeline issue?&lt;/li&gt;
&lt;li&gt;The mitigation section is &lt;em&gt;branched&lt;/em&gt; into two cases based on the diagnostic. Case A (upstream issue) requires &lt;em&gt;no pipeline change&lt;/em&gt; — a common on-call trap is to "fix" a pipeline that's correctly reflecting a broken source. Case B (pipeline issue) requires quarantine + backfill.&lt;/li&gt;
&lt;li&gt;The quarantine step (&lt;code&gt;RENAME PARTITION ... TO ..._quarantine_...&lt;/code&gt;) is a &lt;em&gt;reversible&lt;/em&gt; mitigation — it removes bad data from the live table without deleting it. Backfill then produces the correct data; the quarantine partition can be dropped after verification.&lt;/li&gt;
&lt;li&gt;Escalation names &lt;em&gt;different people&lt;/em&gt; for Case A vs Case B — Case A escalates to the service-owner (upstream), Case B stays in the platform team. This differentiation is what stops the wrong team from being paged repeatedly for an incident they can't fix.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Diagnostic&lt;/th&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;th&gt;Escalation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A: source low&lt;/td&gt;
&lt;td&gt;source count ≈ target count&lt;/td&gt;
&lt;td&gt;no pipeline change; notify service-owner&lt;/td&gt;
&lt;td&gt;@eng-manager-orders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: source normal&lt;/td&gt;
&lt;td&gt;source count normal; target low&lt;/td&gt;
&lt;td&gt;quarantine + backfill&lt;/td&gt;
&lt;td&gt;@platform-lead&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any SLI with more than one common cause, branch the mitigation section by diagnostic. A single monolithic mitigation list encourages the on-call to run every step in order regardless of what actually broke.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — escalation matrix and comms template
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The escalation matrix documents &lt;em&gt;who&lt;/em&gt; is paged at each tier for each incident class. The comms template gives the on-call ready-made language for Slack, status pages, and executive updates. Build both.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Matrix.&lt;/strong&gt; Per-service, per-tier, named on-call groups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comms template.&lt;/strong&gt; Fill-in-the-blank Slack message + hourly update cadence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the escalation matrix for the data platform and the standard comms template for a Sev-1 incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Services&lt;/td&gt;
&lt;td&gt;orders, customers, events, payments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiers&lt;/td&gt;
&lt;td&gt;1 (primary), 2 (+lead), 3 (+eng-mgr), 4 (+VP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comms channels&lt;/td&gt;
&lt;td&gt;#incidents (Slack), status.internal (page), executive email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update cadence&lt;/td&gt;
&lt;td&gt;every 30 min while incident is live&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# escalation_matrix.yml — versioned in Git; referenced by every runbook&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tier_1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@data-oncall-primary'&lt;/span&gt;
    &lt;span class="na"&gt;tier_2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@data-oncall-secondary,@platform-lead'&lt;/span&gt;
    &lt;span class="na"&gt;tier_3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@eng-manager-data,@platform-sre,@service-orders-owner'&lt;/span&gt;
    &lt;span class="na"&gt;tier_4&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@vp-data,@vp-eng'&lt;/span&gt;
  &lt;span class="na"&gt;customers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tier_1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@data-oncall-primary'&lt;/span&gt;
    &lt;span class="na"&gt;tier_2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@data-oncall-secondary,@platform-lead'&lt;/span&gt;
    &lt;span class="na"&gt;tier_3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@eng-manager-data,@platform-sre,@service-customers-owner'&lt;/span&gt;
    &lt;span class="na"&gt;tier_4&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@vp-data,@vp-eng'&lt;/span&gt;
  &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tier_1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@streaming-oncall-primary'&lt;/span&gt;
    &lt;span class="na"&gt;tier_2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@streaming-oncall-secondary,@streaming-lead'&lt;/span&gt;
    &lt;span class="na"&gt;tier_3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@eng-manager-streaming,@platform-sre'&lt;/span&gt;
    &lt;span class="na"&gt;tier_4&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@vp-data,@vp-eng'&lt;/span&gt;
  &lt;span class="na"&gt;payments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tier_1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@data-oncall-primary'&lt;/span&gt;
    &lt;span class="na"&gt;tier_2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@data-oncall-secondary,@platform-lead,@security-oncall'&lt;/span&gt;
    &lt;span class="na"&gt;tier_3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@eng-manager-data,@security-lead,@finance-ops'&lt;/span&gt;
    &lt;span class="na"&gt;tier_4&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@vp-data,@vp-eng,@cfo'&lt;/span&gt;   &lt;span class="c1"&gt;# PCI-scope; finance leadership involved&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- comms_template.md — copy-paste for the on-call --&amp;gt;&lt;/span&gt;
&lt;span class="gu"&gt;## Incident: {sev} — {short_title}&lt;/span&gt;

&lt;span class="gs"&gt;**Start:**&lt;/span&gt; {utc_timestamp}
&lt;span class="gs"&gt;**Impact:**&lt;/span&gt; {who_is_affected_and_how}
&lt;span class="gs"&gt;**Current status:**&lt;/span&gt; {investigating|mitigating|monitoring|resolved}
&lt;span class="gs"&gt;**Next update:**&lt;/span&gt; {timestamp}
&lt;span class="gs"&gt;**On-call:**&lt;/span&gt; {@primary}, {@secondary}
&lt;span class="gs"&gt;**Ticket:**&lt;/span&gt; {link_to_incident_doc}

&lt;span class="gu"&gt;### Timeline&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; {t+00:00} Alert fired: {alert_name}
&lt;span class="p"&gt;-&lt;/span&gt; {t+00:04} On-call acked; runbook opened
&lt;span class="p"&gt;-&lt;/span&gt; {t+00:12} Detection confirmed: {finding}
&lt;span class="p"&gt;-&lt;/span&gt; {t+00:22} Mitigation applied: {action}
&lt;span class="p"&gt;-&lt;/span&gt; {t+00:35} Recovery observed on {metric}
&lt;span class="p"&gt;-&lt;/span&gt; {t+01:00} Incident resolved; postmortem scheduled for {date}

&lt;span class="gu"&gt;### Consumer notification&lt;/span&gt;
| Consumer | Notified | Method |
|---|---|---|
| Sales Ops | yes | #sales-ops Slack + email to VP-Sales |
| Fraud ML | yes | #ml-fraud Slack |
| Reverse ETL | yes | #revops Slack |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The escalation matrix is a single YAML file per data platform. Every runbook references it by service name (&lt;code&gt;escalation: matrix.orders.tier_1&lt;/code&gt;) so name changes propagate. When a person leaves the team, one edit updates every runbook.&lt;/li&gt;
&lt;li&gt;The tier structure is &lt;em&gt;the same&lt;/em&gt; across services — 4 tiers, defined times. Different services have different Tier 2/3/4 populations (payments adds security + finance because of PCI scope), but the tier concept is stable.&lt;/li&gt;
&lt;li&gt;The comms template gives the on-call a fill-in-the-blank Slack post. This matters because at 3 AM, composing an incident comms message from scratch is error-prone; the template ensures nothing critical is omitted (impact, next update, on-call, ticket).&lt;/li&gt;
&lt;li&gt;The timeline section is populated &lt;em&gt;in real time&lt;/em&gt; during the incident, not written afterwards. This becomes the raw material for the postmortem. &lt;code&gt;t+00:00&lt;/code&gt; timestamps are relative to the alert firing.&lt;/li&gt;
&lt;li&gt;The consumer notification table forces the on-call to think about downstream consumers. Even a fully-mitigated incident has a comms obligation: "sales-ops, your dashboard was stale between 09:04 and 10:15." Under-communication is the surest way to erode trust in the platform team.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artefact&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;escalation_matrix.yml&lt;/td&gt;
&lt;td&gt;who to page per service per tier&lt;/td&gt;
&lt;td&gt;reviewed quarterly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comms_template.md&lt;/td&gt;
&lt;td&gt;Slack/email template for incidents&lt;/td&gt;
&lt;td&gt;updated after major incidents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;runbook links&lt;/td&gt;
&lt;td&gt;detailed diagnostic + mitigation&lt;/td&gt;
&lt;td&gt;reviewed per postmortem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;handoff_docs/YYYY-MM-DD.md&lt;/td&gt;
&lt;td&gt;weekly rotation handoff&lt;/td&gt;
&lt;td&gt;one per Monday&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The escalation matrix, comms template, runbooks, and handoff docs are the four operational artefacts that make on-call sustainable. All four live in Git alongside the pipeline code; all four are reviewed at defined cadences; all four are updated after every incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on data on-call
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You lead a 6-engineer data platform team. Design the on-call rotation, escalation matrix, and runbook coverage that supports 24×7 SLO enforcement across 50 pipelines. Cover the rotation shape, handoff protocol, runbook backlog prioritisation, and how you'd measure the on-call load on each engineer over time."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a weekly primary+secondary rotation with follow-the-sun handoff, tiered escalation, prioritised runbook backlog, and on-call load tracking
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. Rotation shape — weekly primary + secondary; follow-the-sun over EU/US&lt;/span&gt;
&lt;span class="na"&gt;rotation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cadence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weekly&lt;/span&gt;
  &lt;span class="na"&gt;handoff_day&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Monday&lt;/span&gt;
  &lt;span class="na"&gt;handoff_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10:00 local (incoming)&lt;/span&gt;
  &lt;span class="na"&gt;primary_count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;secondary_count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;min_gap_between_shifts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4 weeks&lt;/span&gt;
  &lt;span class="na"&gt;compensation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1 day off per week on-call + on-call stipend&lt;/span&gt;

  &lt;span class="na"&gt;regions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;US&lt;/span&gt;
      &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;08:00-20:00 US-Central&lt;/span&gt;
      &lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;alice&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;bob&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;carol&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;dan&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EU&lt;/span&gt;
      &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;08:00-20:00 CET&lt;/span&gt;
      &lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;emily&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;frank&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;overnight&lt;/span&gt;
      &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20:00-08:00 US-Central&lt;/span&gt;
      &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secondary in EU takes overnight for the US half of the week; vice versa&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Escalation matrix (see previous example)&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Runbook backlog — prioritise by SLO coverage + incident frequency&lt;/span&gt;
&lt;span class="na"&gt;runbook_backlog_priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ranking_formula&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;(downstream_users × pageable_alerts_per_month) / runbook_maturity&lt;/span&gt;
  &lt;span class="c1"&gt;# runbook_maturity: 0=none, 1=draft, 2=reviewed, 3=battle-tested&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;every pageable alert has a runbook at maturity 2+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 4. On-call load tracking — pages per engineer per week&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;oncall_load&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;engineer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'week'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;week&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;off_hours_pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPOCH&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;page_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_resolve_min&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;engineer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;page_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;resolved_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HOUR&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;page_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;pagerduty_incidents&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;page_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'90 days'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;engineer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'week'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;week&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_pages&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Alert on team-lead if any engineer exceeds 10 off-hours pages / week&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;engineer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;week&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;off_hours_pages&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;oncall_load&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;off_hours_pages&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;week&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'4 weeks'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 5. Runbook backlog auto-updater — runs monthly
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prioritise_runbook_backlog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT c.table_name, c.sli_name,
                   COALESCE(rb.maturity, 0) AS runbook_maturity,
                   COUNT(p.id) AS pages_last_90d,
                   COALESCE(ct.downstream_users, 1) AS users
            FROM   slo_catalogue c
            LEFT JOIN runbooks rb USING (table_name, sli_name)
            LEFT JOIN pagerduty_incidents p
              ON p.alert_labels @&amp;gt; jsonb_build_object(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, c.table_name, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sli&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, c.sli_name)
             AND p.page_time &amp;gt;= now() - INTERVAL &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;90 days&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
            LEFT JOIN table_consumers ct USING (table_name)
            GROUP BY 1, 2, 3, 5
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;scored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maturity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maturity&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# higher = higher priority
&lt;/span&gt;        &lt;span class="n"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maturity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; maturity=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; pages=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; users=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; score=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Design choice&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rotation cadence&lt;/td&gt;
&lt;td&gt;weekly&lt;/td&gt;
&lt;td&gt;balance handoff overhead vs burnout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handoff&lt;/td&gt;
&lt;td&gt;Monday 10:00 local&lt;/td&gt;
&lt;td&gt;fresh brains; not weekend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Follow-the-sun&lt;/td&gt;
&lt;td&gt;US + EU pools&lt;/td&gt;
&lt;td&gt;reduces overnight pages by 50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalation matrix&lt;/td&gt;
&lt;td&gt;Git-tracked YAML&lt;/td&gt;
&lt;td&gt;one source of truth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runbook priority&lt;/td&gt;
&lt;td&gt;(users × pages) / maturity&lt;/td&gt;
&lt;td&gt;quantitative backlog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load tracking&lt;/td&gt;
&lt;td&gt;off-hours pages per engineer&lt;/td&gt;
&lt;td&gt;detect burnout early&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the design is deployed, the team has a documented rotation, a searchable escalation matrix, a scored runbook backlog, and a load-tracking dashboard. The next quarterly review adjusts the rotation pool as engineers join and leave, retargets the runbook backlog based on incident patterns, and reviews the load-tracking dashboard for engineers approaching burnout thresholds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;On-call engineers&lt;/td&gt;
&lt;td&gt;6 (rotation every 6 weeks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handoff cadence&lt;/td&gt;
&lt;td&gt;weekly Monday&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Follow-the-sun coverage&lt;/td&gt;
&lt;td&gt;US + EU pool splits overnight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runbook backlog target&lt;/td&gt;
&lt;td&gt;100% pageable alerts at maturity 2+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Off-hours page threshold&lt;/td&gt;
&lt;td&gt;10/week per engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalation matrix format&lt;/td&gt;
&lt;td&gt;Git YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Weekly primary + secondary&lt;/strong&gt;&lt;/strong&gt; — the rotation shape that trades off handoff overhead (frequent enough) against burnout (not too frequent). Adding secondary decouples "primary is asleep / in a meeting" from "no-one is responding."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Follow-the-sun overnight coverage&lt;/strong&gt;&lt;/strong&gt; — a US-only rotation means every on-call gets some overnight pages. Splitting overnight with an EU pool cuts off-hours pages roughly in half — the single biggest on-call quality-of-life lever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Escalation matrix as YAML&lt;/strong&gt;&lt;/strong&gt; — human-readable, Git-tracked, referenced by name from every runbook. One edit updates every alert; roles are role-based (&lt;code&gt;@platform-lead&lt;/code&gt;) not person-based, so team changes don't cascade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Runbook backlog prioritised by (users × pages) / maturity&lt;/strong&gt;&lt;/strong&gt; — the runbook you write next should be the one whose gap costs the most and whose maturity is lowest. This is a quantitative backlog ranker; without it, teams write runbooks for the wrong alerts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — 6 engineers × ~2 hours/week on-call meta-work (handoff + runbook maintenance) = ~12 engineer-hours/week to sustain the rotation. Compared to the "hero on-call" alternative (one senior engineer paged every incident, burns out in 6 months), this is the operationally sane approach that keeps teams together for years.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on operational tooling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Log processing&lt;/span&gt;
&lt;span&gt;Topic — log-processing&lt;/span&gt;
&lt;strong&gt;Log-processing and alert-signal problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/log-processing" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Post-incident review and reliability roadmap
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Blameless postmortem → action items → reliability roadmap — the loop that compounds
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;a post-incident review is the structured, blameless meeting held within 5 business days of every SLO breach that produces a written timeline, an explicit list of contributing factors (not a single root cause), a set of tracked action items with owners and due dates, and an update to the reliability roadmap — and the roadmap itself is the quarterly plan that turns incidents into a compounding investment in SLI coverage, runbook maturity, and chaos-drill practice&lt;/strong&gt;. Every senior data engineer must be able to describe the postmortem template, defend the "contributing factors, not root cause" preference, and explain how action items feed the reliability roadmap.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff3cw8bl0khgmgsx3vff2.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff3cw8bl0khgmgsx3vff2.jpeg" alt="Iconographic post-incident diagram — a horizontal timeline with four labelled stops (detect, mitigate, resolve, review) and an action-items board on the right with three cards showing owner and due date." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The postmortem template — one document per incident.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header.&lt;/strong&gt; Incident title, severity, start time, end time, MTTA (mean time to acknowledge), MTTM (mitigate), MTTR (resolve).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impact.&lt;/strong&gt; Which SLIs breached, budget consumed, downstream consumers affected, revenue impact (if any).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeline.&lt;/strong&gt; Minute-by-minute log of what happened, ideally populated in real time from the comms template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contributing factors.&lt;/strong&gt; A list — not one root cause. Every incident has 3–7 factors that together enabled it; ignoring the softer contributors misses future prevention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What went well.&lt;/strong&gt; Non-negotiable section. Reinforces successful mitigations; motivates the team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What went badly.&lt;/strong&gt; What we wish we'd done differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action items.&lt;/strong&gt; Concrete tickets, each with owner + due date + SLI impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Blameless — what it actually means.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Focus on systems, not people.&lt;/strong&gt; "The on-call missed the alert" → "The alert routing was mis-configured and did not reach the on-call phone." The system is the target of improvement, not the human.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume rational actors.&lt;/strong&gt; Everyone involved was doing their best with the information they had. If a decision looks bad in hindsight, ask "what information was missing?" not "who was careless?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language matters.&lt;/strong&gt; Ban words like "should have" ("the on-call should have checked X"). Prefer "we observed that…" and "in future we can…"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Author is the incident owner.&lt;/strong&gt; Not the on-call themselves — someone one step removed writes the postmortem to reduce self-blame framing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;"5 whys" vs "contributing factors" — the SRE preference.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5 whys.&lt;/strong&gt; Sequential drill-down to a single root cause. Popular in classical incident response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why SRE prefers contributing factors.&lt;/strong&gt; Real incidents have multiple simultaneous causes: an upstream schema change &lt;em&gt;and&lt;/em&gt; a stale runbook &lt;em&gt;and&lt;/em&gt; a missing alert &lt;em&gt;and&lt;/em&gt; a confused escalation. Picking one "root" misses the other three; fixing only the root leaves the other three primed to re-fire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The alternative.&lt;/strong&gt; List 3–7 contributing factors; assign an action item per factor. Some may be "no action" (accepted risk), but all are named.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example.&lt;/strong&gt; Freshness breach: (1) upstream connector stalled on schema change, (2) burn-rate alert threshold too high to catch early, (3) runbook didn't include the schema-change check, (4) on-call was in a meeting and secondary was on PTO, (5) dashboard consumer wasn't notified for 45 min. Fix all five.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The reliability roadmap.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cadence.&lt;/strong&gt; Reviewed quarterly at platform-team offsite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs.&lt;/strong&gt; Postmortem action items, SLI-coverage backlog, on-call load metrics, chaos-drill results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure.&lt;/strong&gt; One-quarter view (in-flight), next-quarter view (planned), 6-month view (aspirational).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sizing.&lt;/strong&gt; Each item sized as small (1 sprint), medium (2 sprints), large (a quarter). Aim for ~30% capacity on reliability work per engineer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owner.&lt;/strong&gt; Platform-team lead; reviewed with engineering director.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on postmortems.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What's in a postmortem template?" — required answer: header, impact, timeline, contributing factors, what went well, what went badly, action items.&lt;/li&gt;
&lt;li&gt;"Why blameless?" — required answer: focus on systems not people; assume rational actors; language matters.&lt;/li&gt;
&lt;li&gt;"Root cause vs contributing factors — which do you prefer?" — required answer: contributing factors; explain why single root misses the softer factors.&lt;/li&gt;
&lt;li&gt;"How do action items become reliability improvements?" — required answer: feed the quarterly reliability roadmap.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — postmortem for a 2-hour freshness SLO breach
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A concrete postmortem for a real-shape incident on &lt;code&gt;analytics.orders&lt;/code&gt;. The freshness SLO (99.9% &amp;lt; 65 min) breached at 09:15 and did not recover until 11:12 — 117 minutes over the threshold, consuming ~117 min of the 43 min monthly budget (overspent by ~2.7x).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Root event.&lt;/strong&gt; An upstream Postgres schema change added a &lt;code&gt;discount_cents&lt;/code&gt; column; the Debezium connector required a schema-history sync that briefly stalled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compounding factors.&lt;/strong&gt; Fast-burn alert threshold was too coarse; runbook did not include the schema-change check; on-call was in a meeting; consumer was not notified until minute 45.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the postmortem for the incident using the standard template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Incident&lt;/td&gt;
&lt;td&gt;freshness breach on analytics.orders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duration&lt;/td&gt;
&lt;td&gt;117 min above threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Budget consumed&lt;/td&gt;
&lt;td&gt;117 min (277% of month's total)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Severity&lt;/td&gt;
&lt;td&gt;Sev-2 (business-hours; single dashboard affected)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumers affected&lt;/td&gt;
&lt;td&gt;Sales Ops dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Postmortem — Freshness SLO breach on analytics.orders (2026-07-30)&lt;/span&gt;

&lt;span class="gu"&gt;## Header&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Incident ID:**&lt;/span&gt; INC-2026-07-30-001
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Severity:**&lt;/span&gt; Sev-2
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Start:**&lt;/span&gt; 2026-07-30 09:15 UTC
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**End:**&lt;/span&gt; 2026-07-30 11:12 UTC
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Duration:**&lt;/span&gt; 117 min above SLO threshold
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**MTTA:**&lt;/span&gt; 12 min (alert fired 09:15, acked 09:27)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**MTTM:**&lt;/span&gt; 42 min (mitigation applied 09:57)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**MTTR:**&lt;/span&gt; 117 min (fully recovered 11:12)

&lt;span class="gu"&gt;## Impact&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**SLIs breached:**&lt;/span&gt; freshness on analytics.orders
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Budget consumed:**&lt;/span&gt; 117 min (277% of monthly 43-min budget; overspent by 74 min)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Downstream consumers:**&lt;/span&gt; Sales Ops dashboard (staleness visible to VP-Sales)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Revenue impact:**&lt;/span&gt; none directly; delayed sales-pipeline update by 2 h

&lt;span class="gu"&gt;## Timeline&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**09:12**&lt;/span&gt; Upstream deployed &lt;span class="sb"&gt;`ALTER TABLE production.orders ADD COLUMN discount_cents INT`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**09:15**&lt;/span&gt; Debezium connector paused for schema-history sync
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**09:15**&lt;/span&gt; Freshness metric began rising above threshold
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**09:27**&lt;/span&gt; FreshnessBudgetFastBurn fired; on-call (Alice) acked (was in team meeting)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**09:35**&lt;/span&gt; Alice opened runbook; ran Airflow + Postgres checks — nothing obvious
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**09:47**&lt;/span&gt; Alice noticed Debezium connector was paused
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**09:57**&lt;/span&gt; Alice restarted connector; freshness metric began recovering
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**10:00**&lt;/span&gt; Alice posted to #sales-ops (45 min after alert): "Data delay affecting orders dashboard"
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**10:45**&lt;/span&gt; Freshness reached below threshold intermittently; still burning budget
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**11:12**&lt;/span&gt; Freshness stable under threshold; incident resolved
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**11:30**&lt;/span&gt; Alice paged secondary + platform-lead to write postmortem

&lt;span class="gu"&gt;## Contributing factors (not root cause)&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Upstream schema change was not coordinated with the platform team.**&lt;/span&gt; The service team deployed the ALTER TABLE without notifying data platform. Downstream systems (including Debezium) require a heads-up for schema changes.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Debezium connector required manual restart after schema-history sync.**&lt;/span&gt; The connector paused for ~40 min waiting for schema propagation; auto-restart on schema change is not configured.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Runbook did not include the "check Debezium connector" step.**&lt;/span&gt; The on-call spent 20 min investigating Airflow and Postgres before checking the connector — this should have been step 1.
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**On-call was in a team meeting when alert fired.**&lt;/span&gt; Meeting was internal (skippable), but the on-call did not have "leave meeting on page" muscle memory.
&lt;span class="p"&gt;5.&lt;/span&gt; &lt;span class="gs"&gt;**Consumer (Sales Ops) was notified 45 min into the incident.**&lt;/span&gt; Comms template exists but was not invoked until after mitigation was underway.

&lt;span class="gu"&gt;## What went well&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Fast-burn alert fired within 12 min of SLI degradation — the alerting worked.
&lt;span class="p"&gt;-&lt;/span&gt; Alice correctly identified Debezium as the cause once she looked at it.
&lt;span class="p"&gt;-&lt;/span&gt; Mitigation (connector restart) was fast and clean once identified.
&lt;span class="p"&gt;-&lt;/span&gt; Team switched to secondary/lead promptly to write the postmortem.

&lt;span class="gu"&gt;## What went badly&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; MTTM (42 min) was too high because the runbook did not point at Debezium.
&lt;span class="p"&gt;-&lt;/span&gt; Consumer comms delayed 45 min — dashboard viewers had no explanation for the stale data.
&lt;span class="p"&gt;-&lt;/span&gt; Monthly error budget was consumed 2.7x in a single incident.

&lt;span class="gu"&gt;## Action items&lt;/span&gt;
| ID | Owner | Due | SLI impact |
|---|---|---|---|
| AI-1: Coordinate schema-change protocol with service teams (RFC + Confluence page) | @platform-lead | +14 d | prevents recurrence |
| AI-2: Add "check Debezium connector" as step 1 in freshness runbook | @alice | +3 d | reduces MTTM to ~15 min |
| AI-3: Configure Debezium auto-restart on schema-history sync | @bob | +21 d | eliminates cause |
| AI-4: Add "leave meetings on page" to on-call onboarding doc | @platform-lead | +7 d | reduces MTTA |
| AI-5: Add consumer-comms trigger to fast-burn alert (auto-Slack to dashboard channel) | @carol | +14 d | consumer comms lag → 0 |
| AI-6: Re-negotiate freshness SLO from 99.9% to 99.7% (achievable at current infra) | @platform-lead | +30 d | sustainable SLO |

&lt;span class="gu"&gt;## Reliability roadmap update&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Add "Debezium schema-change hardening" to Q3 platform roadmap (from AI-3).
&lt;span class="p"&gt;-&lt;/span&gt; Add "schema-change protocol" to Q3 cross-team RFC list (from AI-1).
&lt;span class="p"&gt;-&lt;/span&gt; Track AI-2, AI-4, AI-5 in weekly platform standup until closed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The header captures the four operational metrics — MTTA, MTTM, MTTR, duration — that every postmortem should quantify. These become the trend metrics the team watches over quarters ("MTTM is dropping — the runbooks are getting better").&lt;/li&gt;
&lt;li&gt;The impact section names &lt;em&gt;which SLIs&lt;/em&gt; breached and by &lt;em&gt;how much&lt;/em&gt;. "Freshness on analytics.orders, 117 min over threshold, 277% of monthly budget." This is the language leadership understands; vague "the pipeline was slow" is uselessly abstract.&lt;/li&gt;
&lt;li&gt;The timeline is populated &lt;em&gt;from the comms template&lt;/em&gt; — the Slack messages posted during the incident become the raw timeline. This is why the comms template matters: it produces documentation as a byproduct of communication.&lt;/li&gt;
&lt;li&gt;The contributing factors section lists 5 concrete factors — not one root cause. "The upstream schema change" is &lt;em&gt;a&lt;/em&gt; factor, but "the runbook didn't include the check" and "the on-call was in a meeting" are equally load-bearing. Fixing only the schema-change misses the softer factors.&lt;/li&gt;
&lt;li&gt;Every action item is a Jira ticket-shape: ID, owner, due date, SLI impact. This is what turns a postmortem from a document into an operational improvement. The postmortem author's job is to &lt;em&gt;enforce&lt;/em&gt; that action items are entered into Jira before the postmortem meeting closes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Postmortem section&lt;/th&gt;
&lt;th&gt;Word count target&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Header&lt;/td&gt;
&lt;td&gt;~50 words&lt;/td&gt;
&lt;td&gt;quantitative facts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impact&lt;/td&gt;
&lt;td&gt;~100 words&lt;/td&gt;
&lt;td&gt;consumer + budget framing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeline&lt;/td&gt;
&lt;td&gt;~200 words&lt;/td&gt;
&lt;td&gt;minute-by-minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contributing factors&lt;/td&gt;
&lt;td&gt;~200 words&lt;/td&gt;
&lt;td&gt;3–7 named factors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What went well&lt;/td&gt;
&lt;td&gt;~100 words&lt;/td&gt;
&lt;td&gt;motivation + reinforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What went badly&lt;/td&gt;
&lt;td&gt;~100 words&lt;/td&gt;
&lt;td&gt;honest self-critique&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action items&lt;/td&gt;
&lt;td&gt;table of 5–10 rows&lt;/td&gt;
&lt;td&gt;traceable improvement backlog&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every postmortem is authored by someone one step removed from the on-call, sized to fit on 2 pages, and finalised within 5 business days of the incident. Every action item is entered into Jira before the review meeting adjourns.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — action-item tracker with owner + due date + SLI impact
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Action items from every postmortem land in a single tracker that the platform-team lead reviews weekly. The tracker feeds the quarterly reliability roadmap. Build the tracker.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema.&lt;/strong&gt; ID, source postmortem, owner, due date, status, SLI impact, size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence.&lt;/strong&gt; Reviewed at weekly platform standup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalation.&lt;/strong&gt; Overdue items surfaced to eng-manager at the +2 week mark.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the action-item tracker table and the weekly-review query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Jira project &lt;code&gt;RELIAB&lt;/code&gt; with custom fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Or&lt;/td&gt;
&lt;td&gt;Postgres &lt;code&gt;reliability_action_items&lt;/code&gt; table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly review&lt;/td&gt;
&lt;td&gt;Monday standup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overdue escalation&lt;/td&gt;
&lt;td&gt;+2 weeks past due date&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- reliability_action_items — tracker table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;reliability_action_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt;   &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;postmortem_id&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- e.g. INC-2026-07-30-001&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;           &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;owner&lt;/span&gt;           &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- @-handle&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;      &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;due_date&lt;/span&gt;        &lt;span class="nb"&gt;DATE&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;          &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- open|in-progress|done|dropped&lt;/span&gt;
    &lt;span class="n"&gt;sli_impact&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- 'prevents recurrence' | 'reduces MTTM by 30 min' | ...&lt;/span&gt;
    &lt;span class="k"&gt;size&lt;/span&gt;            &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- 'small' | 'medium' | 'large'&lt;/span&gt;
    &lt;span class="n"&gt;closed_at&lt;/span&gt;       &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dropped_reason&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_action_items_status_due&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;reliability_action_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Weekly review query — what's open, what's overdue&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;postmortem_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;due_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;days_until_due&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sli_impact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'done'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'closed'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'14 days'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'ESCALATE'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'overdue'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'due soon'&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'on-track'&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;review_status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;reliability_action_items&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'in-progress'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Overdue-escalation cron — emails the eng-manager weekly
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.message&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;EmailMessage&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;escalate_overdue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DB_DSN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT id, postmortem_id, title, owner, due_date
            FROM   reliability_action_items
            WHERE  status IN (&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;in-progress&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
              AND  due_date &amp;lt; CURRENT_DATE - INTERVAL &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;14 days&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;The following reliability action items are &amp;gt;2 weeks overdue:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;aid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;due&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;  [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;aid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; · owner &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; · due &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;due&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; · from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Please follow up in Monday standup.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmailMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Subject&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; reliability action items overdue &amp;gt;2w&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;From&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reliability-bot@internal&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;To&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eng-manager-data@internal&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SMTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mail-relay:25&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;escalate_overdue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The tracker table stores one row per action item with the four fields that make follow-through possible: owner, due date, status, and SLI impact. Every item comes from a postmortem, so &lt;code&gt;postmortem_id&lt;/code&gt; links back to the source incident document.&lt;/li&gt;
&lt;li&gt;The weekly review query surfaces open items by due date. The &lt;code&gt;review_status&lt;/code&gt; column translates raw dates into review categories: on-track / due soon / overdue / ESCALATE. The Monday standup walks the query results top-down.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ESCALATE&lt;/code&gt; status (&amp;gt;2 weeks past due) triggers the escalation cron — an email to the eng-manager. This is the mechanism that prevents action items from silently rotting; overdue items get management visibility on a fixed cadence.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;sli_impact&lt;/code&gt; column forces the postmortem author to name &lt;em&gt;why&lt;/em&gt; the action item matters. "Prevents recurrence" is stronger than "improves reliability"; "reduces MTTM by 30 min on freshness alerts" is stronger still. Vague impacts predict items that never get done.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;dropped_reason&lt;/code&gt; column exists for items that get explicitly abandoned. Sometimes a postmortem action turns out to be wrong ("we thought we needed to buy X, but we found a cheaper fix"). Recording &lt;em&gt;why&lt;/em&gt; it was dropped preserves the reasoning for future postmortems.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item ID&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Due&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Review&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;Add Debezium check to runbook&lt;/td&gt;
&lt;td&gt;&lt;a class="mentioned-user" href="https://dev.to/alice"&gt;@alice&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2026-08-02&lt;/td&gt;
&lt;td&gt;in-progress&lt;/td&gt;
&lt;td&gt;on-track&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;Configure Debezium auto-restart&lt;/td&gt;
&lt;td&gt;&lt;a class="mentioned-user" href="https://dev.to/bob"&gt;@bob&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2026-08-20&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;on-track&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;Schema-change protocol RFC&lt;/td&gt;
&lt;td&gt;@platform-lead&lt;/td&gt;
&lt;td&gt;2026-07-25&lt;/td&gt;
&lt;td&gt;in-progress&lt;/td&gt;
&lt;td&gt;overdue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;Volume runbook v2&lt;/td&gt;
&lt;td&gt;&lt;a class="mentioned-user" href="https://dev.to/carol"&gt;@carol&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2026-07-12&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;ESCALATE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every postmortem action item must have (owner, due date, SLI impact, size) fields populated before the postmortem meeting closes. Weekly review + 2-week auto-escalation is what keeps the tracker alive.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — quarterly reliability roadmap tied to SLO trend
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The reliability roadmap is a quarterly plan that turns the action-item backlog into a prioritised sequence of platform investments. Build the Q3 roadmap for the data platform team.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inputs.&lt;/strong&gt; Open action items, SLO-trend data, on-call load, chaos-drill gaps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output.&lt;/strong&gt; Prioritised list of ~10 items sized to consume 30% of team capacity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence.&lt;/strong&gt; Reviewed at quarter-start offsite; revisited at mid-quarter checkpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draft the Q3 2026 reliability roadmap for the data platform team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Team size&lt;/td&gt;
&lt;td&gt;6 engineers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability capacity&lt;/td&gt;
&lt;td&gt;30% = 1.8 engineer-quarters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roadmap items&lt;/td&gt;
&lt;td&gt;~10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prioritisation input&lt;/td&gt;
&lt;td&gt;SLO trend, incident frequency, on-call load&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Q3 2026 — Data Platform Reliability Roadmap&lt;/span&gt;

&lt;span class="gu"&gt;## Context&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Q2 saw 4 Sev-2 incidents; 2 were freshness-related on analytics.orders.
&lt;span class="p"&gt;-&lt;/span&gt; SLO trend: freshness slipping from 99.94% → 99.87% quarter-over-quarter.
&lt;span class="p"&gt;-&lt;/span&gt; On-call load: 2 engineers over 10 off-hours pages/week threshold.
&lt;span class="p"&gt;-&lt;/span&gt; Q3 goal: hit 99.9% freshness on top-10 tables, reduce off-hours pages 30%.

&lt;span class="gu"&gt;## Q3 Investments (prioritised — 30% team capacity ≈ 1.8 engineer-quarters)&lt;/span&gt;

&lt;span class="gu"&gt;### Tier 1 — must ship this quarter (0.8 EQ)&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Debezium schema-change hardening**&lt;/span&gt; (@bob, medium, from AI-3 INC-2026-07-30-001)
   → SLI impact: eliminates freshness cliffs from upstream ALTER TABLE.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Runbook coverage pass on top-10 tables**&lt;/span&gt; (@alice, medium)
   → SLI impact: raises runbook maturity from 1.4 avg to 2.5+.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Consumer-comms auto-trigger on fast-burn alerts**&lt;/span&gt; (@carol, small, from AI-5)
   → SLI impact: consumer comms lag 45 min → 0.

&lt;span class="gu"&gt;### Tier 2 — plan to ship (0.6 EQ)&lt;/span&gt;
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Hot-standby DAG for orders pipeline**&lt;/span&gt; (@dan, large)
   → SLI impact: freshness recovery time p99 from 30 min to 5 min.
&lt;span class="p"&gt;5.&lt;/span&gt; &lt;span class="gs"&gt;**Quarterly chaos drills — kill Debezium mid-hour**&lt;/span&gt; (@emily, small)
   → SLI impact: verifies runbooks + on-call muscle memory.
&lt;span class="p"&gt;6.&lt;/span&gt; &lt;span class="gs"&gt;**On-call load rebalance — recruit 2 more into pool**&lt;/span&gt; (@platform-lead, medium)
   → SLI impact: off-hours pages per engineer 12/wk → 7/wk.

&lt;span class="gu"&gt;### Tier 3 — best-effort (0.4 EQ)&lt;/span&gt;
&lt;span class="p"&gt;7.&lt;/span&gt; &lt;span class="gs"&gt;**Volume SLI on top-20 tables**&lt;/span&gt; (@frank, medium)
   → SLI coverage: 10 → 20 tables under volume SLO.
&lt;span class="p"&gt;8.&lt;/span&gt; &lt;span class="gs"&gt;**Automated backfill on volume breach**&lt;/span&gt; (@bob, medium)
   → SLI impact: volume MTTR 90 min → 30 min.
&lt;span class="p"&gt;9.&lt;/span&gt; &lt;span class="gs"&gt;**Postmortem author rotation**&lt;/span&gt; (@platform-lead, small)
   → team practice: spread postmortem-writing skill.

&lt;span class="gu"&gt;### Stretch (if capacity allows)&lt;/span&gt;
&lt;span class="p"&gt;10.&lt;/span&gt; &lt;span class="gs"&gt;**Great Expectations integration for quality SLI**&lt;/span&gt; (@carol, large)
    → SLI coverage: quality SLI on all top-20 tables (currently 5).

&lt;span class="gu"&gt;## Success metrics for Q3&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Freshness SLO on top-10 tables: 99.87% → ≥ 99.9%.
&lt;span class="p"&gt;-&lt;/span&gt; Off-hours pages per engineer: 12/wk → ≤ 8/wk median.
&lt;span class="p"&gt;-&lt;/span&gt; Runbook coverage on pageable alerts: 60% → 100% at maturity 2+.
&lt;span class="p"&gt;-&lt;/span&gt; Reliability action-item completion rate: 60% → 80% within due date.

&lt;span class="gu"&gt;## Mid-quarter checkpoint&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Week 6 offsite: review Tier 1 progress; re-prioritise Tier 2/3 if needed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The context section grounds the roadmap in &lt;em&gt;quantitative&lt;/em&gt; Q2 data: incident count, SLO trend, on-call load. Prioritisation reasoning is transparent — a director reviewing the roadmap can see &lt;em&gt;why&lt;/em&gt; Debezium hardening is Tier 1 (2 incidents in Q2, causing 74% of budget overspend).&lt;/li&gt;
&lt;li&gt;Investments are sized in engineer-quarters (EQ) and grouped into three tiers by priority. Tier 1 (0.8 EQ) is roughly half a person's quarter — must-ship items where cutting is not an option. Tier 2 is planned; Tier 3 is best-effort. Stretch is opportunistic.&lt;/li&gt;
&lt;li&gt;Every item names owner, size, source postmortem (where applicable), and SLI impact. The SLI-impact line is the "why this matters" that survives roadmap review. Items without a clear SLI impact are candidates for de-prioritisation.&lt;/li&gt;
&lt;li&gt;The success metrics section commits to &lt;em&gt;measurable outcomes&lt;/em&gt; for the quarter. "Freshness SLO 99.87% → 99.9%" is testable; "improve reliability" is not. These become the Q4-review inputs.&lt;/li&gt;
&lt;li&gt;The mid-quarter checkpoint is non-negotiable — real quarters produce surprises (a new incident, a person leaving, a business priority shift). The checkpoint is when the roadmap is re-scoped honestly rather than silently over-committing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Roadmap section&lt;/th&gt;
&lt;th&gt;Items&lt;/th&gt;
&lt;th&gt;EQ&lt;/th&gt;
&lt;th&gt;Success metric&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tier 1 must-ship&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;td&gt;freshness ≥ 99.9%, comms lag = 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier 2 planned&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;recovery ≤ 5 min, pages ≤ 8/wk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier 3 best-effort&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0.4&lt;/td&gt;
&lt;td&gt;volume coverage → 20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stretch&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;opportunistic&lt;/td&gt;
&lt;td&gt;quality SLI on 20 tables&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The reliability roadmap is &lt;em&gt;quantitative&lt;/em&gt; (EQ, SLI impact, target metrics), &lt;em&gt;time-bounded&lt;/em&gt; (one quarter, with a mid-quarter checkpoint), and &lt;em&gt;traceable back to postmortems&lt;/em&gt; (every Tier 1 item cites a source incident). Roadmaps that lack these three properties become wish lists that never ship.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on postmortems
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You had a 4-hour freshness SLO breach that consumed the entire monthly budget in one incident. Walk me through your postmortem process, the contributing factors you'd expect to find, the action items you'd extract, and how those action items become platform reliability improvements over the next quarter."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a blameless postmortem with contributing-factors analysis, tracked action items, and quarterly-roadmap integration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Postmortem — 4h Freshness SLO Breach (INC-2026-08-14-001)&lt;/span&gt;

&lt;span class="gu"&gt;## Header&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Severity:**&lt;/span&gt; Sev-1 (revenue-affecting)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Duration:**&lt;/span&gt; 4 h 12 min above threshold
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**MTTA / MTTM / MTTR:**&lt;/span&gt; 8 min / 2 h 15 min / 4 h 12 min
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Budget consumed:**&lt;/span&gt; 252 min (585% of monthly)

&lt;span class="gu"&gt;## Impact&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Freshness SLO on analytics.orders and analytics.line_items breached.
&lt;span class="p"&gt;-&lt;/span&gt; Executive dashboard, ML fraud model, and reverse-ETL to Salesforce all
  showed stale data.
&lt;span class="p"&gt;-&lt;/span&gt; Sales-ops meeting at 14:00 was held with stale numbers; revised the
  next morning.

&lt;span class="gu"&gt;## Contributing factors&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Postgres primary failed over unexpectedly.**&lt;/span&gt; Hardware issue on the
   AZ; failover to replica took 90 s but Debezium slot did not follow.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Debezium reconnect logic did not handle failover cleanly.**&lt;/span&gt; Slot on
   the new primary did not exist; connector entered CrashLoopBackoff.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="ge"&gt;**&lt;/span&gt;Fast-burn alert fired quickly (8 min MTTA) but runbook did not
   include the "failover" case.&lt;span class="ge"&gt;**&lt;/span&gt; On-call spent 90 min investigating
   other causes.
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Debezium slot re-creation required schema-registry manual sync.**&lt;/span&gt;
   Documented procedure existed but was in an unfamiliar Confluence page.
&lt;span class="p"&gt;5.&lt;/span&gt; &lt;span class="gs"&gt;**Consumer notifications were manual and delayed.**&lt;/span&gt; Reverse-ETL owner
   found out via customer complaint, not our comms.

&lt;span class="gu"&gt;## What went well&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; MTTA of 8 min shows the fast-burn alert is properly tuned.
&lt;span class="p"&gt;-&lt;/span&gt; Team collaboration was fast — 3 engineers on the call within 30 min.
&lt;span class="p"&gt;-&lt;/span&gt; Root cause identified correctly once on-call opened the connector logs.

&lt;span class="gu"&gt;## What went badly&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; MTTM of 2h 15m was way too high; the runbook missed the failover case.
&lt;span class="p"&gt;-&lt;/span&gt; Consumer comms delayed; reverse-ETL owner learned via customer.
&lt;span class="p"&gt;-&lt;/span&gt; 252 min of budget consumed = 5.85× monthly allowance.

&lt;span class="gu"&gt;## Action items&lt;/span&gt;
| ID | Title | Owner | Due | Size |
|---|---|---|---|---|
| AI-1 | Add "check for Postgres failover" as detection step 1 in freshness runbook | @primary-oncall | +3d | small |
| AI-2 | Configure Debezium slot re-creation on connect failure | @platform-lead | +21d | medium |
| AI-3 | Move slot re-creation procedure from Confluence into runbook itself | @primary-oncall | +7d | small |
| AI-4 | Auto-Slack the consumer channel when fast-burn fires | @carol | +14d | small |
| AI-5 | Add "Postgres failover" scenario to Q3 chaos drills | @emily | +30d | medium |
| AI-6 | Post-mortem: renegotiate freshness SLO given failover risk | @platform-lead | +45d | small |

&lt;span class="gu"&gt;## Roadmap update&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Elevate AI-2 to Q3 Tier 1 (was Tier 2).
&lt;span class="p"&gt;-&lt;/span&gt; Move AI-5 chaos-drill to Q3 Tier 1 (was Tier 2).
&lt;span class="p"&gt;-&lt;/span&gt; Adjust Q3 success metric: freshness p99 recovery ≤ 15 min (was 30 min).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Reliability roadmap-update trigger: any Sev-1 auto-elevates related items&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;reliability_action_items&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;    &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Tier 1'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;postmortem_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'INC-2026-08-14-001'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Tier 2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Tier 3'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Auto-notify the roadmap owner&lt;/span&gt;
&lt;span class="k"&gt;NOTIFY&lt;/span&gt; &lt;span class="n"&gt;roadmap_update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'INC-2026-08-14-001 auto-elevated to Tier 1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postmortem written&lt;/td&gt;
&lt;td&gt;within 5 business days&lt;/td&gt;
&lt;td&gt;fresh memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contributing factors&lt;/td&gt;
&lt;td&gt;5 named&lt;/td&gt;
&lt;td&gt;avoids single-root reductionism&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action items&lt;/td&gt;
&lt;td&gt;6 with owner + due date&lt;/td&gt;
&lt;td&gt;traceable improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roadmap elevation&lt;/td&gt;
&lt;td&gt;AI-2, AI-5 → Q3 Tier 1&lt;/td&gt;
&lt;td&gt;Sev-1 forces re-prioritisation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO renegotiation&lt;/td&gt;
&lt;td&gt;tabled as AI-6&lt;/td&gt;
&lt;td&gt;data-driven, not reactive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the postmortem, the platform team has 6 concrete tracked items, two of which have been elevated to the current-quarter must-ship tier. The Q3 roadmap is republished with the updated priorities; the SLO catalogue is scheduled for renegotiation in 45 days when AI-2 (Debezium hardening) is complete and we have new failover-recovery data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postmortem authored&lt;/td&gt;
&lt;td&gt;day +3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contributing factors identified&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action items opened&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roadmap items elevated&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLO renegotiation scheduled&lt;/td&gt;
&lt;td&gt;day +45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3 goal adjusted&lt;/td&gt;
&lt;td&gt;recovery p99 30 → 15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Blameless framing&lt;/strong&gt;&lt;/strong&gt; — no individual is named as at-fault. The contributing factors are all &lt;em&gt;system&lt;/em&gt; properties (missing runbook step, missing Debezium logic, missing comms automation). This preserves psychological safety while producing sharper improvements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Contributing factors, not root cause&lt;/strong&gt;&lt;/strong&gt; — the Postgres failover is &lt;em&gt;a&lt;/em&gt; factor, but the Debezium reconnect logic, the missing runbook step, the buried procedure, and the manual comms are equally load-bearing. Fixing all five is what prevents the next 4-hour breach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Action items with owner + due date&lt;/strong&gt;&lt;/strong&gt; — every factor becomes a concrete ticket. AI-1 and AI-3 are small (3–7 days); AI-2 and AI-5 are medium (2–4 weeks). The distribution keeps the postmortem operational: small items ship fast; medium items feed the roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Roadmap auto-elevation&lt;/strong&gt;&lt;/strong&gt; — the postmortem doesn't just recommend roadmap changes; it &lt;em&gt;executes&lt;/em&gt; them via the SQL update. This is the operational mechanism that prevents postmortem findings from being ignored between quarterly offsites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one postmortem meeting (~90 min), one 2-page document, 6 Jira tickets, one roadmap re-shuffle. Compared to the alternative — quiet Slack apology, informal Slack thread, no tracked improvements — this is what turns a bad incident into a compounding reliability investment over quarters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on reliability engineering&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;API integration&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — api-integration&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;API-integration and comms-flow problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/api-integration" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — Data SRE recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The four axes as the SLI menu.&lt;/strong&gt; Every pipeline gets up to four SLIs: &lt;strong&gt;freshness&lt;/strong&gt; (&lt;code&gt;now() - MAX(ingested_at)&lt;/code&gt; on the target table), &lt;strong&gt;completeness&lt;/strong&gt; (target rowcount / source rowcount per hour), &lt;strong&gt;volume&lt;/strong&gt; (target rowcount vs 7-day median band ±30%), &lt;strong&gt;quality&lt;/strong&gt; (fraction of rows passing schema + constraint checks). Freshness is per-minute; completeness and volume are per-hour; quality is per-daily-run. Skip any axis that doesn't have a downstream consumer who cares — SLIs cost engineer-time to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness SLI PromQL.&lt;/strong&gt; &lt;code&gt;data_freshness_meets_slo{table="..."}&lt;/code&gt; — 1 if within threshold, 0 otherwise. Roll up: &lt;code&gt;sum_over_time(...[28d]) / count_over_time(...[28d])&lt;/code&gt;. Budget remaining: &lt;code&gt;(current_slo − target) / (1 − target)&lt;/code&gt;. Show the last one as a big-bold-number Grafana panel per top-10 table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completeness SLI SQL.&lt;/strong&gt; &lt;code&gt;WITH src AS (SELECT hour, COUNT(*) n_src FROM source GROUP BY hour), tgt AS (SELECT hour, COUNT(*) n_tgt FROM target GROUP BY hour) SELECT hour, n_tgt::float / n_src FROM src LEFT JOIN tgt USING (hour) WHERE hour &amp;lt; now() - INTERVAL '5 min';&lt;/code&gt; — persist per-hour to &lt;code&gt;slo_measurements&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error-budget monthly template.&lt;/strong&gt; Total = &lt;code&gt;(1 − slo_target) × observation_windows_per_month&lt;/code&gt;. For per-minute observations: 99% = 432 min, 99.5% = 216 min, 99.9% = 43 min, 99.95% = 21.6 min, 99.99% = 4.3 min. Track remaining as &lt;code&gt;total − spent&lt;/code&gt;. Show as a fuel-tank icon on the reliability dashboard; halt releases at 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-window multi-burn-rate alert.&lt;/strong&gt; Fast burn: &lt;code&gt;(1 - avg_over_time(sli[1h])) &amp;gt; 14 × (1 - slo_target) AND (1 - avg_over_time(sli[5m])) &amp;gt; 14 × (1 - slo_target) for 2m&lt;/code&gt; → page. Slow burn: &lt;code&gt;[6h] &amp;gt; 6× AND [30m] &amp;gt; 6× for 15m&lt;/code&gt; → ticket. Both wired against the same SLI; both link to the same runbook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5-field runbook template.&lt;/strong&gt; &lt;code&gt;## Symptom&lt;/code&gt; (2 sentences), &lt;code&gt;## Detection&lt;/code&gt; (3–5 commands or dashboards), &lt;code&gt;## Mitigation&lt;/code&gt; (ordered try-list with success criteria), &lt;code&gt;## Escalation&lt;/code&gt; (time-bounded tiers with @-handles), &lt;code&gt;## Owner&lt;/code&gt; (team + last-reviewed date). One screen max. No runbook link = alert doesn't ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-call rotation shape.&lt;/strong&gt; Weekly primary + secondary; handoff Monday 10:00 local (incoming); minimum 4 weeks between shifts; follow-the-sun with EU/US pools for overnight coverage. Compensate with one day off per week on-call plus stipend. Track pages per engineer per week; escalate to eng-manager if any engineer exceeds 10 off-hours pages weekly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalation matrix as YAML.&lt;/strong&gt; One &lt;code&gt;escalation_matrix.yml&lt;/code&gt; in Git; per-service tier 1/2/3/4 as @-handles. Every runbook references by name (&lt;code&gt;matrix.orders.tier_1&lt;/code&gt;). Change once, propagates everywhere. Role-based (&lt;code&gt;@platform-lead&lt;/code&gt;) not person-based to survive team changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comms template.&lt;/strong&gt; Fill-in-the-blank Slack post: {sev}, {start}, {impact}, {status}, {next_update}, {on_call}. Timeline populated real-time (&lt;code&gt;t+00:00 alert fired&lt;/code&gt;, &lt;code&gt;t+00:04 acked&lt;/code&gt;, ...). Consumer notification table forces the on-call to acknowledge downstream consumers exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmortem sections.&lt;/strong&gt; Header (MTTA/MTTM/MTTR), Impact (SLIs + budget + consumers), Timeline (from comms template), Contributing factors (3–7, not one root), What went well, What went badly, Action items (owner + due + SLI impact + size). 2 pages; finalised within 5 business days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contributing factors, not root cause.&lt;/strong&gt; Real incidents have multiple simultaneous causes. Listing all 3–7 and assigning an action per factor catches the "soft" contributors (missing runbook step, missed comms) that a single-root-cause analysis silently ignores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action-item tracker fields.&lt;/strong&gt; ID, postmortem_id, title, owner, due_date, status, sli_impact, size (small/medium/large). Weekly standup review; auto-escalate to eng-manager at +2 weeks past due. Track completion rate; aim for &amp;gt;80% within due date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability roadmap.&lt;/strong&gt; Quarterly, sized in engineer-quarters, prioritised Tier 1/2/3/stretch. Tier 1 ≈ 40% of reliability capacity (must-ship); Tier 2 ≈ 35% (planned); Tier 3 ≈ 25% (best-effort). Mid-quarter checkpoint mandatory. Every Tier 1 item cites a source postmortem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLO renegotiation trigger.&lt;/strong&gt; If the current 28-day SLO is below target for three consecutive months &lt;em&gt;and&lt;/em&gt; the platform team has already shipped two Tier 1 reliability fixes without moving the metric, renegotiate the SLO downward. The alternative — keeping an aspirational SLO the team cannot hit — destroys the credibility of every SLO in the catalogue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a data SLO in one sentence?
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;data SLO&lt;/strong&gt; (service-level objective) is an explicit, agreed-upon target for a &lt;strong&gt;data SLI&lt;/strong&gt; (service-level indicator) computed over a rolling measurement window — for example, "99.9% of one-minute freshness observations on &lt;code&gt;analytics.orders&lt;/code&gt; have &lt;code&gt;MAX(ingested_at)&lt;/code&gt; within 65 minutes of &lt;code&gt;now()&lt;/code&gt; over the trailing 28 days." The SLO is a &lt;em&gt;contract&lt;/em&gt; between the platform team producing the dataset and the downstream consumers depending on it; the SLO percentage translates to an &lt;strong&gt;error budget&lt;/strong&gt; (&lt;code&gt;1 − target&lt;/code&gt;) that governs the operational trade-off between shipping features and investing in reliability. Every senior data-engineering interview probes SLOs because they are the load-bearing mechanism that turns "we monitor pipelines" into "we own user-visible reliability."&lt;/p&gt;

&lt;h3&gt;
  
  
  SLI vs SLO vs error budget — what's the difference?
&lt;/h3&gt;

&lt;p&gt;An &lt;strong&gt;SLI&lt;/strong&gt; (service-level indicator) is the &lt;em&gt;measurement&lt;/em&gt; — a numeric fact about the pipeline's output artifact, computed on the dataset rather than on the job that produced it. Examples: "age in seconds of the newest row in &lt;code&gt;analytics.orders&lt;/code&gt;," "target rowcount / source rowcount for the last hour," "fraction of rows passing dbt tests today." An &lt;strong&gt;SLO&lt;/strong&gt; (service-level objective) is the &lt;em&gt;target&lt;/em&gt; expressed as a percentage of measurement windows in which the SLI meets a threshold — for example, "99.9% of one-minute windows have freshness under 65 minutes." An &lt;strong&gt;error budget&lt;/strong&gt; is the mathematical complement of the SLO: &lt;code&gt;1 − SLO&lt;/code&gt;, expressed in minutes or days per month. A 99.9% freshness SLO is a 43-minute monthly error budget. The three concepts stack: SLI → SLO → error budget → burn-rate alerts → release-halt policy. Getting each definition right is the first thing senior interviewers probe.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I pick freshness targets for my pipelines?
&lt;/h3&gt;

&lt;p&gt;Derive the target from the &lt;em&gt;downstream consumer&lt;/em&gt; who cares most, then sanity-check against current observed performance. Start with an interview: sit with the dashboard owner or service owner who consumes the dataset and ask "how stale is too stale?" Their answer is usually anchored to a business cadence ("I check it every 5 minutes; more than an hour behind and my meeting is derailed") — that becomes your freshness target with a small buffer (add ~5 minutes above the natural cadence). Then compute the SLI over the last 28 days: if current freshness is 99.7% and the consumer wants 99.9%, the gap is a 3× improvement in bad-minute count — that's a project, not a target. If the gap is 10× or larger, either invest in specific reliability improvements first, or renegotiate the target down to something achievable. Publish the SLO in a Git-tracked catalogue with the consumer's name against it; review quarterly. The wrong pattern is to unilaterally invent a 99.99% target because it "sounds professional" — an aspirational SLO no team can hit destroys the credibility of every SLO in your catalogue.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does a data on-call rotation actually do?
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;data on-call rotation&lt;/strong&gt; is the documented schedule of who receives pages when a data SLI breaches, structured around three anchors: &lt;strong&gt;rotation shape&lt;/strong&gt; (primary + secondary weekly cadence with follow-the-sun handoff for global teams), &lt;strong&gt;escalation ladder&lt;/strong&gt; (tier 1 primary in 5 min → tier 2 secondary+lead in 15 min → tier 3 eng-manager+SRE in 30 min → tier 4 VP in 60 min for revenue-affecting incidents), and &lt;strong&gt;linked runbooks&lt;/strong&gt; (every alert points to a 5-field document: symptom, detection, mitigation, escalation, owner). The on-call's job during a shift is to acknowledge pages within 5 minutes, open the linked runbook, execute the detection and mitigation steps, escalate on the defined timeline if unresolved, communicate to affected consumers using the standard comms template, and author (or receive) the postmortem within 5 business days if an SLO is breached. Between shifts, the on-call attends the Monday handoff standup, contributes to runbook updates from the last week's incidents, and participates in the quarterly reliability-roadmap review. Rotation should recur no more often than every 4 weeks per engineer to prevent burnout; follow-the-sun EU/US splits cut off-hours pages roughly in half — the single biggest quality-of-life lever available.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a burn-rate alert and why do I need multi-window?
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;burn-rate alert&lt;/strong&gt; fires when an SLI is spending its error budget faster than the sustainable rate that would exhaust the budget over the full SLO window. For a 99.9% freshness SLO with 43 minutes of monthly budget, the sustainable burn rate is 1× (1.43 min/day). A burn rate of 14× means the whole month's budget would be gone in about 2 days — that's the fast-burn threshold that pages the on-call immediately. A burn rate of 6× means the budget is gone in about 5 days — the slow-burn threshold that files a ticket. &lt;strong&gt;Multi-window multi-burn-rate&lt;/strong&gt; (from the Google SRE workbook) computes burn on both a short window (typically 1 hour) &lt;em&gt;and&lt;/em&gt; a long window (typically 6 hours), and alerts only when both windows agree the burn is high — this catches cliff-falls quickly via the short window while catching creeping degradation via the long window, and it prevents old resolved spikes from re-firing as they age out of a single long window. Without multi-window burn-rate, teams either over-alert on transient spikes (alert fatigue, false-page cost) or under-alert on slow creeping degradations (silent SLO breaches, discovered from consumer complaints). Multi-window is the standard SRE pattern; naming it in a senior interview is a strong signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I run a blameless postmortem for a data incident?
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;blameless postmortem&lt;/strong&gt; is a structured written review of an incident, authored within 5 business days by someone one step removed from the on-call, following a template that captures &lt;strong&gt;impact&lt;/strong&gt; (which SLIs breached, budget consumed, downstream consumers affected), &lt;strong&gt;timeline&lt;/strong&gt; (populated in real-time from the incident comms template — &lt;code&gt;t+00:00 alert fired&lt;/code&gt;, &lt;code&gt;t+00:04 acked&lt;/code&gt;, etc.), &lt;strong&gt;contributing factors&lt;/strong&gt; (a list of 3–7 named factors, &lt;em&gt;not&lt;/em&gt; a single root cause — a schema change &lt;em&gt;and&lt;/em&gt; a missing runbook step &lt;em&gt;and&lt;/em&gt; a delayed comms &lt;em&gt;and&lt;/em&gt; an on-call in a meeting are all load-bearing), &lt;strong&gt;what went well&lt;/strong&gt; (reinforces successful mitigations), &lt;strong&gt;what went badly&lt;/strong&gt; (honest self-critique), and &lt;strong&gt;action items&lt;/strong&gt; (each with owner, due date, SLI impact, and size — small, medium, or large). The &lt;em&gt;blameless&lt;/em&gt; discipline is enforced by focusing every finding on system properties rather than individual people ("the alert routing didn't reach on-call" not "the on-call missed the alert"), banning "should have" language, and assuming rational actors doing their best with available information. Action items become Jira tickets before the postmortem meeting closes; the tracker is reviewed weekly; overdue items auto-escalate to the eng-manager at +2 weeks; and the reliability roadmap absorbs the medium/large items into the next quarter's plan. The whole loop — detect → mitigate → resolve → review → improve → roadmap — is what turns individual incidents into a compounding reliability investment over quarters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt; for pipeline observability, incremental-load, and reconcile problems senior interviewers love.&lt;/li&gt;
&lt;li&gt;Rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt; for the reliability, on-call, and platform-architecture scenarios that show up in senior data-platform loops.&lt;/li&gt;
&lt;li&gt;Sharpen the reliability axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;data-validation practice library →&lt;/a&gt; for the SLI-shaped quality and schema-check questions.&lt;/li&gt;
&lt;li&gt;Add the &lt;a href="https://pipecode.ai/explore/practice/topic/time-series" rel="noopener noreferrer"&gt;time-series practice library →&lt;/a&gt; and the &lt;a href="https://pipecode.ai/explore/practice/topic/time-series/sql" rel="noopener noreferrer"&gt;SQL time-series subset →&lt;/a&gt; for burn-rate, rolling-window, and freshness-metric SQL patterns.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the four-axis SLI framework against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Own reliability like an SRE, not a task-runner&lt;/h3&gt;

&lt;p&gt;Docs explain SLOs. PipeCode drills explain the decision — how to pick freshness targets a consumer will actually sign, when a burn-rate alert should page vs ticket, how to write a runbook the on-call reads at 3 AM, and how to convert a postmortem into a quarter of reliability improvement. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — pattern-first practice tuned for the production trade-offs senior data engineers actually face.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice ETL problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice design problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Pytest for Data Engineering: Fixtures, Parametrization &amp; Docker-Compose Integration Tests</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 31 Jul 2026 14:33:59 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/pytest-for-data-engineering-fixtures-parametrization-docker-compose-integration-tests-3omd</link>
      <guid>https://dev.to/gowthampotureddi/pytest-for-data-engineering-fixtures-parametrization-docker-compose-integration-tests-3omd</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;pytest for data engineering&lt;/code&gt;&lt;/strong&gt; is the load-bearing test framework for every serious 2026 data platform — the one tool that lets a senior engineer wire dbt model contracts, Airflow DAG smoke tests, Spark transform property tests, and end-to-end Postgres+Kafka+MinIO integration suites into a single &lt;code&gt;pytest&lt;/code&gt; invocation that runs on a laptop, on a Codespace, and on GitHub Actions with the exact same behaviour. Every ingestion pipeline you ship pushes rows through code paths whose failure modes (schema drift, null explosion, off-by-one window boundaries, transaction leakage) can only be caught by tests that exercise the real behaviour against a real database — and pytest's fixture model, parametrization primitives, and testcontainers ecosystem are what turn that from a Herculean manual chore into a &lt;code&gt;pytest -n auto&lt;/code&gt; one-liner.&lt;/p&gt;

&lt;p&gt;This guide is the senior-DE walkthrough you wished existed the first time an interviewer asked "walk me through your &lt;code&gt;pytest fixtures&lt;/code&gt; scope strategy for a suite with 300 DB-touching tests," or "how do you &lt;code&gt;pytest parametrize&lt;/code&gt; a Spark UDF across the null / edge / valid axis without exploding your test count," or "your integration suite spins up &lt;code&gt;pytest docker-compose&lt;/code&gt; per-worker under &lt;code&gt;pytest-xdist&lt;/code&gt; — how do you keep the Postgres port from colliding?" It walks through the four canonical test layers — unit (pure Python transforms), contract (dbt/Spark schema + not-null assertions), component-with-DB (&lt;code&gt;pytest-postgresql&lt;/code&gt; transactional fixtures), and integration-with-compose (&lt;code&gt;pytest testcontainers&lt;/code&gt; running Postgres + Kafka + MinIO) — the four axes senior interviewers actually probe (fixture scope, parametrization discipline, container hygiene, CI reproducibility), the canonical config for each, and the CI wiring that keeps a 1000-test data-engineering suite under three minutes on GitHub Actions. Each section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjtmxtj5x6jolvey5f3ju.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjtmxtj5x6jolvey5f3ju.jpeg" alt="PipeCode blog header for pytest for data engineering — bold white headline 'Pytest for Data Engineering' over a hero composition of four small glyph medallions (fixture-brick, parametrize-grid, docker-whale, CI-gear) arranged around a central purple 'TEST' seal, on a dark gradient." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/python" rel="noopener noreferrer"&gt;Python practice library →&lt;/a&gt;, rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt;, and sharpen the validation axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;data-validation practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why pytest is the load-bearing test framework for data engineering in 2026&lt;/li&gt;
&lt;li&gt;Fixtures — session scope, factory patterns, DB seed lifecycles&lt;/li&gt;
&lt;li&gt;Parametrization — table-driven tests over dbt/Spark transforms&lt;/li&gt;
&lt;li&gt;Docker-Compose + Testcontainers integration tests&lt;/li&gt;
&lt;li&gt;CI wiring — coverage, xdist parallelism, flaky-test discipline&lt;/li&gt;
&lt;li&gt;Cheat sheet — pytest recipes for data engineering&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why pytest is the load-bearing test framework for data engineering in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Four test layers, one framework — why pytest wins the DE stack over unittest, nose, and hand-rolled runners
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;&lt;code&gt;pytest for data engineering&lt;/code&gt; is the framework where every meaningful test layer (unit transforms, dbt/Spark schema contracts, component tests against a real Postgres, and end-to-end integration suites spun up via &lt;code&gt;pytest docker-compose&lt;/code&gt; or &lt;code&gt;pytest testcontainers&lt;/code&gt;) shares one runner, one fixture graph, one parametrization primitive, and one CI story — which is what makes a 1000-test data-engineering suite feasible on a laptop, in a Codespace, and on GitHub Actions with identical behaviour&lt;/strong&gt;. The choice of &lt;code&gt;pytest&lt;/code&gt; over &lt;code&gt;unittest&lt;/code&gt;, &lt;code&gt;nose2&lt;/code&gt;, or Bazel-native runners is not aesthetic; it is structural. Fixtures compose. Parametrization is declarative. Plugins (&lt;code&gt;pytest-xdist&lt;/code&gt;, &lt;code&gt;pytest-cov&lt;/code&gt;, &lt;code&gt;pytest-postgresql&lt;/code&gt;, &lt;code&gt;pytest-mock&lt;/code&gt;, &lt;code&gt;pytest-rerunfailures&lt;/code&gt;) plug in without ceremony. Every other framework in the Python ecosystem has receded, and in 2026 there is no serious data-platform team that does not run &lt;code&gt;pytest&lt;/code&gt; as the entry point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four test layers every serious DE team ships.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layer 1 — unit tests.&lt;/strong&gt; Pure-Python transforms with no I/O — a &lt;code&gt;normalize_email&lt;/code&gt; function, a &lt;code&gt;parse_iso_timestamp&lt;/code&gt; helper, a &lt;code&gt;chunk_iterable&lt;/code&gt; utility. Sub-millisecond per test; hundreds per file; run on every save. This is the base of the pyramid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 2 — contract tests.&lt;/strong&gt; dbt schema tests (&lt;code&gt;not_null&lt;/code&gt;, &lt;code&gt;unique&lt;/code&gt;, &lt;code&gt;accepted_values&lt;/code&gt;), Great Expectations suites, JSON-schema validators for API payloads, &lt;code&gt;pydantic&lt;/code&gt; model validation. Fast; deterministic; catch schema drift before it reaches production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 3 — component tests with a real DB.&lt;/strong&gt; SQL-emitting Python code executed against a real Postgres (via &lt;code&gt;pytest-postgresql&lt;/code&gt; template DB) or DuckDB (in-process). Catch off-by-one window boundaries, transaction-leakage bugs, and index-usage regressions that unit tests cannot. ~100 ms per test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 4 — integration tests with docker-compose.&lt;/strong&gt; Airflow scheduler + Postgres + Redis + a Kafka broker + MinIO, all spun up via &lt;code&gt;docker-compose&lt;/code&gt; or &lt;code&gt;testcontainers-python&lt;/code&gt;, then a DAG runs end-to-end. ~5–30 s per test; run on PR and nightly. Catches the failure modes that only appear when networked services interact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The four axes senior interviewers probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixture scope discipline.&lt;/strong&gt; Do you know when to use &lt;code&gt;session&lt;/code&gt; (expensive setup like a container) versus &lt;code&gt;module&lt;/code&gt; (per-file setup) versus &lt;code&gt;function&lt;/code&gt; (per-test isolation)? Do you know that &lt;code&gt;autouse=True&lt;/code&gt; at session scope is a footgun that couples every test to that fixture whether it needs it or not? This is the single most-probed pytest topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parametrization hygiene.&lt;/strong&gt; Do you know how to use &lt;code&gt;ids=&lt;/code&gt; to keep parametrized test names readable? Do you know when to use &lt;code&gt;indirect=True&lt;/code&gt; to run params through a fixture? Do you know that &lt;code&gt;pytest_generate_tests&lt;/code&gt; lets you parametrize dynamically from a manifest? Senior teams reject test files where parametrized test IDs are &lt;code&gt;test_transform[test_transform0]&lt;/code&gt; instead of &lt;code&gt;test_transform[null-input]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container hygiene.&lt;/strong&gt; Do you spin one container per session (fast, shared state) or per-test (slow, hermetic)? Do you know the &lt;code&gt;wait-for-ready&lt;/code&gt; health-check idiom? Do you clean up volumes at teardown? Do you know how to make containers survive &lt;code&gt;pytest-xdist&lt;/code&gt; parallelism without port collisions? Senior signal: name &lt;code&gt;testcontainers-python&lt;/code&gt; and the &lt;code&gt;network=&lt;/code&gt; parameter unprompted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI reproducibility.&lt;/strong&gt; Does the suite pass on your laptop, in Codespaces, and in GitHub Actions the same way — or does it flake on CI only? Do you have a coverage gate (&lt;code&gt;--cov-fail-under&lt;/code&gt;)? Do you have flaky-test discipline (&lt;code&gt;pytest-rerunfailures&lt;/code&gt; + quarantine + nightly retriage)? Do you know how to shard across xdist workers without breaking DB fixtures?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The 2026 reality — pytest + testcontainers is the default; the alternatives are legacy.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;unittest&lt;/code&gt;&lt;/strong&gt; ships in the stdlib and remains for legacy codebases, but nobody starts a greenfield DE project with it in 2026. The lack of fixture composition, the boilerplate &lt;code&gt;setUp&lt;/code&gt; / &lt;code&gt;tearDown&lt;/code&gt;, and the absence of parametrization make it a dead end for the kind of matrix-driven testing DE work demands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nose&lt;/code&gt; / &lt;code&gt;nose2&lt;/code&gt;&lt;/strong&gt; are historical curiosities; the maintainers have moved on. Any codebase still on nose should schedule a migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest&lt;/code&gt; + &lt;code&gt;pytest-xdist&lt;/code&gt; + &lt;code&gt;pytest-cov&lt;/code&gt; + &lt;code&gt;pytest-postgresql&lt;/code&gt; (or &lt;code&gt;testcontainers-python&lt;/code&gt;)&lt;/strong&gt; is the canonical stack. Every dbt-adapter, every Airflow provider, every managed-Spark toolkit publishes fixtures and helpers targeting this stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bazel / pants / native build runners&lt;/strong&gt; wrap pytest rather than replacing it — even in monorepos, the actual test execution goes through &lt;code&gt;pytest&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you name &lt;strong&gt;all four test layers&lt;/strong&gt; without prompting? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you say &lt;strong&gt;"session-scope for the expensive container, function-scope for the test data"&lt;/strong&gt; in the first sentence when scope comes up? — required answer.&lt;/li&gt;
&lt;li&gt;Do you push back on &lt;strong&gt;"just use unittest.mock everywhere"&lt;/strong&gt; with the argument that "mocks don't catch schema drift; component tests against a real Postgres do"? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name &lt;strong&gt;&lt;code&gt;testcontainers-python&lt;/code&gt;&lt;/strong&gt; (or &lt;code&gt;pytest-docker-compose&lt;/code&gt;) as the integration primitive rather than "we shell out to docker-compose"? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you describe pytest as &lt;strong&gt;"a fixture graph plus a parametrization matrix"&lt;/strong&gt; rather than as "a test framework"? — required answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the four-layer test pyramid for a dbt + Airflow + Spark platform
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful artifact for a pytest-for-DE interview is a memorised four-layer pyramid table. Every senior test-strategy discussion converges on this table within the first ten minutes; having it in your head is what separates a fluent answer from a stumbling one. Walk through building the table for a hypothetical retail data platform that runs dbt in Snowflake, Airflow on Kubernetes, and Spark on EMR.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform.&lt;/strong&gt; dbt-core targeting Snowflake, Airflow 2.9 on Kubernetes, Spark 3.5 on EMR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test count budget.&lt;/strong&gt; ~800 unit + ~300 contract + ~150 component + ~40 integration = ~1,290 tests total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed budget.&lt;/strong&gt; Unit ~10 s total, contract ~30 s, component ~2 min, integration ~5 min. Full suite under 8 minutes on CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage target.&lt;/strong&gt; ≥85% line coverage, ≥70% branch coverage on the Python packages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the four-layer test pyramid for the platform, name the pytest plugin for each layer, and pick the run-frequency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Plugins&lt;/th&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;td&gt;~10 s&lt;/td&gt;
&lt;td&gt;pytest, pytest-mock&lt;/td&gt;
&lt;td&gt;on save&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contract&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;~30 s&lt;/td&gt;
&lt;td&gt;pytest, pydantic, dbt-core&lt;/td&gt;
&lt;td&gt;on save&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;~2 min&lt;/td&gt;
&lt;td&gt;pytest-postgresql, pytest-mock&lt;/td&gt;
&lt;td&gt;on PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;td&gt;testcontainers-python, pytest-xdist&lt;/td&gt;
&lt;td&gt;on PR + nightly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/conftest.py — one file wires all four layers
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="c1"&gt;# ------------------------------------------------------------------
# Layer 1 (unit) — no fixtures needed; pure functions
# ------------------------------------------------------------------
&lt;/span&gt;

&lt;span class="c1"&gt;# ------------------------------------------------------------------
# Layer 2 (contract) — pydantic + manifest-driven parametrization
# ------------------------------------------------------------------
&lt;/span&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dbt_manifest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Loaded once per session; used by parametrized contract tests.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
    &lt;span class="n"&gt;manifest_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target/manifest.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manifest_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="c1"&gt;# ------------------------------------------------------------------
# Layer 3 (component-with-DB) — pytest-postgresql template
# ------------------------------------------------------------------
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pytest_postgresql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;

&lt;span class="n"&gt;postgresql_proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postgresql_proc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unixsocketdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;postgresql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql_proc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# ------------------------------------------------------------------
# Layer 4 (integration) — testcontainers-python
# ------------------------------------------------------------------
&lt;/span&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compose_stack&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Spin up Postgres + Kafka + MinIO once per session.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;testcontainers.compose&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DockerCompose&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DockerCompose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compose_file_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose.test.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9001/minio/health/ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Layer 1 (unit) needs no fixtures — pure Python transforms are called directly with in-memory args. This is the fastest layer and where the bulk of the tests live. Sub-millisecond per test.&lt;/li&gt;
&lt;li&gt;Layer 2 (contract) uses one session-scoped &lt;code&gt;dbt_manifest&lt;/code&gt; fixture loaded once, then parametrized tests iterate every model in the manifest. This gives you 300 tests for the cost of ~30 lines of code — the parametrization amplifier is what makes contract testing tractable.&lt;/li&gt;
&lt;li&gt;Layer 3 (component) uses &lt;code&gt;pytest-postgresql&lt;/code&gt;'s &lt;code&gt;postgresql_proc&lt;/code&gt; (session-scoped process fixture that starts one Postgres binary) and &lt;code&gt;postgresql&lt;/code&gt; (function-scoped database that gets a fresh template-cloned DB per test). This gives per-test DB isolation at the cost of ~100 ms per test — template-DB cloning is what makes the pattern fast.&lt;/li&gt;
&lt;li&gt;Layer 4 (integration) uses &lt;code&gt;testcontainers-python&lt;/code&gt;'s &lt;code&gt;DockerCompose&lt;/code&gt; fixture at session scope — the compose stack starts once per pytest run, and every integration test shares it. The &lt;code&gt;wait_for&lt;/code&gt; health check is non-optional; without it, tests race the container startup and flake.&lt;/li&gt;
&lt;li&gt;The four layers are declared in a single &lt;code&gt;conftest.py&lt;/code&gt; at the repo root and &lt;em&gt;automatically discovered&lt;/em&gt; by pytest — you don't import them, you don't wire them; they just appear in every test file that names them. This is fixture-graph magic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Fixture scope&lt;/th&gt;
&lt;th&gt;Per-test cost&lt;/th&gt;
&lt;th&gt;Isolation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;&amp;lt;1 ms&lt;/td&gt;
&lt;td&gt;pure functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contract&lt;/td&gt;
&lt;td&gt;session (manifest)&lt;/td&gt;
&lt;td&gt;~1 ms per model&lt;/td&gt;
&lt;td&gt;in-memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component&lt;/td&gt;
&lt;td&gt;session (proc) + function (db)&lt;/td&gt;
&lt;td&gt;~100 ms&lt;/td&gt;
&lt;td&gt;fresh DB per test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;session (compose)&lt;/td&gt;
&lt;td&gt;~5–30 s&lt;/td&gt;
&lt;td&gt;shared containers, isolated schemas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never mix scopes across layers by accident. If a component test starts to depend on a session-scoped compose fixture, you have promoted it to Layer 4 — accept the ~5 s per-test cost or refactor the dependency out.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — what interviewers actually probe about pytest for DE
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The senior data-engineering pytest interview has a predictable structure: the interviewer opens with an ambiguous question ("how would you test a dbt project?"), then progressively narrows to test whether you know the axes. The candidates who name the four layers in sentence one score highest; the candidates who describe "we write unit tests with mocks for everything" score lowest. Walk through the interview grading rubric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous opener.&lt;/strong&gt; "How would you build a test suite for our dbt + Airflow + Spark platform?" — invites you to name the four layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 1.&lt;/strong&gt; "How do you make the Postgres fixture fast enough for 150 tests?" — probes fixture scope + template-DB knowledge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 2.&lt;/strong&gt; "How do you parametrize a schema-contract test over every table in the schema?" — probes &lt;code&gt;pytest_generate_tests&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 3.&lt;/strong&gt; "How do you run the suite in parallel without Postgres fixtures colliding?" — probes xdist + port allocation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 4.&lt;/strong&gt; "One test in the integration suite flakes 5% of the time. What's your discipline?" — probes rerunfailures + quarantine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draft a 5-minute senior pytest-for-DE answer that covers all four axes without waiting to be asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interview signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;Senior answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Layers named&lt;/td&gt;
&lt;td&gt;"we write pytest tests"&lt;/td&gt;
&lt;td&gt;"unit, contract, component-with-DB, integration-with-compose"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixture scope&lt;/td&gt;
&lt;td&gt;"we use setUp"&lt;/td&gt;
&lt;td&gt;"session for containers, function for data, template-DB clone per test"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parametrize discipline&lt;/td&gt;
&lt;td&gt;"we loop inside the test"&lt;/td&gt;
&lt;td&gt;"@pytest.mark.parametrize with ids=; pytest_generate_tests for dynamic"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container story&lt;/td&gt;
&lt;td&gt;"we shell out to docker-compose"&lt;/td&gt;
&lt;td&gt;"testcontainers-python with wait_for health checks; --xdist-group by port"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flaky discipline&lt;/td&gt;
&lt;td&gt;"we rerun the pipeline"&lt;/td&gt;
&lt;td&gt;"pytest-rerunfailures with reruns=2; quarantine marker; nightly retriage"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Senior pytest-for-DE answer template (5 minutes)
================================================

Minute 1 — name the four layers up front
  "I'd build four layers: unit for pure transforms, contract for
   dbt/pydantic schema, component-with-DB via pytest-postgresql for
   SQL-emitting code, and integration-with-compose via
   testcontainers-python for the end-to-end Airflow DAG."

Minute 2 — fixture scope discipline
  "Session-scoped fixtures for the expensive things — the Postgres
   process, the compose stack, the dbt manifest. Function-scoped
   fixtures for the isolation-critical things — the DB itself
   (template-cloned per test), the tmp_path, the monkeypatched env."

Minute 3 — parametrization discipline
  "Every contract test uses @pytest.mark.parametrize with explicit
   ids= so test names read like test_not_null[orders.customer_id]. For
   dynamic axes — every model in the dbt manifest — I use
   pytest_generate_tests to expand at collection time."

Minute 4 — container hygiene
  "testcontainers-python for integration; DockerCompose fixture at
   session scope with a wait_for health-check. Under xdist, I group
   by --xdist-group so all tests hitting the same container land on
   the same worker; container port is randomised via published_port=0."

Minute 5 — CI reproducibility + flaky discipline
  "GitHub Actions matrix on 3.11/3.12; pytest -n auto with
   --cov-fail-under=85; --junit-xml uploaded to a flaky-test dashboard.
   Flaky tests get @pytest.mark.flaky(reruns=2) plus a quarantine
   label; a nightly job retriages them so nothing stays flaky forever."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Minute 1 is the crucial framing. Naming the four layers immediately — "unit, contract, component-with-DB, integration-with-compose" — signals you're a strategy-shaper, not a task-runner. Weak candidates dive into tools ("we'd use pytest-mock and…") before naming the layers.&lt;/li&gt;
&lt;li&gt;Minute 2 addresses fixture scope before the interviewer asks. This preempts the common trap where you commit to "all fixtures at function scope" and then have to defend a 40-minute test run. Naming session/module/function up front is senior signal.&lt;/li&gt;
&lt;li&gt;Minute 3 is the parametrization probe. Every senior DE test suite is 80% parametrized cases; the fluent answer names &lt;code&gt;ids=&lt;/code&gt;, &lt;code&gt;indirect=True&lt;/code&gt;, and &lt;code&gt;pytest_generate_tests&lt;/code&gt; unprompted.&lt;/li&gt;
&lt;li&gt;Minute 4 is the container hygiene question. Naming &lt;code&gt;testcontainers-python&lt;/code&gt; and the &lt;code&gt;wait_for&lt;/code&gt; health-check pattern shows you've built the pattern, not read about it. Naming &lt;code&gt;--xdist-group&lt;/code&gt; shows you've run it under parallel CI.&lt;/li&gt;
&lt;li&gt;Minute 5 covers CI and flakes — the reliability axis. The rerunfailures + quarantine + nightly retriage story shows you've inherited a flaky suite and rescued it, which is the operational credential senior teams hire for.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading criterion&lt;/th&gt;
&lt;th&gt;Weak score&lt;/th&gt;
&lt;th&gt;Senior score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Names four layers in minute 1&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names fixture scope discipline&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names parametrize + ids&lt;/td&gt;
&lt;td&gt;occasional&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names testcontainers-python&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names flaky discipline&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The senior pytest-for-DE answer is a 5-minute monologue that covers all four axes without waiting for the follow-ups. Rehearse it once; deploy it every time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the "pick the test layer" decision tree
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Given a new piece of code, the senior engineer runs a 4-question decision tree to place it on the right test layer. Codifying the tree makes the interview answer reproducible: any interviewer can hand you a code snippet and you can walk the tree out loud. Walk through the tree with three canonical inputs: a pure &lt;code&gt;normalize_phone&lt;/code&gt; function, a SQL-emitting &lt;code&gt;find_orders_in_window&lt;/code&gt; helper, and an Airflow DAG that reads from Postgres and writes to S3.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Q1.&lt;/strong&gt; Does the code touch any I/O (network, disk, DB)? → no = Layer 1 unit; yes = go to Q2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q2.&lt;/strong&gt; Is the I/O purely SQL against one database? → yes = Layer 3 component with &lt;code&gt;pytest-postgresql&lt;/code&gt;; no = go to Q3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q3.&lt;/strong&gt; Does the code coordinate multiple services (broker, DB, storage)? → yes = Layer 4 integration with compose; no = mock the one external dependency with &lt;code&gt;pytest-mock&lt;/code&gt; and stay at Layer 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q4 (parallel branch).&lt;/strong&gt; Is the code a schema definition (dbt model, pydantic model, JSON schema)? → yes = Layer 2 contract with parametrized assertion; N/A to Q1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Walk the decision tree for the three code snippets and record the layer each ends up on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Q1 (I/O?)&lt;/th&gt;
&lt;th&gt;Q2 (SQL only?)&lt;/th&gt;
&lt;th&gt;Q3 (multi-service?)&lt;/th&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;normalize_phone&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Layer 1 unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;find_orders_in_window&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Layer 3 component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ingest_daily_orders DAG&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Layer 4 integration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Decision-tree helper (illustrative)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pick_test_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;touches_io&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;is_sql_only&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;multi_service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;is_schema_def&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the pytest layer for a code artifact.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_schema_def&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Layer 2 contract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;touches_io&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Layer 1 unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_sql_only&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Layer 3 component&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;multi_service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Layer 4 integration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Layer 1 unit (mock the one external)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="c1"&gt;# Walk the three code snippets
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_test_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# -&amp;gt; Layer 1 unit
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_test_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# -&amp;gt; Layer 3 component
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_test_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# -&amp;gt; Layer 4 integration
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_test_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# -&amp;gt; Layer 2 contract
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scenario 1 — &lt;code&gt;normalize_phone(raw: str) -&amp;gt; str&lt;/code&gt; is a pure function with no I/O. The tree short-circuits at Q1 → Layer 1 unit. Sub-millisecond, hundreds of these, run on save.&lt;/li&gt;
&lt;li&gt;Scenario 2 — &lt;code&gt;find_orders_in_window(conn, start, end)&lt;/code&gt; executes SQL against Postgres and returns rows. Q1 = yes, Q2 = yes → Layer 3 component with &lt;code&gt;pytest-postgresql&lt;/code&gt;. Per-test template DB clone; ~100 ms; catches window-boundary bugs unit tests cannot.&lt;/li&gt;
&lt;li&gt;Scenario 3 — &lt;code&gt;ingest_daily_orders_dag&lt;/code&gt; reads from Postgres, writes JSONL to MinIO, publishes a completion event to Kafka. Q1 = yes, Q2 = no, Q3 = yes → Layer 4 integration with &lt;code&gt;testcontainers-python&lt;/code&gt;. Session-scoped compose; ~10 s; catches inter-service failures.&lt;/li&gt;
&lt;li&gt;Scenario 4 — a &lt;code&gt;pydantic.BaseModel&lt;/code&gt; for the API payload → Layer 2 contract. Parametrized &lt;code&gt;test_payload_shape[orders-good]&lt;/code&gt;, &lt;code&gt;test_payload_shape[orders-missing-total]&lt;/code&gt;, etc. Fast; deterministic.&lt;/li&gt;
&lt;li&gt;If the code fails Q1-Q3 but interacts with exactly one external dependency (say, an HTTP API), mock the dependency with &lt;code&gt;pytest-mock&lt;/code&gt;'s &lt;code&gt;mocker.patch(...)&lt;/code&gt; and keep it at Layer 1. This preserves the pyramid shape — thousands of unit tests, dozens of integration tests, not the inverted pyramid that kills CI budgets.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Fixture needed&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;normalize_phone&lt;/td&gt;
&lt;td&gt;Layer 1 unit&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;&amp;lt;1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;find_orders_in_window&lt;/td&gt;
&lt;td&gt;Layer 3 component&lt;/td&gt;
&lt;td&gt;postgresql (function-scope)&lt;/td&gt;
&lt;td&gt;~100 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ingest_daily_orders_dag&lt;/td&gt;
&lt;td&gt;Layer 4 integration&lt;/td&gt;
&lt;td&gt;compose_stack (session-scope)&lt;/td&gt;
&lt;td&gt;~10 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OrdersPayload (pydantic)&lt;/td&gt;
&lt;td&gt;Layer 2 contract&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;&amp;lt;1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The four-question decision tree is a whiteboard-friendly answer. Practice walking it end-to-end so an interviewer can hand you any code snippet and get a layer name in under 30 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on pytest test-layer strategy
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You inherit a data platform with 900 tests, most of them Layer 1 unit tests with &lt;code&gt;unittest.mock&lt;/code&gt; mocking Postgres, and a CI run that takes 25 minutes because the 40 integration tests each spin up their own docker-compose. Walk me through the test-layer strategy you'd migrate to, the fixture-scope changes, and the CI wiring you'd expect to bring the run under 8 minutes."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a four-layer pytest pyramid with session-scoped compose + template-DB fixtures + xdist parallelism
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. tests/conftest.py — one fixture graph for all four layers
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pytest_postgresql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;

&lt;span class="c1"&gt;# Layer 3 — pytest-postgresql: one process per session, fresh DB per test
&lt;/span&gt;&lt;span class="n"&gt;postgresql_proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postgresql_proc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;# OS-allocated port (xdist-safe)
&lt;/span&gt;    &lt;span class="n"&gt;unixsocketdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;  &lt;span class="c1"&gt;# template DB is pre-seeded
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;postgresql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql_proc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;                        &lt;span class="c1"&gt;# per-test DB is a template clone
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# Layer 4 — testcontainers: one compose stack per session
&lt;/span&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compose_stack&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;testcontainers.compose&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DockerCompose&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DockerCompose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;compose_file_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose.test.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pull&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9001/minio/health/ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Also wait for Postgres + Kafka
&lt;/span&gt;        &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9092/kafka-health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. tests/unit/test_normalize.py — Layer 1
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mypkg.transforms&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;normalize_phone&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw,expected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+1 415-555-1212&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+14155551212&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;415.555.1212&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+14155551212&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(415) 555-1212&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+14155551212&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plus-space&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dot-sep&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paren-sep&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_normalize_phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;normalize_phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 3. tests/component/test_find_orders.py — Layer 3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mypkg.orders&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;find_orders_in_window&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_find_orders_in_window_inclusive_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Window is [start, end) — start is inclusive, end is exclusive.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO orders(id, created_at) VALUES (1, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2026-01-01 00:00:00&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO orders(id, created_at) VALUES (2, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2026-01-02 00:00:00&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_orders_in_window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01-01 00:00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;end&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01-02 00:00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# id=1 is inside; id=2 is boundary-exclusive
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 4. tests/integration/test_dag_end_to_end.py — Layer 4
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.mark.integration&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_ingest_daily_orders_dag_end_to_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compose_stack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Full DAG: read Postgres, write MinIO, publish Kafka completion.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mypkg.dags.ingest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ingest_daily_orders_dag&lt;/span&gt;

    &lt;span class="c1"&gt;# (arrange test data in compose_stack.postgres; elided for brevity)
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ingest_daily_orders_dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# Assert MinIO object landed
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;compose_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_service_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="c1"&gt;# Assert Kafka completion event
&lt;/span&gt;    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 5. .github/workflows/tests.yml — CI matrix&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pytest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-24.04&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;python&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.11"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;matrix.python&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -e '.[test]'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pytest \&lt;/span&gt;
            &lt;span class="s"&gt;-n auto \&lt;/span&gt;
            &lt;span class="s"&gt;--dist=loadgroup \&lt;/span&gt;
            &lt;span class="s"&gt;--cov=mypkg \&lt;/span&gt;
            &lt;span class="s"&gt;--cov-branch \&lt;/span&gt;
            &lt;span class="s"&gt;--cov-fail-under=85 \&lt;/span&gt;
            &lt;span class="s"&gt;--junit-xml=junit.xml \&lt;/span&gt;
            &lt;span class="s"&gt;tests/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;junit-$&lt;/span&gt;&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;matrix.python&lt;/span&gt; &lt;span class="pi"&gt;}},&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;junit.xml&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Before (mock-heavy, per-test compose)&lt;/th&gt;
&lt;th&gt;After (four-layer pyramid, session compose + xdist)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit tests&lt;/td&gt;
&lt;td&gt;900 heavy mocks&lt;/td&gt;
&lt;td&gt;800 pure-function tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contract tests&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;300 parametrized schema tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component tests&lt;/td&gt;
&lt;td&gt;0 (mocked)&lt;/td&gt;
&lt;td&gt;150 pytest-postgresql tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration tests&lt;/td&gt;
&lt;td&gt;40 per-test compose&lt;/td&gt;
&lt;td&gt;40 sharing one session compose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres startup&lt;/td&gt;
&lt;td&gt;none (all mocked)&lt;/td&gt;
&lt;td&gt;one per session (~1 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compose startup&lt;/td&gt;
&lt;td&gt;40 × ~10 s = 6.7 min&lt;/td&gt;
&lt;td&gt;1 × ~10 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallelism&lt;/td&gt;
&lt;td&gt;serial&lt;/td&gt;
&lt;td&gt;xdist -n auto (8 workers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total CI time&lt;/td&gt;
&lt;td&gt;~25 min&lt;/td&gt;
&lt;td&gt;~7 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the migration, the suite shape has &lt;em&gt;inverted&lt;/em&gt; from mock-heavy (unit tests full of &lt;code&gt;mocker.patch("psycopg2.connect")&lt;/code&gt; that never caught the off-by-one window bug) to real-behaviour-heavy (component tests against a real Postgres that catch it every time), and the CI budget has dropped from 25 minutes to 7 minutes despite adding 450 tests. The &lt;code&gt;pytest-xdist&lt;/code&gt; parallelism amortises the compose startup across all workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total tests&lt;/td&gt;
&lt;td&gt;940&lt;/td&gt;
&lt;td&gt;1,290&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI runtime&lt;/td&gt;
&lt;td&gt;25 min&lt;/td&gt;
&lt;td&gt;7 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres coverage&lt;/td&gt;
&lt;td&gt;mocked (0% real)&lt;/td&gt;
&lt;td&gt;100% real behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compose spin-ups&lt;/td&gt;
&lt;td&gt;40 per run&lt;/td&gt;
&lt;td&gt;1 per run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flakes / week&lt;/td&gt;
&lt;td&gt;~15&lt;/td&gt;
&lt;td&gt;~1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug catch rate on PR&lt;/td&gt;
&lt;td&gt;~40%&lt;/td&gt;
&lt;td&gt;~85%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Four-layer pyramid&lt;/strong&gt;&lt;/strong&gt; — Unit / Contract / Component-with-DB / Integration-with-compose is the shape the industry converged on. Each layer catches a different class of bug at a different cost. Mixing scopes across layers by accident is the anti-pattern this shape prevents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Session-scoped compose&lt;/strong&gt;&lt;/strong&gt; — The compose stack starts once per pytest run, shared across all Layer-4 tests. This is the ~40-minute-to-40-second win. The &lt;code&gt;wait_for&lt;/code&gt; health check makes the fixture reliable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Template-DB cloning&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;pytest-postgresql&lt;/code&gt; runs one Postgres process per session, but every test gets a fresh DB by cloning the template. Cloning is ~100 ms; starting a fresh Postgres is ~1 s. This is what makes 150 Layer-3 tests feasible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;xdist parallelism&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;-n auto&lt;/code&gt; spawns one worker per CPU. Session-scoped fixtures are per-worker, so each worker gets its own Postgres process and the compose stack is shared across workers via &lt;code&gt;--dist=loadgroup&lt;/code&gt;. This is the ~7-minute-to-~2-minute latent win when hardware allows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one Postgres binary (~50 MB), one compose stack (~2 GB with Postgres + Kafka + MinIO), coverage tooling (&lt;code&gt;pytest-cov&lt;/code&gt; ~3% CPU overhead), and xdist (one worker per CPU). The eliminated cost is the 6.7 minutes of compose spin-ups and the false confidence of mock-heavy unit tests. Net O(1) startup per run versus O(N) startup per test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Python&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — python&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Python testing and pytest patterns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/python" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;ETL&lt;/span&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;
&lt;strong&gt;ETL problems on pipeline testing&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Fixtures — session scope, factory patterns, DB seed lifecycles
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;@pytest.fixture&lt;/code&gt; is a dependency-injection graph — and scope discipline is the difference between a 2-minute suite and a 20-minute one
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;pytest fixtures&lt;/code&gt; are a directed dependency-injection graph where every fixture declares its own construction cost via a scope (&lt;code&gt;function&lt;/code&gt; / &lt;code&gt;class&lt;/code&gt; / &lt;code&gt;module&lt;/code&gt; / &lt;code&gt;session&lt;/code&gt;) and its own teardown via &lt;code&gt;yield&lt;/code&gt; or a &lt;code&gt;finalizer&lt;/code&gt;, and the difference between a fast suite and a slow one is almost always a fixture that was silently promoted from &lt;code&gt;session&lt;/code&gt; to &lt;code&gt;function&lt;/code&gt; scope by a well-meaning refactor&lt;/strong&gt;. Every senior data engineer has debugged the "why did the test suite go from 90 s to 12 min overnight" mystery, and roughly nine times out of ten the answer is that someone changed &lt;code&gt;scope="session"&lt;/code&gt; to the default &lt;code&gt;scope="function"&lt;/code&gt; on the Postgres process fixture, and now every one of 400 tests starts its own Postgres.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1zr9wm0pvihfduwruv2h.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1zr9wm0pvihfduwruv2h.jpeg" alt="Iconographic pytest fixtures diagram — a scope-tree with function/module/session tiers, a factory-brick emitting seed-row chips, and a teardown broom glyph on the right." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four scopes for pytest fixtures.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;function&lt;/code&gt; (default).&lt;/strong&gt; Constructed and torn down for every test. Best for data that mutates during the test (a fresh DB row, a temp file, a monkeypatched env var). Cheapest to reason about; most expensive at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;class&lt;/code&gt;.&lt;/strong&gt; Constructed once per test class, torn down after. Rarely used in modern DE codebases because pytest style discourages &lt;code&gt;class TestFoo:&lt;/code&gt; grouping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;module&lt;/code&gt;.&lt;/strong&gt; Constructed once per test file. Useful when a group of related tests share the same setup — e.g. all tests in &lt;code&gt;test_orders_analytics.py&lt;/code&gt; reuse the same seeded &lt;code&gt;orders&lt;/code&gt; dataset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;session&lt;/code&gt;.&lt;/strong&gt; Constructed once per pytest run, torn down at exit. Mandatory for expensive setup: the Postgres process, the compose stack, the dbt manifest, an HTTP mock server. The rule of thumb: &lt;strong&gt;if setup takes &amp;gt;1 second, it belongs at session scope&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Yield-teardown vs finalizer — pick one style and stick to it.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;yield&lt;/code&gt;-teardown.&lt;/strong&gt; The modern, Pythonic style. &lt;code&gt;yield the_thing&lt;/code&gt; at the point where the test receives the fixture; anything after the &lt;code&gt;yield&lt;/code&gt; runs at teardown. Reads top-to-bottom like a &lt;code&gt;contextmanager&lt;/code&gt;. This is what &amp;gt;95% of 2026 pytest code uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;request.addfinalizer(fn)&lt;/code&gt;.&lt;/strong&gt; The older style. Register a callback that runs at teardown. Useful when teardown depends on runtime state that doesn't exist yet at &lt;code&gt;yield&lt;/code&gt; time (e.g. you need to tear down N containers where N is determined mid-test).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;autouse=True&lt;/code&gt;.&lt;/strong&gt; The footgun. Marks a fixture as automatically active for every test in scope without being explicitly requested. Legitimate use: cross-cutting concerns like resetting a global logger, seeding a UTC timezone, patching &lt;code&gt;datetime.now&lt;/code&gt;. Illegitimate use: silently coupling every test to a Postgres fixture — which then breaks every unit test that doesn't need a DB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The factory-as-fixture pattern — the single most useful pattern for DE test data.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The problem.&lt;/strong&gt; Tests need "an order row for customer X with N line items" — but every test needs a &lt;em&gt;different&lt;/em&gt; shape. A single &lt;code&gt;orders_seed&lt;/code&gt; fixture that returns a hardcoded row is either too specific (only works for one test) or too general (returns a dict every test has to reshape).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pattern.&lt;/strong&gt; The fixture returns a &lt;em&gt;factory function&lt;/em&gt; that tests call with the shape they need. &lt;code&gt;def make_order(customer_id=1, total_cents=1000, status='pending'): ...&lt;/code&gt; — every test calls &lt;code&gt;make_order(status='shipped')&lt;/code&gt; or &lt;code&gt;make_order(customer_id=99)&lt;/code&gt; and gets exactly what it needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it wins.&lt;/strong&gt; Test data stays close to the test that needs it (readability), the factory centralises the DB-write logic (DRY), and the factory can produce multiple rows in one test without re-invoking the fixture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composability.&lt;/strong&gt; Factories compose — &lt;code&gt;make_shipment&lt;/code&gt; can call &lt;code&gt;make_order&lt;/code&gt; internally. This is how you build hierarchical test data without ceremony.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The DB-seed lifecycle — three patterns, one right answer per use case.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Truncate-per-test.&lt;/strong&gt; Fixture truncates every table at teardown. Slow (~50 ms per test); simple; works for small schemas. Suitable for &amp;lt;50 tests total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback-per-test (savepoint).&lt;/strong&gt; Fixture opens a savepoint at setup; rolls back at teardown. Fast (&amp;lt;5 ms per test); requires the test to &lt;em&gt;not&lt;/em&gt; commit; works only when the code under test doesn't call &lt;code&gt;COMMIT&lt;/code&gt;. Suitable for read-only tests and code that uses the injected connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template-DB clone-per-test.&lt;/strong&gt; &lt;code&gt;pytest-postgresql&lt;/code&gt; clones a pre-seeded template DB for every test (~100 ms per test); test gets a fresh DB it can COMMIT into; teardown drops the clone. Suitable for tests that must exercise real commit behaviour. This is the pattern for the majority of DE component tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on pytest fixtures.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What scope should the Postgres fixture be?" — required answer is "the Postgres process at session; the DB itself at function via template-clone."&lt;/li&gt;
&lt;li&gt;"When would you use &lt;code&gt;autouse=True&lt;/code&gt;?" — cross-cutting concerns only; never for I/O fixtures.&lt;/li&gt;
&lt;li&gt;"How do you avoid teardown order bugs?" — declare explicit dependencies (fixture A requests fixture B); pytest tears down in reverse dependency order.&lt;/li&gt;
&lt;li&gt;"How do factory fixtures compose?" — one factory calls another; both are yielded as callables from separate fixtures.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — session-scoped Postgres + function-scoped template DB
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical DE Postgres fixture setup: one Postgres process per pytest session (~1 s startup, amortised across N tests) plus one template-cloned database per test (~100 ms clone, hermetic). This gives real Postgres behaviour with per-test isolation, at ~1/10th the cost of spinning up a fresh Postgres per test. Walk through the fixture wiring.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session fixture.&lt;/strong&gt; &lt;code&gt;postgresql_proc&lt;/code&gt; — one binary, one datadir, one Postgres process. Started once, killed at exit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session fixture.&lt;/strong&gt; &lt;code&gt;template_db&lt;/code&gt; — a database created inside &lt;code&gt;postgresql_proc&lt;/code&gt;, pre-seeded with schema + reference data. Cloned by every function-scoped &lt;code&gt;postgresql&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function fixture.&lt;/strong&gt; &lt;code&gt;postgresql&lt;/code&gt; — a fresh database cloned from &lt;code&gt;template_db&lt;/code&gt; for every test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teardown.&lt;/strong&gt; Function-scoped fixture drops its clone; session-scoped fixture kills the process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Wire the three fixtures and write one Layer-3 component test that inserts a row and asserts the schema is present.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Setup cost&lt;/th&gt;
&lt;th&gt;Teardown&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;postgresql_proc&lt;/td&gt;
&lt;td&gt;session&lt;/td&gt;
&lt;td&gt;~1 s&lt;/td&gt;
&lt;td&gt;kill process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;template_db&lt;/td&gt;
&lt;td&gt;session&lt;/td&gt;
&lt;td&gt;~500 ms (schema load)&lt;/td&gt;
&lt;td&gt;drop DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;postgresql&lt;/td&gt;
&lt;td&gt;function&lt;/td&gt;
&lt;td&gt;~100 ms (clone)&lt;/td&gt;
&lt;td&gt;drop DB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/conftest.py — the three-fixture pattern
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pytest_postgresql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;

&lt;span class="c1"&gt;# Session — one Postgres process for the entire pytest run
&lt;/span&gt;&lt;span class="n"&gt;postgresql_proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postgresql_proc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                          &lt;span class="c1"&gt;# OS-allocated (xdist-safe)
&lt;/span&gt;    &lt;span class="n"&gt;unixsocketdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;                            &lt;span class="c1"&gt;# no schema on the process itself
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Session — the template database, pre-seeded with schema.sql
&lt;/span&gt;&lt;span class="n"&gt;postgresql_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql_proc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Function — a fresh clone of template_db for every test
&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql_proc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- tests/schema.sql — the schema every test starts with&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;   &lt;span class="nb"&gt;BIGINT&lt;/span&gt;       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_cents&lt;/span&gt;   &lt;span class="nb"&gt;BIGINT&lt;/span&gt;       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt;         &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;    &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_status&lt;/span&gt;      &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;      &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt;      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/component/test_orders_schema.py — Layer-3 test
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_orders_schema_has_indexes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Every test gets a fresh DB with the schema pre-loaded.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT indexname
            FROM   pg_indexes
            WHERE  schemaname = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
              AND  tablename  = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
            ORDER  BY indexname
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;indexes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idx_orders_customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;indexes&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idx_orders_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;      &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;indexes&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders_pkey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;            &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;indexes&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_orders_insert_visible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fresh DB per test — no leakage from previous tests.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            INSERT INTO public.orders(customer_id, total_cents)
            VALUES (1, 1000)
            RETURNING id
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) FROM public.orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;     &lt;span class="c1"&gt;# exactly one — no leakage
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;postgresql_proc&lt;/code&gt; starts one real Postgres binary on an OS-allocated port with a temp datadir. The &lt;code&gt;port=None&lt;/code&gt; is critical for xdist safety — every worker gets a distinct port. This runs &lt;em&gt;once per pytest session&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;postgresql_template&lt;/code&gt; (session) creates one database inside that process, loads &lt;code&gt;schema.sql&lt;/code&gt;, and stays around for the duration. It is never mutated during tests — it's the "clean stamp" that every function-scoped clone starts from.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;postgresql&lt;/code&gt; (function) creates a fresh database per test by cloning the template (&lt;code&gt;CREATE DATABASE test_db TEMPLATE template_db&lt;/code&gt;). Cloning is a Postgres-native O(1) operation that copies file blocks; ~100 ms even for a large template.&lt;/li&gt;
&lt;li&gt;Every test receives a &lt;code&gt;psycopg2.Connection&lt;/code&gt; to its own fresh DB. Tests can INSERT / COMMIT / DELETE freely; teardown drops the entire database, so leakage between tests is impossible.&lt;/li&gt;
&lt;li&gt;The alternative — one long-lived DB with a rollback-per-test savepoint — is faster (~5 ms per test) but breaks any code under test that itself calls COMMIT (nested savepoints don't behave the same as top-level transactions). The template-clone pattern is the more forgiving default.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Postgres process&lt;/th&gt;
&lt;th&gt;Template DB&lt;/th&gt;
&lt;th&gt;Function DB&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;test_orders_schema_has_indexes&lt;/td&gt;
&lt;td&gt;reused&lt;/td&gt;
&lt;td&gt;reused&lt;/td&gt;
&lt;td&gt;freshly cloned&lt;/td&gt;
&lt;td&gt;~110 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_orders_insert_visible&lt;/td&gt;
&lt;td&gt;reused&lt;/td&gt;
&lt;td&gt;reused&lt;/td&gt;
&lt;td&gt;freshly cloned&lt;/td&gt;
&lt;td&gt;~115 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(subsequent tests)&lt;/td&gt;
&lt;td&gt;reused&lt;/td&gt;
&lt;td&gt;reused&lt;/td&gt;
&lt;td&gt;freshly cloned&lt;/td&gt;
&lt;td&gt;~100–120 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First test in run&lt;/td&gt;
&lt;td&gt;started&lt;/td&gt;
&lt;td&gt;seeded&lt;/td&gt;
&lt;td&gt;cloned&lt;/td&gt;
&lt;td&gt;~1.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Postgres &lt;em&gt;process&lt;/em&gt; at session scope; the &lt;em&gt;database&lt;/em&gt; at function scope via template clone. Never start a Postgres per test; never share a database across tests.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — factory fixture for dbt seed data
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; DE tests routinely need to seed varied data shapes: "an order with 3 line items", "a customer with a 5-year history", "an inventory row at the safety-stock boundary". A single &lt;code&gt;orders_seed&lt;/code&gt; fixture can't cover all shapes without becoming a config-driven monster. The factory-as-fixture pattern solves this: the fixture yields a &lt;em&gt;callable&lt;/em&gt; that tests invoke with the shape they need. Walk through the pattern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The factory function.&lt;/strong&gt; &lt;code&gt;make_order(customer_id=1, total_cents=1000, status='pending', **kwargs) -&amp;gt; Order&lt;/code&gt;. Every kwarg has a default; tests override only what they care about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fixture that yields the factory.&lt;/strong&gt; Function-scoped — the fixture closes over the DB connection and returns the factory. Test invokes the factory zero or more times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup.&lt;/strong&gt; The DB connection is torn down by the underlying &lt;code&gt;postgresql&lt;/code&gt; fixture; created rows disappear with the cloned DB. No manual cleanup needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composability.&lt;/strong&gt; &lt;code&gt;make_order_with_items(customer_id, items)&lt;/code&gt; can call &lt;code&gt;make_order&lt;/code&gt; internally, then insert line items.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the &lt;code&gt;make_order&lt;/code&gt; factory fixture and a test that uses it to seed three orders with distinct statuses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Factory signature&lt;/td&gt;
&lt;td&gt;make_order(customer_id=1, total_cents=1000, status='pending')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Return type&lt;/td&gt;
&lt;td&gt;dataclass Order(id, customer_id, total_cents, status)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB&lt;/td&gt;
&lt;td&gt;function-scoped postgresql fixture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleanup&lt;/td&gt;
&lt;td&gt;automatic (template-clone drops at teardown)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/conftest.py — factory-as-fixture pattern
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return a factory function that inserts an order and returns it.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                INSERT INTO public.orders(customer_id, total_cents, status)
                VALUES (%s, %s, %s)
                RETURNING id
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_make&lt;/span&gt;        &lt;span class="c1"&gt;# yield the callable, not the row
&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Composable partner factory — used by higher-level make_order_full.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Test User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                INSERT INTO public.customers(email, name)
                VALUES (%s, %s)
                RETURNING id
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;cid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_make&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/component/test_orders_factory.py — using the factories
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_status_filter_returns_only_matching&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Seed three orders; query by status; assert selectivity.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shipped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancelled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) FROM public.orders WHERE status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shipped&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_customer_order_join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;make_customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Compose two factories — customer + order — in one test.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;alice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT c.name, COUNT(o.id)
            FROM   public.customers c
            JOIN   public.orders   o ON o.customer_id = c.id
            GROUP  BY c.name
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;make_order&lt;/code&gt; (fixture) opens a closure over &lt;code&gt;postgresql&lt;/code&gt; (the function-scoped DB connection) and returns an inner &lt;code&gt;_make&lt;/code&gt; function. The &lt;em&gt;fixture itself&lt;/em&gt; runs once per test that requests it; the &lt;em&gt;inner function&lt;/em&gt; can be called any number of times by the test.&lt;/li&gt;
&lt;li&gt;Every call to &lt;code&gt;_make(...)&lt;/code&gt; executes one INSERT with defaulted-or-overridden values, commits, and returns an &lt;code&gt;Order&lt;/code&gt; dataclass carrying the generated &lt;code&gt;id&lt;/code&gt;. The test can compose arbitrary sequences of factory calls.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;make_customer&lt;/code&gt; shows the composability pattern — it also uses a counter closure to generate unique emails so tests don't collide on the &lt;code&gt;UNIQUE(email)&lt;/code&gt; constraint. This is a common factory idiom: side-effect-free defaults with per-call uniqueness.&lt;/li&gt;
&lt;li&gt;In &lt;code&gt;test_customer_order_join&lt;/code&gt;, both factories are requested; pytest resolves both against the same underlying &lt;code&gt;postgresql&lt;/code&gt; connection because that fixture is function-scoped and the two factory fixtures both request it. Fixture-graph sharing is automatic.&lt;/li&gt;
&lt;li&gt;Cleanup is trivial: the underlying &lt;code&gt;postgresql&lt;/code&gt; fixture tears down the entire cloned database at test end, so every row inserted by every factory call vanishes with it. Factories never need explicit cleanup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Factory calls&lt;/th&gt;
&lt;th&gt;DB rows&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;test_status_filter_returns_only_matching&lt;/td&gt;
&lt;td&gt;make_order × 3&lt;/td&gt;
&lt;td&gt;3 orders&lt;/td&gt;
&lt;td&gt;~115 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_customer_order_join&lt;/td&gt;
&lt;td&gt;make_customer × 1, make_order × 2&lt;/td&gt;
&lt;td&gt;1 cust + 2 orders&lt;/td&gt;
&lt;td&gt;~120 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(fresh DB per test)&lt;/td&gt;
&lt;td&gt;inputs isolated&lt;/td&gt;
&lt;td&gt;outputs isolated&lt;/td&gt;
&lt;td&gt;(no leakage)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every DE test suite needs at least one factory per major table. Return a callable, defaulted-and-overridable, closing over the DB fixture. This is the pattern the industry converged on because it scales: 5 factories cover 500 tests worth of data-shape variation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — &lt;code&gt;tmp_path&lt;/code&gt; + &lt;code&gt;monkeypatch&lt;/code&gt; for filesystem and env isolation
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; DE code routinely reads env vars (&lt;code&gt;AWS_REGION&lt;/code&gt;, &lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;KAFKA_BOOTSTRAP&lt;/code&gt;), writes to disk (parquet dumps, JSONL landing), and shells out to external tools (&lt;code&gt;dbt run&lt;/code&gt;). Tests must isolate these side effects: an env var set in test A cannot leak into test B; a file written in test C cannot pollute test D. Pytest ships two built-in fixtures for this: &lt;code&gt;tmp_path&lt;/code&gt; (per-test temp directory that auto-cleans) and &lt;code&gt;monkeypatch&lt;/code&gt; (per-test env-var / attribute patcher that auto-restores).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tmp_path&lt;/code&gt;.&lt;/strong&gt; Yields a &lt;code&gt;pathlib.Path&lt;/code&gt; to a fresh directory. Auto-deleted after the test. Alternative: &lt;code&gt;tmp_path_factory&lt;/code&gt; at session scope for cross-test reuse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;monkeypatch&lt;/code&gt;.&lt;/strong&gt; Method-rich — &lt;code&gt;monkeypatch.setenv&lt;/code&gt;, &lt;code&gt;monkeypatch.setattr&lt;/code&gt;, &lt;code&gt;monkeypatch.delenv&lt;/code&gt;. Every change auto-reverts at teardown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;monkeypatch.chdir&lt;/code&gt;.&lt;/strong&gt; Changes the working directory for the test; reverts at teardown. Critical for dbt tests that assume &lt;code&gt;dbt_project.yml&lt;/code&gt; in cwd.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to reach for these.&lt;/strong&gt; Any test that touches &lt;code&gt;os.environ&lt;/code&gt;, &lt;code&gt;os.chdir&lt;/code&gt;, &lt;code&gt;pathlib.Path&lt;/code&gt; for writes, or global module-level attributes. Never modify these directly — always through the fixture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a test that reads env vars for its DB config, writes a JSONL landing file, and asserts both without leaking to sibling tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Fixture&lt;/th&gt;
&lt;th&gt;Restoration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DATABASE_URL env var&lt;/td&gt;
&lt;td&gt;monkeypatch.setenv&lt;/td&gt;
&lt;td&gt;automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KAFKA_BOOTSTRAP env var&lt;/td&gt;
&lt;td&gt;monkeypatch.setenv&lt;/td&gt;
&lt;td&gt;automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/tmp/landing directory&lt;/td&gt;
&lt;td&gt;tmp_path&lt;/td&gt;
&lt;td&gt;auto-deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cwd for dbt run&lt;/td&gt;
&lt;td&gt;monkeypatch.chdir&lt;/td&gt;
&lt;td&gt;reverted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/component/test_env_and_fs.py — tmp_path + monkeypatch
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mypkg.landing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;write_orders_landing&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_write_orders_landing_creates_jsonl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Isolated env + filesystem; every assertion is post-test cleaned.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Env — inject config the code reads via os.environ
&lt;/span&gt;    &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LANDING_ROOT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-west-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Filesystem — everything under tmp_path is auto-cleaned
&lt;/span&gt;    &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_cents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_cents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2500&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Exercise the code under test
&lt;/span&gt;    &lt;span class="n"&gt;output_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;write_orders_landing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 4. Assert the file materialised where we expected
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_cents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_env_var_does_not_leak_to_next_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Sanity check — env from the previous test must be gone.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LANDING_ROOT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;      &lt;span class="c1"&gt;# cleared by monkeypatch teardown
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-west-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# or wasn't set at all
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_dbt_test_uses_chdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;dbt requires cwd == project root; monkeypatch.chdir reverts.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Create a fake dbt_project.yml in tmp_path
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_project.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name: test_project&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;version: 1.0&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Anything the code does with Path.cwd() sees tmp_path
&lt;/span&gt;    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt;
    &lt;span class="c1"&gt;# After the test, cwd reverts automatically
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;tmp_path&lt;/code&gt; is a function-scoped built-in fixture that yields a fresh temp dir. Pytest deletes the directory at test end (subject to the &lt;code&gt;--basetemp&lt;/code&gt; retention setting, typically "keep the last 3"). This makes filesystem tests hermetic by default.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;monkeypatch.setenv("LANDING_ROOT", str(tmp_path))&lt;/code&gt; injects the env var &lt;em&gt;for the duration of this test only&lt;/em&gt;. Pytest records the original value at set time and restores it at teardown — no test can leak an env var into a sibling.&lt;/li&gt;
&lt;li&gt;Step 3 exercises &lt;code&gt;write_orders_landing&lt;/code&gt;, which reads &lt;code&gt;LANDING_ROOT&lt;/code&gt; from &lt;code&gt;os.environ&lt;/code&gt; and writes &lt;code&gt;orders.jsonl&lt;/code&gt; under it. Because both the env var and the target directory are per-test, this test can be re-run under &lt;code&gt;pytest-xdist&lt;/code&gt; in parallel with 10 copies of itself without collision.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_env_var_does_not_leak_to_next_test&lt;/code&gt; is the sanity check — proves &lt;code&gt;monkeypatch&lt;/code&gt; cleaned up. In CI, this pattern is worth its weight in gold for catching accidental global-state leakage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_dbt_test_uses_chdir&lt;/code&gt; shows the &lt;code&gt;monkeypatch.chdir&lt;/code&gt; idiom for tools that require a specific working directory. Without this, sibling tests would start in the wrong cwd and fail cryptically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;tmp_path&lt;/th&gt;
&lt;th&gt;env vars set&lt;/th&gt;
&lt;th&gt;env vars visible after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;test_write_orders_landing&lt;/td&gt;
&lt;td&gt;/tmp/pytest-.../test_0&lt;/td&gt;
&lt;td&gt;LANDING_ROOT, AWS_REGION&lt;/td&gt;
&lt;td&gt;(cleared)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_env_var_does_not_leak&lt;/td&gt;
&lt;td&gt;/tmp/pytest-.../test_1&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;(still cleared)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_dbt_test_uses_chdir&lt;/td&gt;
&lt;td&gt;/tmp/pytest-.../test_2&lt;/td&gt;
&lt;td&gt;(none, chdir only)&lt;/td&gt;
&lt;td&gt;cwd restored&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any test that touches env vars, cwd, or filesystem writes, use &lt;code&gt;monkeypatch&lt;/code&gt; and &lt;code&gt;tmp_path&lt;/code&gt; — never modify globals directly and never rely on test-ordered cleanup. Test isolation is non-negotiable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on fixture scope discipline
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You inherit a suite with 400 pytest tests where every test spins up its own Postgres via &lt;code&gt;docker.run(...)&lt;/code&gt; in a function-scoped fixture. Full CI run takes 42 minutes. Walk me through the fixture-scope migration you'd do, the template-DB pattern, the factory pattern for test data, and the exact time budget you'd expect on 8-core CI."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using session-scoped Postgres process + template-clone per test + factory fixtures
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. tests/conftest.py — session Postgres, function template clone, factories
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pytest_postgresql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;


&lt;span class="c1"&gt;# ------------------------------------------------------------------
# Session — one Postgres binary for the whole test run
# ------------------------------------------------------------------
&lt;/span&gt;&lt;span class="n"&gt;postgresql_proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postgresql_proc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                          &lt;span class="c1"&gt;# OS-allocated port; xdist-safe
&lt;/span&gt;    &lt;span class="n"&gt;unixsocketdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;postgres_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c fsync=off &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# turn off durability for speed
&lt;/span&gt;                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c synchronous_commit=off &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c full_page_writes=off&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# ------------------------------------------------------------------
# Function — a fresh clone of the schema-loaded template
# ------------------------------------------------------------------
&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql_proc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# ------------------------------------------------------------------
# Factory — reusable data shape generator
# ------------------------------------------------------------------
&lt;/span&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Callable that inserts an order with defaulted overrides.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                INSERT INTO public.orders(customer_id, total_cents, status)
                VALUES (%s, %s, %s) RETURNING id
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_make&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. A representative Layer-3 test using both fixtures
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_bulk_status_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Seed 5 pending orders; bulk-update to shipped; assert count.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;make_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            UPDATE public.orders
            SET    status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shipped&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
            WHERE  status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rowcount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rowcount&lt;/span&gt;
    &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;rowcount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) FROM public.orders WHERE status=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shipped&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# 3. pytest.ini — test-discovery + speed-critical options
&lt;/span&gt;&lt;span class="nn"&gt;[pytest]&lt;/span&gt;
&lt;span class="py"&gt;addopts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;-ra -q --strict-markers --strict-config&lt;/span&gt;
&lt;span class="py"&gt;markers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="err"&gt;integration:&lt;/span&gt; &lt;span class="err"&gt;end-to-end&lt;/span&gt; &lt;span class="err"&gt;tests&lt;/span&gt; &lt;span class="err"&gt;that&lt;/span&gt; &lt;span class="err"&gt;require&lt;/span&gt; &lt;span class="err"&gt;docker-compose&lt;/span&gt;
    &lt;span class="err"&gt;slow:&lt;/span&gt; &lt;span class="err"&gt;tests&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;1&lt;/span&gt; &lt;span class="err"&gt;s&lt;/span&gt; &lt;span class="err"&gt;that&lt;/span&gt; &lt;span class="err"&gt;we&lt;/span&gt; &lt;span class="err"&gt;want&lt;/span&gt; &lt;span class="err"&gt;to&lt;/span&gt; &lt;span class="err"&gt;skip&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;the&lt;/span&gt; &lt;span class="err"&gt;fast&lt;/span&gt; &lt;span class="err"&gt;lane&lt;/span&gt;
&lt;span class="py"&gt;testpaths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Before (function-scope Postgres)&lt;/th&gt;
&lt;th&gt;After (session + template clone)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres startup&lt;/td&gt;
&lt;td&gt;400 × ~1 s = 6.7 min&lt;/td&gt;
&lt;td&gt;1 × ~1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB clone&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;400 × ~100 ms = ~40 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema load&lt;/td&gt;
&lt;td&gt;400 × ~200 ms = 80 s&lt;/td&gt;
&lt;td&gt;1 × ~200 ms (into template)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test body&lt;/td&gt;
&lt;td&gt;400 × ~50 ms = 20 s&lt;/td&gt;
&lt;td&gt;400 × ~50 ms = 20 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full run (serial)&lt;/td&gt;
&lt;td&gt;~42 min&lt;/td&gt;
&lt;td&gt;~2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full run (8-core xdist)&lt;/td&gt;
&lt;td&gt;~6 min&lt;/td&gt;
&lt;td&gt;~30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the migration, the same 400 tests run in 2 minutes serially and 30 seconds with 8-core xdist. The Postgres process starts once per session (per xdist worker), the template DB is created once with the schema, and every test gets a fresh clone via Postgres's native &lt;code&gt;CREATE DATABASE ... TEMPLATE&lt;/code&gt; machinery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CI runtime (serial)&lt;/td&gt;
&lt;td&gt;42 min&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI runtime (8-core xdist)&lt;/td&gt;
&lt;td&gt;~6 min&lt;/td&gt;
&lt;td&gt;~30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres binary starts&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;1 (× N workers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker image pulls&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;0 (pytest-postgresql uses local binary)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test isolation&lt;/td&gt;
&lt;td&gt;full (own container)&lt;/td&gt;
&lt;td&gt;full (own DB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory pressure&lt;/td&gt;
&lt;td&gt;~400 × 100 MB = 40 GB peak&lt;/td&gt;
&lt;td&gt;~1 × 100 MB per worker&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Session-scoped process fixture&lt;/strong&gt;&lt;/strong&gt; — the Postgres binary starts once and is reused. Amortises ~1 s of startup across all N tests. This is the O(1) versus O(N) win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Template-DB clone&lt;/strong&gt;&lt;/strong&gt; — Postgres's &lt;code&gt;CREATE DATABASE ... TEMPLATE&lt;/code&gt; is a file-block copy at the OS level. ~100 ms even for GB-scale schemas. Every test gets a fresh DB with the schema pre-loaded, no per-test schema loading needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Speed-critical Postgres flags&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;fsync=off&lt;/code&gt;, &lt;code&gt;synchronous_commit=off&lt;/code&gt;, &lt;code&gt;full_page_writes=off&lt;/code&gt; trade durability for speed. Safe in tests because the DB is thrown away at exit. Cuts commit latency by ~5×.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Factory-as-fixture&lt;/strong&gt;&lt;/strong&gt; — one factory per major table, closure over &lt;code&gt;postgresql&lt;/code&gt;, callable defaults. Scales to arbitrary data shapes without exploding the fixture count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one Postgres process per xdist worker (~100 MB RAM), one template DB (~50 MB), ~100 ms per test for the clone. The eliminated cost is 400 container startups + 400 schema loads = ~7 minutes. Net O(1) startup per worker versus O(N) startup per test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Python&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — oop/python&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;OOP + fixtures + factories in Python&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/oop/python" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;
&lt;strong&gt;SQL problems on schema seeding and test data&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Parametrization — table-driven tests over dbt/Spark transforms
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;@pytest.mark.parametrize&lt;/code&gt; turns a 100-test suite into a 3-file suite — as long as your &lt;code&gt;ids=&lt;/code&gt; are readable
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;pytest parametrize&lt;/code&gt; is a decorator that runs the same test function once per row of a parameter table, giving you N logical tests for the cost of one function definition — but the readability of the resulting test IDs (&lt;code&gt;test_transform[null-input]&lt;/code&gt; vs &lt;code&gt;test_transform[0]&lt;/code&gt;) is the single largest predictor of whether your team maintains the parametrized style or refactors it into 30 copy-pasted test functions six months later&lt;/strong&gt;. Every senior data engineer has inherited a suite with &lt;code&gt;test_transform[0]&lt;/code&gt; through &lt;code&gt;test_transform[47]&lt;/code&gt; — impossible to grep for the failing case — and every senior data engineer has replaced it with &lt;code&gt;ids=["null-input", "empty-str", "unicode-emoji", ...]&lt;/code&gt; in a two-hour refactor that pays for itself the first time a single case fails.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyszs4gkmzwt0tnc997u6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyszs4gkmzwt0tnc997u6.jpeg" alt="Iconographic pytest parametrize diagram — a table-driven grid with rows of test-cases feeding a single test-function card, plus an @pytest.mark.parametrize chip and dbt/Spark transform icons." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four flavours of parametrization every DE engineer uses.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static &lt;code&gt;@pytest.mark.parametrize&lt;/code&gt;.&lt;/strong&gt; The classic — a list of tuples decorating the test function. Best when the test cases are known at collection time and small enough to inline. This covers ~70% of DE parametrized tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@pytest.mark.parametrize(..., indirect=True)&lt;/code&gt;.&lt;/strong&gt; Passes the parameter through a fixture instead of directly to the test. Useful when the parameter needs &lt;em&gt;setup&lt;/em&gt; — e.g. parametrizing which DB backend a test runs against (Postgres vs DuckDB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixture-level parametrization (&lt;code&gt;@pytest.fixture(params=...)&lt;/code&gt;).&lt;/strong&gt; Parametrizes the fixture itself; every test that requests the fixture runs once per param. Powerful for "run the whole suite against multiple backends" — but easy to over-use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic &lt;code&gt;pytest_generate_tests&lt;/code&gt;.&lt;/strong&gt; The hook — pytest calls it during collection and you supply the params programmatically. Essential for "run this contract test once per model in the dbt manifest" or "once per table in the schema" — where the axes are discovered at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Axes senior DEs parametrize over — the canonical five.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Null / empty / edge / valid / invalid.&lt;/strong&gt; The base axis for any transform. Every column that can be null must have a null-case test; every string must have empty and unicode-emoji cases; every numeric must have zero, negative, and overflow cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend.&lt;/strong&gt; For code that must work on Postgres, MySQL, Snowflake, DuckDB — parametrize the fixture; run the same test across all backends. Catches dialect-specific SQL bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time boundary.&lt;/strong&gt; For window/session logic — &lt;code&gt;[start-inclusive, end-exclusive, span-crossing-midnight, span-crossing-DST, single-instant]&lt;/code&gt;. This axis catches roughly half of all off-by-one bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume.&lt;/strong&gt; For batch code — &lt;code&gt;[1-row, 100-rows, 10k-rows]&lt;/code&gt; cases parametrized over the same transform. The 10k case catches memory-explosion bugs the 100 case doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifest.&lt;/strong&gt; For contract tests — parametrize over every model / column / test / source in the dbt manifest, or every table in the information_schema. Amplifies one test into hundreds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;ids=&lt;/code&gt; discipline — the single most important pytest hygiene rule.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bad.&lt;/strong&gt; &lt;code&gt;@pytest.mark.parametrize("x,y", [(1, 2), (3, 4), (5, 6)])&lt;/code&gt; → test IDs &lt;code&gt;test_foo[1-2]&lt;/code&gt;, &lt;code&gt;test_foo[3-4]&lt;/code&gt;, &lt;code&gt;test_foo[5-6]&lt;/code&gt;. Grep-friendly &lt;em&gt;only&lt;/em&gt; if your values are unique and semantic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better.&lt;/strong&gt; &lt;code&gt;@pytest.mark.parametrize("x,y", [(1, 2), (3, 4)], ids=["simple-case", "boundary-case"])&lt;/code&gt; → readable IDs; the failing case's meaning is obvious from the test name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best.&lt;/strong&gt; Use a helper that generates IDs from a dataclass — &lt;code&gt;ids=lambda p: f"{p.name}-{p.expected}"&lt;/code&gt;. Scales to hundreds of cases without hand-writing IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never.&lt;/strong&gt; Do not omit &lt;code&gt;ids=&lt;/code&gt; for parametrizations with more than 3 cases. Every failing test that reports &lt;code&gt;test_foo[test_foo0]&lt;/code&gt; is a debugging tax.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on pytest parametrize.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you parametrize a test?" — required answer: &lt;code&gt;@pytest.mark.parametrize&lt;/code&gt; with &lt;code&gt;ids=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"What does &lt;code&gt;indirect=True&lt;/code&gt; do?" — routes the param through a fixture, useful for parameterizing setup.&lt;/li&gt;
&lt;li&gt;"How do you parametrize dynamically at collection time?" — &lt;code&gt;pytest_generate_tests&lt;/code&gt; hook.&lt;/li&gt;
&lt;li&gt;"What's the tradeoff between fixture-level params and decorator-level params?" — fixture-level multiplies every test in scope; decorator-level scopes to the specific test.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — Spark UDF parametrized across null/edge/valid inputs
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A Spark UDF that normalises phone numbers is a canonical parametrization target. The correctness contract has ~6 axes: normal input, leading whitespace, embedded punctuation, null input, empty string, malformed input. Six tests × one function = six lines of parametrize data. Walk through the pattern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Function under test.&lt;/strong&gt; &lt;code&gt;normalize_phone(raw: str | None) -&amp;gt; str | None&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Axes.&lt;/strong&gt; null → null; empty → null; valid → digits-only; punctuation → digits-only; unicode → error; too-short → error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ID strategy.&lt;/strong&gt; Human-readable names describing the axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert.&lt;/strong&gt; Exact-match or exception; no wiggle room.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the parametrized test covering all six axes with human-readable IDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;raw input&lt;/th&gt;
&lt;th&gt;expected output&lt;/th&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;null-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty&lt;/td&gt;
&lt;td&gt;""&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;empty-str&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;"+1 415-555-1212"&lt;/td&gt;
&lt;td&gt;"+14155551212"&lt;/td&gt;
&lt;td&gt;valid-with-punct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;digits&lt;/td&gt;
&lt;td&gt;"4155551212"&lt;/td&gt;
&lt;td&gt;"+14155551212"&lt;/td&gt;
&lt;td&gt;digits-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unicode&lt;/td&gt;
&lt;td&gt;"📱4155551212"&lt;/td&gt;
&lt;td&gt;ValueError&lt;/td&gt;
&lt;td&gt;unicode-emoji&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;short&lt;/td&gt;
&lt;td&gt;"555"&lt;/td&gt;
&lt;td&gt;ValueError&lt;/td&gt;
&lt;td&gt;too-short&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/unit/test_normalize_phone.py — parametrized Spark UDF test
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mypkg.udfs.phone&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;normalize_phone&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw,expected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+1 415-555-1212&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+14155551212&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4155551212&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+14155551212&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;null-in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty-str&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid-with-punct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;digits-only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_normalize_phone_valid_inputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Six valid-or-null inputs; exact-match against expected.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;normalize_phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;📱4155551212&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;555&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not-a-phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unicode-emoji&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;too-short&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totally-garbage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_normalize_phone_raises_on_invalid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Three invalid inputs; must raise ValueError.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;normalize_phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pytest collects &lt;code&gt;test_normalize_phone_valid_inputs&lt;/code&gt; four times — once per row in the parametrize table. Each collection produces a distinct test node with a distinct ID: &lt;code&gt;test_normalize_phone_valid_inputs[null-in]&lt;/code&gt;, &lt;code&gt;test_normalize_phone_valid_inputs[empty-str]&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;When a test fails, the pytest report shows the ID — so a regression in the punctuation-handling case shows up as &lt;code&gt;FAILED tests/unit/test_normalize_phone.py::test_normalize_phone_valid_inputs[valid-with-punct]&lt;/code&gt;. You know exactly which axis broke without running the debugger.&lt;/li&gt;
&lt;li&gt;Splitting valid-vs-invalid into two parametrize decorators is idiomatic — the assertion style differs (equality vs &lt;code&gt;pytest.raises&lt;/code&gt;), so one function per assertion style keeps each test focused.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pytest.raises(ValueError)&lt;/code&gt; is a context manager that asserts the block raises that exception (or a subclass). Using it inside a parametrized loop tests three invalid inputs with one function.&lt;/li&gt;
&lt;li&gt;If the UDF later gains a "return &lt;code&gt;None&lt;/code&gt; for whitespace-only input" behaviour, adding a row &lt;code&gt;("   ", None, "whitespace-only")&lt;/code&gt; takes one line — no new test function, no fixture change. This is the parametrization amplifier at work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test ID&lt;/th&gt;
&lt;th&gt;raw&lt;/th&gt;
&lt;th&gt;expected&lt;/th&gt;
&lt;th&gt;actual&lt;/th&gt;
&lt;th&gt;pass/fail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;test_normalize_phone_valid_inputs[null-in]&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_normalize_phone_valid_inputs[empty-str]&lt;/td&gt;
&lt;td&gt;""&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_normalize_phone_valid_inputs[valid-with-punct]&lt;/td&gt;
&lt;td&gt;"+1 415-555-1212"&lt;/td&gt;
&lt;td&gt;"+14155551212"&lt;/td&gt;
&lt;td&gt;"+14155551212"&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_normalize_phone_valid_inputs[digits-only]&lt;/td&gt;
&lt;td&gt;"4155551212"&lt;/td&gt;
&lt;td&gt;"+14155551212"&lt;/td&gt;
&lt;td&gt;"+14155551212"&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_normalize_phone_raises_on_invalid[unicode-emoji]&lt;/td&gt;
&lt;td&gt;"📱..."&lt;/td&gt;
&lt;td&gt;ValueError&lt;/td&gt;
&lt;td&gt;ValueError&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_normalize_phone_raises_on_invalid[too-short]&lt;/td&gt;
&lt;td&gt;"555"&lt;/td&gt;
&lt;td&gt;ValueError&lt;/td&gt;
&lt;td&gt;ValueError&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every non-trivial transform gets a null/empty/valid/edge/invalid parametrization with &lt;code&gt;ids=&lt;/code&gt;. Six lines of table data beats six copy-pasted test functions every time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — dbt model parametrized over source-fixture rows
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A dbt model that computes &lt;code&gt;dim_customer_lifetime_value&lt;/code&gt; from raw orders needs to be tested against several source-row patterns: customer with one order, customer with 100 orders, customer with zero orders (should return zero LTV), customer with a returned order (should net out). A single parametrized test can seed the source data and assert the model output per case. Walk through the pattern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source rows.&lt;/strong&gt; Parametrized dicts describing customer + order data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model.&lt;/strong&gt; &lt;code&gt;dim_customer_lifetime_value.sql&lt;/code&gt; — SUM(revenue) - SUM(returns) per customer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assertion.&lt;/strong&gt; Materialised model row equals expected LTV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup.&lt;/strong&gt; Each parametrized case starts with a fresh DB (template clone).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the parametrized dbt model test covering four source-data shapes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Customer orders&lt;/th&gt;
&lt;th&gt;Expected LTV&lt;/th&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;single order&lt;/td&gt;
&lt;td&gt;1 × $100&lt;/td&gt;
&lt;td&gt;$100&lt;/td&gt;
&lt;td&gt;one-hundred&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bulk orders&lt;/td&gt;
&lt;td&gt;100 × $10&lt;/td&gt;
&lt;td&gt;$1000&lt;/td&gt;
&lt;td&gt;thousand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no orders&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;zero-orders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;returned order&lt;/td&gt;
&lt;td&gt;1 × $100 + 1 × -$50&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;td&gt;with-return&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/component/test_dim_customer_ltv.py — parametrized dbt model test
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mypkg.dbt_helpers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;run_dbt_model&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerLTVCase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="c1"&gt;# cents; negative = return
&lt;/span&gt;    &lt;span class="n"&gt;expected_ltv_cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;


&lt;span class="n"&gt;CASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;CustomerLTVCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;one-hundred&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                                &lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;CustomerLTVCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thousand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1_000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                          &lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;CustomerLTVCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zero-orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;[],&lt;/span&gt;                                          &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;CustomerLTVCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;with-return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                        &lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;case&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CASES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# ids from the dataclass field
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_dim_customer_ltv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CustomerLTVCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Seed source rows for one customer; run the dbt model; assert LTV.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Seed the source table
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            INSERT INTO public.customers(id, email, name)
            VALUES (%s, %s, %s) RETURNING id
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Test User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cents&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                INSERT INTO public.orders(id, customer_id, total_cents, status)
                VALUES (%s, %s, %s, %s)
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;returned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Materialise the dbt model (uses postgresql connection)
&lt;/span&gt;    &lt;span class="nf"&gt;run_dbt_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dim_customer_lifetime_value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Assert the LTV row equals the expected cents
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT ltv_cents
            FROM   analytics.dim_customer_lifetime_value
            WHERE  customer_id = 1
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_ltv_cents&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# No orders means no LTV row (or zero-valued row, depending on the model)
&lt;/span&gt;        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;CustomerLTVCase&lt;/code&gt; dataclass captures every axis of one test case — the name (which drives the ID), the input orders, and the expected LTV. Grouping into a dataclass beats a bare tuple because named fields survive refactoring.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ids=lambda c: c.name&lt;/code&gt; extracts the ID from the dataclass, giving test IDs like &lt;code&gt;test_dim_customer_ltv[one-hundred]&lt;/code&gt; and &lt;code&gt;test_dim_customer_ltv[with-return]&lt;/code&gt;. Adding a new case is one dataclass entry.&lt;/li&gt;
&lt;li&gt;Each parametrized invocation gets its own fresh &lt;code&gt;postgresql&lt;/code&gt; (function-scope) DB, so the four cases don't leak data into each other. This is why parametrization works cleanly with per-test DB isolation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run_dbt_model&lt;/code&gt; is a thin wrapper around &lt;code&gt;dbt run --select dim_customer_lifetime_value&lt;/code&gt; that points at the injected connection — this lets tests exercise real dbt behaviour without a full &lt;code&gt;dbt-core&lt;/code&gt; install ceremony.&lt;/li&gt;
&lt;li&gt;The zero-orders case tests a subtle boundary: does the model emit a zero row or no row? The test asserts both possibilities (&lt;code&gt;is None or == 0&lt;/code&gt;) because either is a reasonable design; the test locks in whichever the current model chooses.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test ID&lt;/th&gt;
&lt;th&gt;Orders seeded&lt;/th&gt;
&lt;th&gt;Expected LTV&lt;/th&gt;
&lt;th&gt;Actual LTV&lt;/th&gt;
&lt;th&gt;pass/fail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;test_dim_customer_ltv[one-hundred]&lt;/td&gt;
&lt;td&gt;1 × 10,000&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_dim_customer_ltv[thousand]&lt;/td&gt;
&lt;td&gt;100 × 1,000&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_dim_customer_ltv[zero-orders]&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 (or no row)&lt;/td&gt;
&lt;td&gt;no row&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_dim_customer_ltv[with-return]&lt;/td&gt;
&lt;td&gt;10,000, -5,000&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Model dbt tests as parametrized cases with a dataclass per axis. Every dbt model gets at least the four cases: single, bulk, zero, edge-case. The dataclass approach outperforms tuple lists for anything past three fields.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — schema-contract test parametrized over every table
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A canonical DE contract test: every table in the &lt;code&gt;public&lt;/code&gt; schema must have (a) a primary key, (b) a &lt;code&gt;created_at&lt;/code&gt; column, (c) a &lt;code&gt;NOT NULL&lt;/code&gt; constraint on the primary key. Writing one test per table is tedious; &lt;code&gt;pytest_generate_tests&lt;/code&gt; discovers the tables at collection time and generates one test per discovered table. Walk through the hook.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discovery.&lt;/strong&gt; &lt;code&gt;pytest_generate_tests(metafunc)&lt;/code&gt; runs at collection; queries &lt;code&gt;information_schema.tables&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation.&lt;/strong&gt; &lt;code&gt;metafunc.parametrize&lt;/code&gt; supplies the table names as params.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assertion.&lt;/strong&gt; Each generated test validates one table's contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effect.&lt;/strong&gt; Adding a new table automatically adds a new test — zero test-file changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement the &lt;code&gt;pytest_generate_tests&lt;/code&gt; hook that generates one contract test per table in &lt;code&gt;public&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Discovery source&lt;/td&gt;
&lt;td&gt;information_schema.tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter&lt;/td&gt;
&lt;td&gt;schema = 'public' AND table_type = 'BASE TABLE'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Params per table&lt;/td&gt;
&lt;td&gt;(table_name,)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assertions per test&lt;/td&gt;
&lt;td&gt;has PK, has created_at, PK is NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/component/test_schema_contract.py — dynamic parametrization
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;

&lt;span class="c1"&gt;# ------------------------------------------------------------------
# The pytest_generate_tests hook — discovers tables at collection
# ------------------------------------------------------------------
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pytest_generate_tests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metafunc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Generate one test per table in public schema.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;metafunc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fixturenames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;          &lt;span class="c1"&gt;# this hook only fires for tests requesting `table_name`
&lt;/span&gt;
    &lt;span class="c1"&gt;# Query the schema (uses a static connection URL; template DB is up)
&lt;/span&gt;    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host=/tmp dbname=template_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;      &lt;span class="c1"&gt;# or use env vars
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT table_name
            FROM   information_schema.tables
            WHERE  table_schema = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
              AND  table_type   = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BASE TABLE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
            ORDER  BY table_name
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;metafunc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;# ID = table name itself
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# ------------------------------------------------------------------
# The single test — runs once per discovered table
# ------------------------------------------------------------------
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_table_has_primary_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Every public table must have a primary key constraint.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT c.contype
            FROM   pg_constraint c
            JOIN   pg_class      t ON t.oid = c.conrelid
            WHERE  t.relname = %s
              AND  c.contype = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;p&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
        &lt;span class="n"&gt;pk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; has no primary key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_table_has_created_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Every public table must have a created_at column.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT column_name
            FROM   information_schema.columns
            WHERE  table_schema = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
              AND  table_name   = %s
              AND  column_name  = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
        &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; missing created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;pytest_generate_tests&lt;/code&gt; is a hook pytest calls during collection for every test function. Inside the hook, we check whether the current test requests the &lt;code&gt;table_name&lt;/code&gt; fixture — if not, we skip (the hook fires for all tests, not just ours).&lt;/li&gt;
&lt;li&gt;When the hook fires for &lt;code&gt;test_table_has_primary_key&lt;/code&gt;, it queries the template DB for the current schema, gets back &lt;code&gt;['customers', 'orders']&lt;/code&gt; (or whatever exists), and calls &lt;code&gt;metafunc.parametrize("table_name", [...])&lt;/code&gt; — this is exactly like the decorator, but supplied dynamically.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ids=tables&lt;/code&gt; sets the test IDs to the table names themselves: &lt;code&gt;test_table_has_primary_key[customers]&lt;/code&gt;, &lt;code&gt;test_table_has_primary_key[orders]&lt;/code&gt;. When one fails, you know which table is the culprit.&lt;/li&gt;
&lt;li&gt;Adding a new table to &lt;code&gt;schema.sql&lt;/code&gt; automatically adds a new test to the run — zero test-file changes required. This is the amplifier's leverage.&lt;/li&gt;
&lt;li&gt;Note that the discovery query targets &lt;code&gt;template_db&lt;/code&gt;, not the function-scoped &lt;code&gt;postgresql&lt;/code&gt;. The discovery must happen at collection time (before any function fixture is available), so we open a direct connection.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test ID&lt;/th&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;PK found&lt;/th&gt;
&lt;th&gt;created_at found&lt;/th&gt;
&lt;th&gt;pass/fail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;test_table_has_primary_key[customers]&lt;/td&gt;
&lt;td&gt;customers&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_table_has_primary_key[orders]&lt;/td&gt;
&lt;td&gt;orders&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_table_has_created_at[customers]&lt;/td&gt;
&lt;td&gt;customers&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_table_has_created_at[orders]&lt;/td&gt;
&lt;td&gt;orders&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any DE contract that must hold across every table in a schema, use &lt;code&gt;pytest_generate_tests&lt;/code&gt; — never hand-write one test per table. The dynamic generation makes new-table addition automatically covered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on pytest parametrize discipline
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You have a Spark job that computes retention cohorts and a dbt model that computes MRR. You need to cover null/edge/valid axes plus a manifest-driven axis 'every model in the marts folder'. Walk me through the parametrization strategy — decorator vs fixture vs &lt;code&gt;pytest_generate_tests&lt;/code&gt;, how you generate readable IDs, and how you keep the parametrize table maintainable."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using layered parametrize decorators + fixture params + &lt;code&gt;pytest_generate_tests&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. Layer A — decorator-level, static, axis parametrize
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RetentionCase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;signup_dates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;activity_dates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;     &lt;span class="c1"&gt;# per user
&lt;/span&gt;    &lt;span class="n"&gt;cohort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;expected_retention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;


&lt;span class="n"&gt;RETENTION_CASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;RetentionCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all-active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01-08&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01-15&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01-22&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;RetentionCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;half-churned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01-08&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01-08&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[]],&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;RetentionCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zero-cohort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;case&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RETENTION_CASES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_retention_cohort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spark_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RetentionCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mypkg.retention&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cohort_retention&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signup_dates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activity_dates&lt;/span&gt;&lt;span class="p"&gt;))],&lt;/span&gt;
        &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signup_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activity_dates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cohort_retention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cohort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cohort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_retention&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. Layer B — fixture-level, params, backend switching
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duckdb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duck&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sql_backend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Yield a connection to either Postgres or DuckDB.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test.duckdb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;# (schema-load elided)
&lt;/span&gt;        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_mrr_query_matches_across_backends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_backend&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Same query, both backends, must agree on MRR.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mypkg.mrr&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;compute_mrr&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_mrr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_backend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;     &lt;span class="c1"&gt;# basic sanity
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 3. Layer C — pytest_generate_tests, manifest-driven
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pytest_generate_tests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metafunc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Discover every model in the dbt manifest at collection.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;metafunc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fixturenames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
    &lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target/manifest.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="c1"&gt;# Filter to models under models/marts/
&lt;/span&gt;    &lt;span class="n"&gt;marts_models&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resource_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marts/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;metafunc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;marts_models&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;marts_models&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_mart_has_row_count_gt_zero&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbt_model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Every mart model must materialise to &amp;gt;= 1 row.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) FROM analytics.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dbt_model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;row_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;row_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mart &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dbt_model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; materialised zero rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Style&lt;/th&gt;
&lt;th&gt;Discovery&lt;/th&gt;
&lt;th&gt;ID example&lt;/th&gt;
&lt;th&gt;Test count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A (decorator)&lt;/td&gt;
&lt;td&gt;static list&lt;/td&gt;
&lt;td&gt;at import&lt;/td&gt;
&lt;td&gt;test_retention_cohort[all-active]&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A (decorator)&lt;/td&gt;
&lt;td&gt;static list&lt;/td&gt;
&lt;td&gt;at import&lt;/td&gt;
&lt;td&gt;test_retention_cohort[half-churned]&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A (decorator)&lt;/td&gt;
&lt;td&gt;static list&lt;/td&gt;
&lt;td&gt;at import&lt;/td&gt;
&lt;td&gt;test_retention_cohort[zero-cohort]&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B (fixture params)&lt;/td&gt;
&lt;td&gt;fixture-level&lt;/td&gt;
&lt;td&gt;at collection&lt;/td&gt;
&lt;td&gt;test_mrr_query_matches_across_backends[pg]&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B (fixture params)&lt;/td&gt;
&lt;td&gt;fixture-level&lt;/td&gt;
&lt;td&gt;at collection&lt;/td&gt;
&lt;td&gt;test_mrr_query_matches_across_backends[duck]&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C (generate_tests)&lt;/td&gt;
&lt;td&gt;dynamic hook&lt;/td&gt;
&lt;td&gt;at collection&lt;/td&gt;
&lt;td&gt;test_mart_has_row_count_gt_zero[fct_orders]&lt;/td&gt;
&lt;td&gt;N (manifest-driven)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, the same parametrized suite covers three orthogonal axes with three distinct techniques: (A) static known cases for retention logic, (B) fixture-level cases for cross-backend agreement, (C) manifest-driven cases for every mart model. Adding a new mart is zero test-file changes; adding a new retention edge case is one dataclass row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Style&lt;/th&gt;
&lt;th&gt;Effort to add new case&lt;/th&gt;
&lt;th&gt;Runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;new retention axis&lt;/td&gt;
&lt;td&gt;Layer A&lt;/td&gt;
&lt;td&gt;1 dataclass row&lt;/td&gt;
&lt;td&gt;+1 test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;add MySQL backend&lt;/td&gt;
&lt;td&gt;Layer B&lt;/td&gt;
&lt;td&gt;1 fixture param + branch&lt;/td&gt;
&lt;td&gt;+N tests (one per test)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;add new mart model&lt;/td&gt;
&lt;td&gt;Layer C&lt;/td&gt;
&lt;td&gt;zero (manifest picks it up)&lt;/td&gt;
&lt;td&gt;+1 test automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Decorator-level parametrize&lt;/strong&gt;&lt;/strong&gt; — the classic; best for known, static axes; readable IDs via lambda over dataclass fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Fixture-level parametrize&lt;/strong&gt;&lt;/strong&gt; — routes the same params through a fixture; ideal for backend/config axes that need per-param setup; every test that requests the fixture inherits the param multiplication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;pytest_generate_tests&lt;/strong&gt;&lt;/strong&gt; — the escape hatch; discovers axes at collection time from runtime sources (dbt manifest, information_schema, YAML config); the amplifier that lets one test cover every model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;ids= discipline&lt;/strong&gt;&lt;/strong&gt; — the difference between &lt;code&gt;test_foo[0]&lt;/code&gt; (hostile) and &lt;code&gt;test_foo[with-return]&lt;/code&gt; (grep-friendly). Use &lt;code&gt;lambda c: c.name&lt;/code&gt; when working with dataclasses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — parametrization overhead is negligible (~µs per case at collection); the dominant cost is the test body itself. The eliminated cost is copy-pasted test functions (typically 10-20 lines each) that drift out of sync. Net: three techniques cover 90% of DE parametrization needs. O(1) code per axis added.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Data validation&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — data-validation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Data validation and contract-testing problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Python&lt;/span&gt;
&lt;span&gt;Topic — defensive-coding&lt;/span&gt;
&lt;strong&gt;Defensive coding and edge-case coverage&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/defensive-coding" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Docker-Compose + Testcontainers integration tests
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;testcontainers-python&lt;/code&gt; + a session-scoped &lt;code&gt;DockerCompose&lt;/code&gt; fixture is the pattern every senior DE has converged on — hermetic, fast, and CI-safe
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;pytest docker-compose&lt;/code&gt; integration testing means declaring a &lt;code&gt;docker-compose.test.yml&lt;/code&gt; next to your source, wrapping it in a session-scoped pytest fixture that spins the whole networked stack (Postgres + Kafka + MinIO + whatever) up once per pytest run and tears it down at exit — and if you use &lt;code&gt;testcontainers-python&lt;/code&gt; instead of shelling out to &lt;code&gt;docker-compose&lt;/code&gt;, you get first-class &lt;code&gt;wait_for&lt;/code&gt; health checks, per-container &lt;code&gt;get_service_host&lt;/code&gt; accessors, and clean handling of xdist-worker port allocation for free&lt;/strong&gt;. Every senior data engineer has debugged the "tests pass on my laptop but fail on CI because Postgres wasn't ready yet" flake and every senior data engineer has fixed it with a &lt;code&gt;wait_for&lt;/code&gt; on &lt;code&gt;SELECT 1&lt;/code&gt; inside a retry loop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2mzalvmww10lnirmfmsm.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2mzalvmww10lnirmfmsm.jpeg" alt="Iconographic docker-compose testcontainers diagram — a compose-yaml card feeding a networked testcontainer with Postgres and Kafka boxes, a pytest session-fixture handshake, and a wait-for-ready chip." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two container primitives — pick one and commit.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;testcontainers-python&lt;/code&gt; per-service.&lt;/strong&gt; Instantiate &lt;code&gt;PostgresContainer("postgres:15")&lt;/code&gt;, &lt;code&gt;KafkaContainer("confluentinc/cp-kafka:7.6.0")&lt;/code&gt;, &lt;code&gt;MinioContainer("minio/minio:latest")&lt;/code&gt; in Python. Best for tests that need one or two containers. Handles wait-for-ready, port publishing, and network isolation. Idiomatic in 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;testcontainers.compose.DockerCompose&lt;/code&gt;.&lt;/strong&gt; Wraps a &lt;code&gt;docker-compose.yml&lt;/code&gt; file. Best for tests that need the full stack (Airflow scheduler + webserver + Postgres + Redis + Celery worker + Kafka). Handles &lt;code&gt;docker-compose up -d&lt;/code&gt;, health-check waits, and &lt;code&gt;docker-compose down -v&lt;/code&gt; at teardown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest-docker-compose&lt;/code&gt; plugin.&lt;/strong&gt; Older wrapper; still maintained. Slightly less flexible than testcontainers-python; retained in legacy suites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw &lt;code&gt;subprocess.run(['docker-compose', 'up'])&lt;/code&gt;.&lt;/strong&gt; The anti-pattern. Loses &lt;code&gt;wait_for&lt;/code&gt;, forces you to hand-roll health checks, and breaks under xdist. Only appropriate for one-off manual scripts, never for a test suite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The three lifecycle patterns for container fixtures.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session scope + shared state.&lt;/strong&gt; Spin the stack up once; every test hits the same DB. Fast (one startup ~10 s amortised over 100 tests) but requires tests to clean up after themselves or use per-test schemas / topics. This is the pattern for most DE integration suites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session scope + per-test isolation.&lt;/strong&gt; Spin the stack up once; each test creates its own schema (&lt;code&gt;SET search_path&lt;/code&gt;) or Kafka topic (&lt;code&gt;test_&amp;lt;uuid&amp;gt;&lt;/code&gt;). Fast and hermetic. This is the pattern for Layer-4 tests that must be independent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function scope + full teardown.&lt;/strong&gt; Spin the stack up per test; tear down completely at teardown. Slow (~10 s per test); use only for tests that need pristine state and can't share containers safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The wait-for-ready idiom — non-optional for every container.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docker-compose up -d&lt;/code&gt;&lt;/strong&gt; returns as soon as the containers are &lt;em&gt;started&lt;/em&gt;, not when they are &lt;em&gt;ready&lt;/em&gt;. Postgres takes ~2-5 s after start to accept connections; Kafka ~5-10 s; MinIO ~2 s. Racing this window is the #1 source of container-test flakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP health-check pattern.&lt;/strong&gt; &lt;code&gt;testcontainers.compose.DockerCompose.wait_for("http://localhost:9200/_cluster/health")&lt;/code&gt; polls until the endpoint returns 200. Set a 30-second timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP-connect pattern.&lt;/strong&gt; For services without HTTP (raw Postgres, Kafka broker), poll a &lt;code&gt;psycopg2.connect(...)&lt;/code&gt; or &lt;code&gt;KafkaConsumer(bootstrap_servers=...)&lt;/code&gt; in a retry loop. Third-party libs like &lt;code&gt;tenacity&lt;/code&gt; (&lt;code&gt;@retry(stop_after_delay=30, wait_fixed=0.5)&lt;/code&gt;) make this concise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker-compose healthchecks.&lt;/strong&gt; Define &lt;code&gt;healthcheck&lt;/code&gt; in the yml (&lt;code&gt;test: pg_isready -U test&lt;/code&gt;) and use &lt;code&gt;depends_on: postgres: condition: service_healthy&lt;/code&gt; — compose itself waits, and the fixture just waits on the top-level service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on pytest docker-compose.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you avoid port collisions under xdist?" — either OS-allocated ports (published on the container side) or per-worker compose projects (&lt;code&gt;COMPOSE_PROJECT_NAME=test_${WORKER_ID}&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;"How do you clean up volumes?" — &lt;code&gt;docker-compose down -v&lt;/code&gt; at teardown; &lt;code&gt;DockerCompose&lt;/code&gt; context manager does this automatically.&lt;/li&gt;
&lt;li&gt;"What's the wait-for-ready story?" — HTTP endpoint poll, TCP connect retry, or compose-native healthcheck + &lt;code&gt;depends_on.condition&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"When would you pick testcontainers-python over compose?" — small stack (1-2 containers) and per-test isolation is required.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — &lt;code&gt;pytest-postgresql&lt;/code&gt; fixture with a schema template
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The Layer-3 equivalent of a docker-compose stack: a real Postgres process, but no container. &lt;code&gt;pytest-postgresql&lt;/code&gt; bundles a Postgres binary invocation directly — no Docker required — and provides &lt;code&gt;postgresql_proc&lt;/code&gt; (session, one binary) plus &lt;code&gt;postgresql&lt;/code&gt; (function, one DB clone). This is faster than dockerised Postgres for local dev and works identically on GitHub Actions Ubuntu runners. Walk through the wiring.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it starts.&lt;/strong&gt; A &lt;code&gt;postgres&lt;/code&gt; binary running against a temp datadir; no image pull, no Docker daemon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How it isolates tests.&lt;/strong&gt; Function-scoped DB via template clone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where the schema lives.&lt;/strong&gt; &lt;code&gt;load=[Path('schema.sql')]&lt;/code&gt; applied to the template at session start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What breaks.&lt;/strong&gt; Nothing if the runner has &lt;code&gt;postgresql-15&lt;/code&gt; installed; a clear error message if not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Wire the pytest-postgresql template + a single component test that inserts a row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres binary&lt;/td&gt;
&lt;td&gt;postgresql-15 on the runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data dir&lt;/td&gt;
&lt;td&gt;ephemeral temp dir&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema&lt;/td&gt;
&lt;td&gt;tests/schema.sql&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test DB&lt;/td&gt;
&lt;td&gt;fresh template clone per test&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/conftest.py — pytest-postgresql fixtures
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pytest_postgresql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;

&lt;span class="n"&gt;TEST_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;
&lt;span class="n"&gt;SCHEMA_SQL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TEST_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;postgresql_proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postgresql_proc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                          &lt;span class="c1"&gt;# OS-allocated
&lt;/span&gt;    &lt;span class="n"&gt;unixsocketdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;postgresql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql_proc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SCHEMA_SQL&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/component/test_pg_template.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_schema_loaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Template clone must include the schema.sql tables.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT tablename FROM pg_tables
            WHERE  schemaname = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
            ORDER  BY tablename
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tables&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tables&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_insert_and_select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Round-trip an insert; assert the row is visible.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            INSERT INTO public.orders(customer_id, total_cents, status)
            VALUES (1, 500, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) RETURNING id
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT total_cents FROM public.orders WHERE id = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;postgresql_proc&lt;/code&gt; at session scope starts one Postgres binary. The &lt;code&gt;port=None&lt;/code&gt; argument tells pytest-postgresql to bind to an OS-allocated port; the caller can read the actual port from &lt;code&gt;postgresql_proc.port&lt;/code&gt;. This is what makes xdist safe.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;postgresql&lt;/code&gt; at function scope creates a fresh database per test by (a) waiting for &lt;code&gt;postgresql_proc&lt;/code&gt; to be up, (b) issuing &lt;code&gt;CREATE DATABASE test_db_&amp;lt;random&amp;gt;&lt;/code&gt;, (c) applying &lt;code&gt;schema.sql&lt;/code&gt;, (d) opening a psycopg2 connection, (e) at teardown dropping the DB.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SCHEMA_SQL&lt;/code&gt; is loaded on every clone, so the schema is guaranteed to be present. For faster startup, some teams pre-load the schema onto a &lt;code&gt;template_db&lt;/code&gt; and clone from &lt;em&gt;that&lt;/em&gt; — pytest-postgresql supports both patterns via &lt;code&gt;template_dbname&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_schema_loaded&lt;/code&gt; proves the schema arrived; &lt;code&gt;test_insert_and_select&lt;/code&gt; proves the DB is writable and reads back. Both are isolated — the second test starts with an empty &lt;code&gt;orders&lt;/code&gt; table even though the first test would have inserted something.&lt;/li&gt;
&lt;li&gt;Compared to a dockerised Postgres, this pattern skips the Docker daemon entirely — ~500 ms faster per pytest session and works on runners where Docker-in-Docker isn't available (some CI providers).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Startup&lt;/th&gt;
&lt;th&gt;Body&lt;/th&gt;
&lt;th&gt;Teardown&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;test_schema_loaded&lt;/td&gt;
&lt;td&gt;~1 s (session, first only) + ~100 ms (clone)&lt;/td&gt;
&lt;td&gt;~10 ms&lt;/td&gt;
&lt;td&gt;~30 ms&lt;/td&gt;
&lt;td&gt;~1.15 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_insert_and_select&lt;/td&gt;
&lt;td&gt;~100 ms (clone)&lt;/td&gt;
&lt;td&gt;~15 ms&lt;/td&gt;
&lt;td&gt;~30 ms&lt;/td&gt;
&lt;td&gt;~145 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(subsequent tests)&lt;/td&gt;
&lt;td&gt;~100 ms (clone)&lt;/td&gt;
&lt;td&gt;~10-50 ms&lt;/td&gt;
&lt;td&gt;~30 ms&lt;/td&gt;
&lt;td&gt;~140-180 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For Layer-3 SQL-emitting tests, prefer &lt;code&gt;pytest-postgresql&lt;/code&gt; over a dockerised Postgres — it's faster to start, easier to reason about, and works on runners without Docker access. Save containers for Layer 4.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Testcontainers Kafka + a smoke consumer test
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A test that produces a message to Kafka and consumes it back, using &lt;code&gt;testcontainers-python&lt;/code&gt; to spin up a single Kafka container. This is the Layer-4 pattern for CDC / event-driven code where the interaction with Kafka is the thing under test. Walk through the fixture + test.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Container.&lt;/strong&gt; &lt;code&gt;KafkaContainer("confluentinc/cp-kafka:7.6.0")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixture scope.&lt;/strong&gt; Session — one Kafka broker for all Kafka tests in the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-test isolation.&lt;/strong&gt; Each test creates its own topic named &lt;code&gt;test_&amp;lt;uuid&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assertion.&lt;/strong&gt; Produce a message; consume with a fresh consumer; assert value matches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the Kafka fixture + a smoke test that round-trips a message through a per-test topic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kafka image&lt;/td&gt;
&lt;td&gt;confluentinc/cp-kafka:7.6.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixture scope&lt;/td&gt;
&lt;td&gt;session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation&lt;/td&gt;
&lt;td&gt;per-test topic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Producer / consumer lib&lt;/td&gt;
&lt;td&gt;kafka-python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/conftest.py — Kafka session fixture
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;testcontainers.kafka&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KafkaContainer&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kafka_container&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Spin up one Kafka broker per pytest run.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;KafkaContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confluentinc/cp-kafka:7.6.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# KafkaContainer.get_bootstrap_server() blocks until ready
&lt;/span&gt;        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kafka_topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kafka_container&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Per-test unique topic; consumers use fresh group-id.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;
    &lt;span class="c1"&gt;# (topic auto-cleaned when broker exits; explicit delete optional)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/integration/test_kafka_roundtrip.py — smoke test
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kafka&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KafkaProducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KafkaConsumer&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_kafka_produce_and_consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kafka_container&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kafka_topic&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Produce one JSON message; consume it back; assert equality.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;bootstrap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kafka_container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_bootstrap_server&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. Produce
&lt;/span&gt;    &lt;span class="n"&gt;producer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KafkaProducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bootstrap_servers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;value_serializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;placed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kafka_topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Consume from earliest
&lt;/span&gt;    &lt;span class="n"&gt;consumer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KafkaConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;kafka_topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bootstrap_servers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;auto_offset_reset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;earliest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;group_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grp_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kafka_topic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;consumer_timeout_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# stop after 5 s of no messages
&lt;/span&gt;        &lt;span class="n"&gt;value_deserializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Assert
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;KafkaContainer&lt;/code&gt; at session scope pulls the Kafka image (once per CI run — cached by Docker), starts the broker, and blocks on &lt;code&gt;get_bootstrap_server()&lt;/code&gt; until the broker accepts client connections. This is the wait-for-ready idiom baked into testcontainers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kafka_topic&lt;/code&gt; at function scope generates a per-test topic name using &lt;code&gt;uuid.uuid4().hex[:8]&lt;/code&gt; — the 8-char hex is short enough to be readable and long enough to collision-resist across 1000+ concurrent test runs.&lt;/li&gt;
&lt;li&gt;The producer serialises Python dicts as JSON bytes and sends one message. &lt;code&gt;producer.flush()&lt;/code&gt; blocks until the broker acks; &lt;code&gt;producer.close()&lt;/code&gt; shuts down cleanly. Both are necessary to guarantee the message is durable before the consumer starts.&lt;/li&gt;
&lt;li&gt;The consumer uses a per-topic group-id (&lt;code&gt;grp_&amp;lt;topic&amp;gt;&lt;/code&gt;) and &lt;code&gt;auto_offset_reset="earliest"&lt;/code&gt; — this guarantees a fresh consumer group with no committed offset reads from the start of the topic. &lt;code&gt;consumer_timeout_ms=5_000&lt;/code&gt; ensures the &lt;code&gt;list(consumer)&lt;/code&gt; returns even if no more messages arrive.&lt;/li&gt;
&lt;li&gt;The assertion is pure equality on the deserialised value. Because the topic is per-test, there's no way for a previous test's messages to leak in — the count is deterministic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kafka startup (session, first test)&lt;/td&gt;
&lt;td&gt;~8 s&lt;/td&gt;
&lt;td&gt;image pull cached, broker init&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kafka_topic creation&lt;/td&gt;
&lt;td&gt;&amp;lt;1 ms&lt;/td&gt;
&lt;td&gt;uuid gen only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Producer send + flush&lt;/td&gt;
&lt;td&gt;~50 ms&lt;/td&gt;
&lt;td&gt;one message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer poll (5s timeout)&lt;/td&gt;
&lt;td&gt;~5 s&lt;/td&gt;
&lt;td&gt;waits for stall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test total (first)&lt;/td&gt;
&lt;td&gt;~13 s&lt;/td&gt;
&lt;td&gt;mostly Kafka startup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test total (subsequent)&lt;/td&gt;
&lt;td&gt;~5.1 s&lt;/td&gt;
&lt;td&gt;just consumer stall&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For Kafka tests, use &lt;code&gt;testcontainers-python&lt;/code&gt;'s &lt;code&gt;KafkaContainer&lt;/code&gt; at session scope; give every test its own uuid-suffixed topic; give every consumer a matching group-id. This is the cheapest correct pattern.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — Airflow DAG integration test spinning up scheduler + Postgres
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; An Airflow DAG that reads from a source table and writes to a target table needs to be tested end-to-end: metadata DB running, scheduler running, DAG file discovered, task instances actually executing. &lt;code&gt;DockerCompose&lt;/code&gt; fixture with an Airflow compose file is the standard pattern. Walk through the yml + fixture + test.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compose services.&lt;/strong&gt; postgres (metadata) + airflow-init + airflow-scheduler + airflow-webserver.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixture scope.&lt;/strong&gt; Session — one Airflow up per test session; every DAG test triggers via CLI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test isolation.&lt;/strong&gt; Each test triggers a fresh DAG run with a unique &lt;code&gt;dag_run_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assertion.&lt;/strong&gt; After &lt;code&gt;airflow dags trigger&lt;/code&gt;, poll &lt;code&gt;airflow tasks state&lt;/code&gt; until success/failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the compose fixture + a DAG test that triggers &lt;code&gt;ingest_daily_orders&lt;/code&gt; and asserts success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compose file&lt;/td&gt;
&lt;td&gt;docker-compose.test.yml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Services&lt;/td&gt;
&lt;td&gt;postgres, airflow-scheduler, airflow-webserver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;airflow dags trigger ingest_daily_orders --run-id test_&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poll&lt;/td&gt;
&lt;td&gt;airflow tasks state ... every 2 s, up to 60 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.test.yml — minimal Airflow stack for tests&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.9"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-U"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;airflow-init&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apache/airflow:2.9.2&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__EXECUTOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LocalExecutor&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__DATABASE__SQL_ALCHEMY_CONN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql+psycopg2://airflow:airflow@postgres/airflow&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-c "airflow db init &amp;amp;&amp;amp; airflow users create --role Admin --username admin --password admin --email a@b.c --firstname a --lastname b"&lt;/span&gt;

  &lt;span class="na"&gt;airflow-scheduler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apache/airflow:2.9.2&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;airflow-init&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_completed_successfully&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__CORE__EXECUTOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LocalExecutor&lt;/span&gt;
      &lt;span class="na"&gt;AIRFLOW__DATABASE__SQL_ALCHEMY_CONN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql+psycopg2://airflow:airflow@postgres/airflow&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./dags:/opt/airflow/dags&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scheduler&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/conftest.py — Airflow compose fixture
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;testcontainers.compose&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DockerCompose&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;airflow_stack&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Spin up Airflow + Postgres via docker-compose.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DockerCompose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;compose_file_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose.test.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pull&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Wait for the scheduler to actually parse the DAGs
&lt;/span&gt;        &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# webserver
&lt;/span&gt;        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/integration/test_ingest_dag.py — end-to-end DAG test
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Shell out to airflow via docker exec on the scheduler container.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;full&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow-scheduler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_ingest_daily_orders_dag_succeeds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;airflow_stack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Trigger ingest_daily_orders; poll for success; assert green.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. Trigger
&lt;/span&gt;    &lt;span class="nf"&gt;_run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trigger&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingest_daily_orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--run-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Poll state up to 60 s
&lt;/span&gt;    &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingest_daily_orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Assert
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DAG &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ended in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The compose file uses Airflow's official image with &lt;code&gt;LocalExecutor&lt;/code&gt; — no Celery, no Redis, minimum surface for a test stack. The metadata DB is a per-test-run Postgres; the scheduler picks up DAGs from a mounted &lt;code&gt;./dags&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;airflow-init&lt;/code&gt; runs &lt;code&gt;airflow db init&lt;/code&gt; + creates an admin user, then exits. &lt;code&gt;depends_on: condition: service_completed_successfully&lt;/code&gt; on the scheduler ensures the init has finished before the scheduler starts.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;airflow_stack&lt;/code&gt; fixture wraps &lt;code&gt;DockerCompose&lt;/code&gt; — the &lt;code&gt;with&lt;/code&gt; block handles &lt;code&gt;docker-compose up -d&lt;/code&gt; on entry and &lt;code&gt;docker-compose down -v&lt;/code&gt; on exit. &lt;code&gt;pull=True&lt;/code&gt; ensures images are pulled once per session.&lt;/li&gt;
&lt;li&gt;The test triggers a DAG run via &lt;code&gt;airflow dags trigger&lt;/code&gt; executed inside the scheduler container (via &lt;code&gt;docker exec&lt;/code&gt;). The run-id is uuid-suffixed for per-test isolation.&lt;/li&gt;
&lt;li&gt;The polling loop calls &lt;code&gt;airflow dags state&lt;/code&gt; every 2 seconds up to a 60-second deadline. &lt;code&gt;success&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt; breaks the loop; the test assertion pins the required outcome.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compose startup (session first)&lt;/td&gt;
&lt;td&gt;~30 s&lt;/td&gt;
&lt;td&gt;postgres, init, scheduler, webserver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAG trigger&lt;/td&gt;
&lt;td&gt;~200 ms&lt;/td&gt;
&lt;td&gt;subprocess exec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task execution&lt;/td&gt;
&lt;td&gt;~5-20 s&lt;/td&gt;
&lt;td&gt;depends on DAG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poll loop&lt;/td&gt;
&lt;td&gt;~5-20 s&lt;/td&gt;
&lt;td&gt;2 s poll interval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test total (first)&lt;/td&gt;
&lt;td&gt;~55 s&lt;/td&gt;
&lt;td&gt;mostly compose startup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test total (subsequent)&lt;/td&gt;
&lt;td&gt;~25 s&lt;/td&gt;
&lt;td&gt;just trigger + poll&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For Airflow DAG integration tests, use &lt;code&gt;DockerCompose&lt;/code&gt; at session scope with a compose file that pins Airflow 2.9+ and Postgres 15+. Every test gets a fresh run_id; the scheduler+worker are shared. Session-scope amortises the ~30 s compose startup across all DAG tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on pytest docker-compose integration testing
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You have a Layer-4 integration suite with 40 tests, each currently spinning up its own docker-compose stack (Postgres + Kafka + MinIO) at ~15 s startup, so the suite takes 10 minutes. You need to bring it under 3 minutes. Walk me through the session-scoped compose fixture, the per-test schema/topic isolation pattern, the wait-for-ready idiom, and how you keep the pattern xdist-safe."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using session-scoped &lt;code&gt;DockerCompose&lt;/code&gt; + per-test schema + wait-for-ready + xdist-safe port allocation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. docker-compose.test.yml — the full stack, one project per xdist worker&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.9"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432"&lt;/span&gt;                          &lt;span class="c1"&gt;# published, port assigned by Docker&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-U"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;

  &lt;span class="na"&gt;kafka&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;confluentinc/cp-kafka:7.6.0&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;KAFKA_ADVERTISED_LISTENERS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PLAINTEXT://kafka:9092&lt;/span&gt;
      &lt;span class="na"&gt;KAFKA_LISTENERS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;            &lt;span class="s"&gt;PLAINTEXT://0.0.0.0:9092&lt;/span&gt;
      &lt;span class="na"&gt;KAFKA_ZOOKEEPER_CONNECT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;zookeeper:2181&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;zookeeper&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9092"&lt;/span&gt;

  &lt;span class="na"&gt;zookeeper&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;confluentinc/cp-zookeeper:7.6.0&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;ZOOKEEPER_CLIENT_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2181&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

  &lt;span class="na"&gt;minio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minio/minio:latest&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;server /data --console-address ":9001"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9000"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9001"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9000/minio/health/live"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. tests/conftest.py — session compose fixture with xdist safety
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;testcontainers.compose&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DockerCompose&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compose_stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One compose project per xdist worker; session-scoped.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;worker_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;       &lt;span class="c1"&gt;# 'master' when serial; 'gw0', 'gw1', ...
&lt;/span&gt;    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPOSE_PROJECT_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DockerCompose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;compose_file_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose.test.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pull&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;# wait for healthchecks
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Belt-and-braces: also block on our own SELECT 1
&lt;/span&gt;        &lt;span class="n"&gt;pg_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_service_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;_wait_postgres_ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pg_port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_wait_postgres_ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Retry SELECT 1 until success or timeout.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
    &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                  &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,):&lt;/span&gt;
                        &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres never became ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 3. Per-test isolation — new schema per test
&lt;/span&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;isolated_pg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compose_stack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Yield a connection to a per-test schema in the shared Postgres.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compose_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_service_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CREATE SCHEMA &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SET search_path TO &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# (schema DDL for this test)
&lt;/span&gt;        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            CREATE TABLE orders(id BIGSERIAL PRIMARY KEY, cents BIGINT)
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;

    &lt;span class="c1"&gt;# Teardown — drop the whole schema
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DROP SCHEMA &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; CASCADE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.mark.integration&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_ingest_writes_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isolated_pg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Real Postgres, per-test schema, no leakage.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mypkg.ingest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ingest_batch&lt;/span&gt;
    &lt;span class="nf"&gt;ingest_batch&lt;/span&gt;&lt;span class="p"&gt;([(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;isolated_pg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;isolated_pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) FROM orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Before (per-test compose)&lt;/th&gt;
&lt;th&gt;After (session + per-schema)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compose startups&lt;/td&gt;
&lt;td&gt;40 × ~15 s = 10 min&lt;/td&gt;
&lt;td&gt;1 × ~15 s per worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test body&lt;/td&gt;
&lt;td&gt;40 × ~1 s = 40 s&lt;/td&gt;
&lt;td&gt;40 × ~1 s = 40 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema create/drop&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;40 × ~30 ms = ~1.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xdist worker safety&lt;/td&gt;
&lt;td&gt;port-collision risk&lt;/td&gt;
&lt;td&gt;COMPOSE_PROJECT_NAME per worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total (4-worker xdist)&lt;/td&gt;
&lt;td&gt;~4 min&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wait-for-ready&lt;/td&gt;
&lt;td&gt;ad-hoc sleeps&lt;/td&gt;
&lt;td&gt;health checks + SELECT 1 retry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, the same 40 tests run in ~1 minute on 4-worker xdist, share one compose stack per worker, and every test isolates via a per-test schema. &lt;code&gt;COMPOSE_PROJECT_NAME=test_&amp;lt;worker_id&amp;gt;&lt;/code&gt; ensures each xdist worker gets its own container network with distinct published ports — no collision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime (serial)&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;td&gt;~2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime (4-worker xdist)&lt;/td&gt;
&lt;td&gt;~4 min (port collisions)&lt;/td&gt;
&lt;td&gt;~1 min (isolated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compose startups&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;1 per worker (4 total)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test isolation&lt;/td&gt;
&lt;td&gt;full (own stack)&lt;/td&gt;
&lt;td&gt;full (own schema)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wait-for-ready&lt;/td&gt;
&lt;td&gt;absent (flaky)&lt;/td&gt;
&lt;td&gt;health check + SELECT 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory pressure&lt;/td&gt;
&lt;td&gt;40 × 2 GB stacks&lt;/td&gt;
&lt;td&gt;1 × 2 GB per worker (~8 GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Session-scoped DockerCompose fixture&lt;/strong&gt;&lt;/strong&gt; — one compose stack per pytest session (per xdist worker). Amortises ~15 s of startup across all Layer-4 tests. The &lt;code&gt;wait=True&lt;/code&gt; argument makes testcontainers-python block on the healthcheck-defined ready conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Per-test schema isolation&lt;/strong&gt;&lt;/strong&gt; — each test creates its own Postgres schema (&lt;code&gt;CREATE SCHEMA t_&amp;lt;uuid&amp;gt;&lt;/code&gt;) and sets &lt;code&gt;search_path&lt;/code&gt;; all its DDL and DML live under that schema. Cleanup is one &lt;code&gt;DROP SCHEMA CASCADE&lt;/code&gt;. This gives per-test isolation on top of a shared DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;COMPOSE_PROJECT_NAME per worker&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;worker_id&lt;/code&gt; is pytest-xdist's per-worker identifier (&lt;code&gt;gw0&lt;/code&gt;, &lt;code&gt;gw1&lt;/code&gt;, ...). Setting the project name isolates each worker's compose stack — separate container names, separate networks, no port collision even on the same host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Wait-for-ready idiom&lt;/strong&gt;&lt;/strong&gt; — compose healthchecks + a belt-and-braces &lt;code&gt;SELECT 1&lt;/code&gt; retry loop. The healthcheck handles ~95% of races; the SELECT 1 retry catches the last 5% (schema not yet migrated, etc).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one compose stack per xdist worker (~2 GB RAM), ~15 s startup amortised, ~30 ms per test for schema create/drop. The eliminated cost is 40 × ~15 s = 10 minutes of compose startups. Net O(1) startup per worker versus O(N) startup per test. Volume cleanup is automatic via &lt;code&gt;docker-compose down -v&lt;/code&gt; inside the context manager's exit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;ETL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;ETL integration and end-to-end pipeline tests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;API&lt;/span&gt;
&lt;span&gt;Topic — api-integration&lt;/span&gt;
&lt;strong&gt;API integration testing patterns&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/api-integration" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. CI wiring — coverage, xdist parallelism, flaky-test discipline
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;pytest -n auto --cov --cov-fail-under=85&lt;/code&gt; is the two-line CI recipe every DE team eventually converges on
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;pytest for data engineering&lt;/code&gt; in CI is the discipline of running the four-layer suite in parallel via &lt;code&gt;pytest-xdist&lt;/code&gt;, gating merges on branch-coverage via &lt;code&gt;pytest-cov&lt;/code&gt; with a &lt;code&gt;--cov-fail-under&lt;/code&gt; threshold, uploading &lt;code&gt;--junit-xml&lt;/code&gt; to a flaky-test dashboard, and quarantining known-flaky tests with &lt;code&gt;pytest-rerunfailures&lt;/code&gt; plus a nightly retriage job — the combination that turns a 25-minute serial suite into a 7-minute PR gate that catches regressions the day the code is written&lt;/strong&gt;. Every senior data engineer has inherited a CI that runs pytest serially, has no coverage gate, and ignores flakes; and every senior data engineer has converted that same CI into a fast, gated, parallel suite over the course of a two-week focus block that pays for itself in developer time within the first month.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2u8lp7fr7max1ukn3wov.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2u8lp7fr7max1ukn3wov.jpeg" alt="Iconographic pytest CI diagram — a horizontal pipeline of install / lint / xdist-parallel workers / coverage-gate / green-check, with flaky-test quarantine bin on the side." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The three-pillar CI recipe for DE pytest suites.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pillar 1 — xdist parallelism.&lt;/strong&gt; &lt;code&gt;pytest -n auto --dist=loadgroup&lt;/code&gt; spawns one worker per CPU; distributes tests to workers by &lt;code&gt;--xdist-group&lt;/code&gt; marker for fixture affinity. This is the raw-speed win: an N-core runner runs the suite ~N× faster (minus startup + coordination overhead).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pillar 2 — coverage gate.&lt;/strong&gt; &lt;code&gt;pytest --cov=mypkg --cov-branch --cov-fail-under=85&lt;/code&gt; measures line-and-branch coverage, fails the run if coverage drops below 85%. This is the anti-regression gate — someone adding untested code breaks the build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pillar 3 — flaky discipline.&lt;/strong&gt; &lt;code&gt;@pytest.mark.flaky(reruns=2)&lt;/code&gt; (from &lt;code&gt;pytest-rerunfailures&lt;/code&gt;) auto-reruns failing tests up to twice; combined with a quarantine marker (&lt;code&gt;@pytest.mark.flaky_quarantined&lt;/code&gt;) and a nightly retriage job, this keeps CI green without hiding real failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;pytest-cov&lt;/code&gt; config — the four settings that matter.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--cov=mypkg&lt;/code&gt;.&lt;/strong&gt; Measure coverage on the &lt;code&gt;mypkg&lt;/code&gt; package. Use one flag per package; don't measure &lt;code&gt;tests/&lt;/code&gt; itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--cov-branch&lt;/code&gt;.&lt;/strong&gt; Include branch coverage — captures whether both sides of every &lt;code&gt;if&lt;/code&gt; have been executed. Line coverage alone hides "we tested the happy path but not the exception branch".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--cov-report=term-missing:skip-covered&lt;/code&gt;.&lt;/strong&gt; Prints uncovered line numbers in the terminal but skips fully-covered files — the report you actually read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--cov-fail-under=85&lt;/code&gt;.&lt;/strong&gt; The gate. Below 85% (or whatever threshold you commit to) the run fails. Ratchet upward as the suite matures — start at 70%, aim for 90%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;pytest-xdist&lt;/code&gt; distribution modes — pick the one that matches your fixture graph.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--dist=load&lt;/code&gt; (default).&lt;/strong&gt; Round-robin tests to workers as they become idle. Best for tests with no shared fixtures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--dist=loadscope&lt;/code&gt;.&lt;/strong&gt; Group tests by file / class / module. Ensures all tests in a file land on the same worker — useful when a module fixture is expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--dist=loadgroup&lt;/code&gt;.&lt;/strong&gt; Group tests by the &lt;code&gt;@pytest.mark.xdist_group("name")&lt;/code&gt; marker. The most flexible — pin all tests that share a Kafka topic to one worker, for example.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--dist=each&lt;/code&gt;.&lt;/strong&gt; Run every test on every worker. Only for very specific benchmarking / matrix cases; almost never right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Flaky-test discipline — the four-step protocol.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 1 — instrument.&lt;/strong&gt; Add &lt;code&gt;--junit-xml=junit.xml&lt;/code&gt; to every CI run; upload the artifact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2 — surface.&lt;/strong&gt; A nightly job parses the junit XMLs from the last N runs and flags tests with pass rate &amp;lt;99%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3 — quarantine.&lt;/strong&gt; Flagged tests get &lt;code&gt;@pytest.mark.flaky(reruns=2)&lt;/code&gt; immediately (fixes the visible CI break) and a &lt;code&gt;@pytest.mark.flaky_quarantined&lt;/code&gt; marker for tracking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4 — retriage.&lt;/strong&gt; A weekly ticket assigns each quarantined test to an owner; owner has 2 weeks to fix-or-delete. No test stays quarantined forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on pytest CI.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you measure coverage?" — &lt;code&gt;pytest-cov --cov=pkg --cov-branch --cov-fail-under=N&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"How do you parallelise safely?" — &lt;code&gt;pytest-xdist -n auto --dist=loadgroup&lt;/code&gt; with per-worker fixtures.&lt;/li&gt;
&lt;li&gt;"What's your flaky-test policy?" — rerun with &lt;code&gt;pytest-rerunfailures&lt;/code&gt;, quarantine, weekly retriage.&lt;/li&gt;
&lt;li&gt;"How do you keep DB fixtures xdist-safe?" — per-worker Postgres process (session-scoped in each worker) or per-worker COMPOSE_PROJECT_NAME.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — GitHub Actions matrix with coverage upload
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A canonical GitHub Actions workflow: matrix over Python 3.11 and 3.12, install deps, run pytest with xdist + coverage, upload junit XML + coverage HTML as artifacts, gate on &lt;code&gt;--cov-fail-under=85&lt;/code&gt;. Walk through the yaml.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Matrix.&lt;/strong&gt; Python 3.11 and 3.12 in parallel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runner.&lt;/strong&gt; ubuntu-24.04 (has Docker installed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache.&lt;/strong&gt; pip cache keyed on requirements hash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage upload.&lt;/strong&gt; GitHub artifact + optional Codecov push.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the &lt;code&gt;.github/workflows/tests.yml&lt;/code&gt; that runs the four-layer pytest suite with xdist and coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Matrix&lt;/td&gt;
&lt;td&gt;Python 3.11, 3.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xdist workers&lt;/td&gt;
&lt;td&gt;auto (CPU count)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage threshold&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Junit output&lt;/td&gt;
&lt;td&gt;junit.xml per matrix cell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/tests.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pytest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-24.04&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.11"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.python-version }}&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pip install -e '.[test]'&lt;/span&gt;
          &lt;span class="s"&gt;pip install pytest-cov pytest-xdist pytest-rerunfailures \&lt;/span&gt;
                      &lt;span class="s"&gt;pytest-postgresql testcontainers&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Postgres&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sudo apt-get install -y postgresql-15&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run pytest&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pytest \&lt;/span&gt;
            &lt;span class="s"&gt;-n auto \&lt;/span&gt;
            &lt;span class="s"&gt;--dist=loadgroup \&lt;/span&gt;
            &lt;span class="s"&gt;--cov=mypkg \&lt;/span&gt;
            &lt;span class="s"&gt;--cov-branch \&lt;/span&gt;
            &lt;span class="s"&gt;--cov-report=xml \&lt;/span&gt;
            &lt;span class="s"&gt;--cov-report=term-missing:skip-covered \&lt;/span&gt;
            &lt;span class="s"&gt;--cov-fail-under=85 \&lt;/span&gt;
            &lt;span class="s"&gt;--junit-xml=junit-${{ matrix.python-version }}.xml \&lt;/span&gt;
            &lt;span class="s"&gt;-v \&lt;/span&gt;
            &lt;span class="s"&gt;tests/&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload junit&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;junit-${{ matrix.python-version }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;junit-${{ matrix.python-version }}.xml&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload coverage HTML&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;coverage-${{ matrix.python-version }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;coverage.xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;strategy.matrix&lt;/code&gt; runs the workflow twice — once per Python version — in parallel. &lt;code&gt;fail-fast: false&lt;/code&gt; lets both cells complete even if one fails; you get coverage data from both.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;actions/setup-python@v5&lt;/code&gt; with &lt;code&gt;cache: pip&lt;/code&gt; uses GitHub's built-in pip cache, shaving ~30 s off cold installs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pytest -n auto&lt;/code&gt; uses &lt;code&gt;pytest-xdist&lt;/code&gt; to fan out to CPU count workers (ubuntu-24.04 default = 4 vCPUs). &lt;code&gt;--dist=loadgroup&lt;/code&gt; respects &lt;code&gt;@pytest.mark.xdist_group&lt;/code&gt; markers for fixture affinity.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--cov-fail-under=85&lt;/code&gt; fails the step if branch coverage drops below 85%. The step exits non-zero; GitHub marks the check as failed; the PR is blocked from merge.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;if: always()&lt;/code&gt; on the artifact-upload steps ensures junit + coverage are uploaded even when the pytest step failed — you get the data to diagnose the failure. This is a small detail that saves hours of debugging.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cell&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Tests run&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Python 3.11&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;td&gt;1,290&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python 3.12&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;td&gt;1,290&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total wall clock&lt;/td&gt;
&lt;td&gt;~5 min (parallel cells)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifacts&lt;/td&gt;
&lt;td&gt;junit-3.11, junit-3.12, coverage-3.11, coverage-3.12&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For DE pytest CI, use a Python-version matrix with &lt;code&gt;cache: pip&lt;/code&gt;, &lt;code&gt;pytest -n auto&lt;/code&gt;, &lt;code&gt;--cov-fail-under=&amp;lt;N&amp;gt;&lt;/code&gt;, and &lt;code&gt;if: always()&lt;/code&gt; on artifact uploads. This is the minimum viable modern setup.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — xdist + docker-compose shared across workers
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The subtle problem: session-scoped fixtures are per-&lt;em&gt;worker&lt;/em&gt; in xdist, not per-&lt;em&gt;session&lt;/em&gt;. If your compose stack is session-scoped, it starts N times (once per worker). Sometimes that's what you want (isolation); sometimes you want the compose stack shared across all workers (for cost reasons). The &lt;code&gt;pytest-xdist&lt;/code&gt; idiom &lt;code&gt;tmp_path_factory&lt;/code&gt; + a lockfile solves the "shared across workers" case. Walk through the pattern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem.&lt;/strong&gt; &lt;code&gt;@pytest.fixture(scope="session")&lt;/code&gt; on &lt;code&gt;compose_stack&lt;/code&gt; starts one compose stack per worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution.&lt;/strong&gt; Use a filesystem lock on a shared &lt;code&gt;tmp_path_factory&lt;/code&gt; directory to elect one worker to start compose; other workers block until ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When.&lt;/strong&gt; When compose startup is &amp;gt;30 s and you're xdist-parallelising past 4 workers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the shared-compose fixture using a filesystem lock so N workers use one compose stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lock file&lt;/td&gt;
&lt;td&gt;tmp_path_factory / "compose.lock"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ready file&lt;/td&gt;
&lt;td&gt;tmp_path_factory / "compose.ready"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compose stack&lt;/td&gt;
&lt;td&gt;docker-compose.test.yml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Election&lt;/td&gt;
&lt;td&gt;one worker starts; others poll ready file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/conftest.py — cross-worker shared compose fixture
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;testcontainers.compose&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DockerCompose&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compose_stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path_factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;worker_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One compose stack shared across all xdist workers.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# tmp_path_factory.getbasetemp() is shared across workers
&lt;/span&gt;    &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tmp_path_factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getbasetemp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;lock_path&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compose.lock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;ready_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compose.ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;worker_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;master&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Serial pytest — no election needed
&lt;/span&gt;        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DockerCompose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compose_file_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose.test.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9001/minio/health/ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="c1"&gt;# Parallel — acquire lock
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;lock_fp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock_fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_EX&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_NB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# We are the elected worker — start compose
&lt;/span&gt;            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPOSE_PROJECT_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_shared&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;compose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DockerCompose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compose_file_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker-compose.test.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;_wait_ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;ready_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;
            &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;ready_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;BlockingIOError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Some other worker started it; wait for ready file
&lt;/span&gt;            &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ready_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ready_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compose never came ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Yield a lightweight reference (compose is up)
&lt;/span&gt;            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;_AttachedCompose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_shared&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_wait_ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Poll all healthchecks; block until green.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# (implementation elided; use compose.wait_for or per-service polls)
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_AttachedCompose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Read-only view of a compose stack another worker owns.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;project_name&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_service_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When pytest runs under xdist, &lt;code&gt;worker_id&lt;/code&gt; is &lt;code&gt;gw0&lt;/code&gt;, &lt;code&gt;gw1&lt;/code&gt;, ... — one per worker. When pytest runs serial, &lt;code&gt;worker_id&lt;/code&gt; is &lt;code&gt;master&lt;/code&gt;. The fixture branches on this.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tmp_path_factory.getbasetemp()&lt;/code&gt; returns a per-worker temp dir, but its &lt;code&gt;.parent&lt;/code&gt; is the &lt;em&gt;shared&lt;/em&gt; base (&lt;code&gt;/tmp/pytest-of-user&lt;/code&gt;). Using the shared parent as the lock location lets multiple workers coordinate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fcntl.flock(..., LOCK_EX | LOCK_NB)&lt;/code&gt; acquires an exclusive lock non-blockingly. Only one worker succeeds; the others get &lt;code&gt;BlockingIOError&lt;/code&gt; and fall into the "wait for ready" branch.&lt;/li&gt;
&lt;li&gt;The elected worker starts compose, waits for readiness, writes a &lt;code&gt;compose.ready&lt;/code&gt; sentinel file, and yields the live compose object. At teardown, it stops compose and cleans the sentinel.&lt;/li&gt;
&lt;li&gt;Non-elected workers spin-wait on &lt;code&gt;ready_path.exists()&lt;/code&gt; for up to 120 s. Once ready, they yield an &lt;code&gt;_AttachedCompose&lt;/code&gt; proxy that lets tests query service ports via &lt;code&gt;docker compose port&lt;/code&gt; — no actual container control.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Worker&lt;/th&gt;
&lt;th&gt;Elected?&lt;/th&gt;
&lt;th&gt;Compose action&lt;/th&gt;
&lt;th&gt;Wait&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gw0&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;start compose&lt;/td&gt;
&lt;td&gt;writes ready file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gw1&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;attaches&lt;/td&gt;
&lt;td&gt;waits ~15 s for ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gw2&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;attaches&lt;/td&gt;
&lt;td&gt;waits ~15 s for ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gw3&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;attaches&lt;/td&gt;
&lt;td&gt;waits ~15 s for ready&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For very-expensive compose stacks (&amp;gt;30 s startup) and 4+ xdist workers, elect one worker to start compose and have others attach. For cheaper stacks (&amp;lt;15 s) or ≤2 workers, per-worker isolation is simpler and safer.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — flaky-test dashboard from &lt;code&gt;--junit-xml&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A tiny Python script that ingests the last N junit XMLs and builds a per-test pass-rate report. Flag anything with pass rate &amp;lt; 99% over the last 20 runs. Walk through the parser + report.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input.&lt;/strong&gt; &lt;code&gt;junit-*.xml&lt;/code&gt; files from the last 20 CI runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parser.&lt;/strong&gt; &lt;code&gt;xml.etree.ElementTree&lt;/code&gt; — each &lt;code&gt;&amp;lt;testcase&amp;gt;&lt;/code&gt; element is one test outcome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregation.&lt;/strong&gt; Group by test node id; compute pass rate = passed / total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report.&lt;/strong&gt; Sorted by pass rate ascending; flag &amp;lt; 99%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the parser + reporter for the flaky-test dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input glob&lt;/td&gt;
&lt;td&gt;reports/junit-*.xml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test key&lt;/td&gt;
&lt;td&gt;classname::name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure marker&lt;/td&gt;
&lt;td&gt; or  child&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threshold&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tools/flaky_dashboard.py
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Compute per-test pass rates from junit XMLs and flag flakes.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;xml.etree.ElementTree&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_junit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return [(test_id, passed?), ...] from one junit XML.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getroot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;testcase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;tid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;classname&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;::&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compute_pass_rates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Aggregate pass rates over all runs.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;tallies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;parse_junit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;tallies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tallies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flag_flakes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return tests below the pass-rate threshold, worst first.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;flakes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;flakes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;flakes&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reports/junit-*.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_pass_rates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;flakes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;flag_flakes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;flakes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK — all &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tests &amp;gt;= 99% pass rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FLAKY — &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flakes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tests below 99% over &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; runs:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pass_rate&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  test_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flakes&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;9.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;parse_junit&lt;/code&gt; walks the XML and extracts every &lt;code&gt;&amp;lt;testcase&amp;gt;&lt;/code&gt;. A test &lt;em&gt;passed&lt;/em&gt; iff it has no &lt;code&gt;&amp;lt;failure&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;error&amp;gt;&lt;/code&gt; child. Skipped tests (which have &lt;code&gt;&amp;lt;skipped&amp;gt;&lt;/code&gt;) are counted as passed — they didn't fail.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;compute_pass_rates&lt;/code&gt; aggregates across all junit files. Tests seen fewer than 5 times are filtered out (insufficient data to call flake vs new). The pass rate is &lt;code&gt;sum(passed) / len(runs)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flag_flakes&lt;/code&gt; returns tests below the threshold, sorted worst first — the flakiest tests appear at the top of the report and get owner attention first.&lt;/li&gt;
&lt;li&gt;The main function glob-collects &lt;code&gt;reports/junit-*.xml&lt;/code&gt; (uploaded by CI) and prints the top 20 offenders. Wire this into a nightly cron job that pushes the report to Slack or a dashboard.&lt;/li&gt;
&lt;li&gt;To integrate with pytest-rerunfailures, this report drives which tests get &lt;code&gt;@pytest.mark.flaky(reruns=2)&lt;/code&gt; added — a manual review by a triage owner turns "reported flake" into "quarantined test with a tracking ticket."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FLAKY — 3 tests below 99% over 20 runs:
  pass_rate  test_id
      75.0%  tests.integration.test_kafka::test_kafka_produce_and_consume
      85.0%  tests.integration.test_dag::test_ingest_daily_orders_dag_succeeds
      95.0%  tests.component.test_pg::test_bulk_status_update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any DE test suite past ~200 tests, ship a flaky-test dashboard from &lt;code&gt;--junit-xml&lt;/code&gt;. Manual pass-rate tracking doesn't scale; automated flagging + a weekly retriage keeps the suite honest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on pytest CI and flaky-test discipline
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your team's pytest CI runs serially, has no coverage gate, and has 15 tests that flake ~10% of the time — devs have started &lt;code&gt;-p 'not integration'&lt;/code&gt;-skipping the suite on their branches. Walk me through the two-week focus block that gets you to &lt;code&gt;pytest -n auto --cov-fail-under=85&lt;/code&gt; with a quarantine + retriage process, and quantify the CI budget savings."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using xdist + coverage gate + rerunfailures + quarantine markers + nightly retriage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. pytest.ini — strict-marker gate + coverage + rerunfailures defaults
&lt;/span&gt;&lt;span class="nn"&gt;[pytest]&lt;/span&gt;
&lt;span class="py"&gt;minversion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;8.0&lt;/span&gt;
&lt;span class="py"&gt;addopts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="err"&gt;-ra&lt;/span&gt;
    &lt;span class="err"&gt;-q&lt;/span&gt;
    &lt;span class="err"&gt;--strict-markers&lt;/span&gt;
    &lt;span class="err"&gt;--strict-config&lt;/span&gt;
    &lt;span class="py"&gt;--cov&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;mypkg&lt;/span&gt;
    &lt;span class="err"&gt;--cov-branch&lt;/span&gt;
    &lt;span class="py"&gt;--cov-report&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;term-missing:skip-covered&lt;/span&gt;
    &lt;span class="py"&gt;--cov-report&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;xml&lt;/span&gt;
    &lt;span class="py"&gt;--cov-fail-under&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;85&lt;/span&gt;
    &lt;span class="py"&gt;--junit-xml&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;junit.xml&lt;/span&gt;

&lt;span class="py"&gt;markers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="err"&gt;unit:&lt;/span&gt;              &lt;span class="err"&gt;base-layer&lt;/span&gt; &lt;span class="err"&gt;pure-function&lt;/span&gt; &lt;span class="err"&gt;tests&lt;/span&gt;
    &lt;span class="err"&gt;contract:&lt;/span&gt;          &lt;span class="err"&gt;schema&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="err"&gt;pydantic&lt;/span&gt; &lt;span class="err"&gt;contract&lt;/span&gt; &lt;span class="err"&gt;tests&lt;/span&gt;
    &lt;span class="err"&gt;component:&lt;/span&gt;         &lt;span class="err"&gt;Layer-3&lt;/span&gt; &lt;span class="err"&gt;pytest-postgresql&lt;/span&gt; &lt;span class="err"&gt;tests&lt;/span&gt;
    &lt;span class="err"&gt;integration:&lt;/span&gt;       &lt;span class="err"&gt;Layer-4&lt;/span&gt; &lt;span class="err"&gt;docker-compose&lt;/span&gt; &lt;span class="err"&gt;tests&lt;/span&gt;
    &lt;span class="err"&gt;flaky:&lt;/span&gt;             &lt;span class="err"&gt;tests&lt;/span&gt; &lt;span class="err"&gt;known&lt;/span&gt; &lt;span class="err"&gt;to&lt;/span&gt; &lt;span class="err"&gt;flake&lt;/span&gt;&lt;span class="c"&gt;; get reruns=2
&lt;/span&gt;    &lt;span class="err"&gt;flaky_quarantined:&lt;/span&gt; &lt;span class="err"&gt;assigned&lt;/span&gt; &lt;span class="err"&gt;to&lt;/span&gt; &lt;span class="err"&gt;an&lt;/span&gt; &lt;span class="err"&gt;owner&lt;/span&gt; &lt;span class="err"&gt;for&lt;/span&gt; &lt;span class="err"&gt;fix-or-delete&lt;/span&gt;

&lt;span class="py"&gt;testpaths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. tests/conftest.py — auto-apply reruns for @pytest.mark.flaky
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pytest_collection_modifyitems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Attach reruns=2 to every test carrying the flaky marker.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_closest_marker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flaky&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_marker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flaky_quarantined&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# for tracking
&lt;/span&gt;            &lt;span class="c1"&gt;# pytest-rerunfailures picks up the reruns param
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 3. .github/workflows/tests.yml — parallel matrix + artifact uploads&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pytest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-24.04&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;python&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.11"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;matrix.python&lt;/span&gt; &lt;span class="pi"&gt;}},&lt;/span&gt; &lt;span class="nv"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;pip&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -e '.[test]'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sudo apt-get install -y postgresql-15&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pytest -n auto --dist=loadgroup --reruns 2 tests/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;junit-${{ matrix.python }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;junit.xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 4. .github/workflows/flaky-retriage.yml — nightly retriage&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flaky-retriage&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;      &lt;span class="c1"&gt;# 06:00 UTC daily&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;retriage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-24.04&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download last 20 junit artifacts&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const artifacts = await github.rest.actions.listArtifactsForRepo({&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner, repo: context.repo.repo, per_page: 100,&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;
            &lt;span class="s"&gt;// (download loop elided)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python tools/flaky_dashboard.py &amp;gt; flaky_report.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post to Slack&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -X POST $SLACK_WEBHOOK -d "text=$(cat flaky_report.txt)"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SLACK_WEBHOOK&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SLACK_WEBHOOK }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 5. Example flaky-quarantined test
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.mark.flaky&lt;/span&gt;
&lt;span class="nd"&gt;@pytest.mark.integration&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_kafka_lag_below_threshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kafka_container&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Known flaky under CPU pressure — reruns=2 attached automatically.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# ... test body ...
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;measure_lag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pytest.ini&lt;/td&gt;
&lt;td&gt;strict-markers + coverage gate&lt;/td&gt;
&lt;td&gt;fails PRs missing coverage or with unknown markers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;conftest.py&lt;/td&gt;
&lt;td&gt;
&lt;a class="mentioned-user" href="https://dev.to/flaky"&gt;@flaky&lt;/a&gt; → auto-quarantine marker&lt;/td&gt;
&lt;td&gt;tracks quarantined tests without hiding them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tests.yml&lt;/td&gt;
&lt;td&gt;xdist + reruns + junit upload&lt;/td&gt;
&lt;td&gt;~5 min CI, junit stored per run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flaky-retriage.yml&lt;/td&gt;
&lt;td&gt;nightly parse + Slack&lt;/td&gt;
&lt;td&gt;flaky tests surfaced daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Example test&lt;/td&gt;
&lt;td&gt;@pytest.mark.flaky&lt;/td&gt;
&lt;td&gt;rerun up to 2× on failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;td&gt;manual retriage of Slack digest&lt;/td&gt;
&lt;td&gt;owners assigned; fix-or-delete in 2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the two-week focus block, CI runtime drops from ~25 minutes serial to ~5 minutes parallel; coverage gate blocks PRs below 85%; flaky tests get reruns automatically and a Slack ping if they persist; the retriage ticket ensures nothing stays flaky forever. Dev unblock: teams can trust &lt;code&gt;pytest -n auto&lt;/code&gt; as the pre-push check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CI runtime&lt;/td&gt;
&lt;td&gt;25 min&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage gate&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;85% branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flaky visibility&lt;/td&gt;
&lt;td&gt;ad-hoc&lt;/td&gt;
&lt;td&gt;nightly Slack digest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quarantine tracking&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@pytest.mark.flaky_quarantined&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Devs skipping suite&lt;/td&gt;
&lt;td&gt;~30%&lt;/td&gt;
&lt;td&gt;~0% (fast enough to always run)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRs blocked by real bugs&lt;/td&gt;
&lt;td&gt;~40% caught&lt;/td&gt;
&lt;td&gt;~85% caught&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;xdist parallelism&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;-n auto --dist=loadgroup&lt;/code&gt; scales test throughput linearly with CPU count. &lt;code&gt;loadgroup&lt;/code&gt; distributes tests grouped by &lt;code&gt;xdist_group&lt;/code&gt; marker for fixture affinity, avoiding container-collision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Coverage gate&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;--cov-fail-under=85&lt;/code&gt; is the anti-regression ratchet. PRs adding untested code fail; PRs improving coverage pass. The gate is what makes coverage a leading indicator instead of a lagging one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;pytest-rerunfailures&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;@pytest.mark.flaky&lt;/code&gt; auto-reruns; &lt;code&gt;--reruns 2&lt;/code&gt; at the CLI applies globally. Buys time to fix flakes without red CI blocking merges. Combined with a quarantine marker for tracking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Junit dashboard&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;--junit-xml&lt;/code&gt; per run + a nightly parser + Slack ping = flaky tests get surfaced daily. No test hides in a "we always retry it" limbo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — pytest-xdist (one process per CPU), pytest-cov (~3% CPU overhead), pytest-rerunfailures (near-zero), junit XML (~1 KB per test). The eliminated cost is 20 minutes of serial CI per PR + hours of debugging flaky failures + weeks of hidden coverage regressions. Net: O(1) developer time to fix a flake vs O(N) developer time to debug it silently. Two-week focus block, ~1 dev-week of net cost, pays back in ~1 month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Python&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — exception-handling&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Exception handling and CI-hardening tests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/exception-handling" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;Python&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — python&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Python testing and CI-workflow problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/python" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — pytest recipes for data engineering
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Four-layer test pyramid.&lt;/strong&gt; Layer 1 unit (pure functions, no I/O, sub-ms per test, hundreds of tests, run on save), Layer 2 contract (dbt schema tests, pydantic validation, ~1 ms per test), Layer 3 component-with-DB (&lt;code&gt;pytest-postgresql&lt;/code&gt; template clone, ~100 ms per test), Layer 4 integration-with-compose (&lt;code&gt;testcontainers-python&lt;/code&gt; session compose, ~5-30 s per test, run on PR + nightly). The pyramid shape — thousands of Layer 1, dozens of Layer 4 — is what keeps CI under 8 minutes for a 1000+ test data-platform suite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixture scope decision matrix.&lt;/strong&gt; Use &lt;code&gt;function&lt;/code&gt; scope for anything that mutates during the test (fresh DB row, tmp file, monkeypatched env). Use &lt;code&gt;session&lt;/code&gt; scope for anything with &amp;gt;1 s setup (Postgres process, compose stack, dbt manifest). Use &lt;code&gt;module&lt;/code&gt; scope for tests-in-one-file that share a large seed. Avoid &lt;code&gt;class&lt;/code&gt; scope in modern pytest; avoid &lt;code&gt;autouse=True&lt;/code&gt; except for cross-cutting concerns like UTC timezone or logger reset — never for I/O fixtures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest-postgresql&lt;/code&gt; template-clone recipe.&lt;/strong&gt; &lt;code&gt;postgresql_proc = factories.postgresql_proc(port=None, unixsocketdir="/tmp", postgres_options="-c fsync=off -c synchronous_commit=off -c full_page_writes=off")&lt;/code&gt; at session scope; &lt;code&gt;postgresql = factories.postgresql("postgresql_proc", dbname="test_db", load=[SCHEMA_SQL])&lt;/code&gt; at function scope. The &lt;code&gt;port=None&lt;/code&gt; is what makes it xdist-safe; the durability flags cut commit latency by ~5×; the template clone is a Postgres-native file-block copy at ~100 ms per test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Factory-as-fixture template.&lt;/strong&gt; &lt;code&gt;@pytest.fixture def make_order(postgresql): def _make(customer_id=1, total_cents=1000, status="pending"): with postgresql.cursor() as cur: cur.execute("INSERT INTO orders(...) VALUES(...) RETURNING id", (...)); return Order(id=..., ...); return _make&lt;/code&gt;. Every DE table gets a factory; factories close over the DB fixture; tests call the factory zero or more times per test with defaulted-and-overridable kwargs; cleanup is automatic via the template-clone drop at teardown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@pytest.mark.parametrize&lt;/code&gt; with &lt;code&gt;ids=&lt;/code&gt;.&lt;/strong&gt; Always supply &lt;code&gt;ids=&lt;/code&gt; for parametrizations with more than 3 rows. Prefer readable strings (&lt;code&gt;ids=["null-in", "empty-str", "valid-with-punct"]&lt;/code&gt;) or &lt;code&gt;ids=lambda c: c.name&lt;/code&gt; when parametrizing over a dataclass. Never accept the default &lt;code&gt;test_foo[0]&lt;/code&gt; / &lt;code&gt;test_foo[test_foo0]&lt;/code&gt; IDs — they turn every failing test into a debugging tax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest_generate_tests&lt;/code&gt; for dynamic axes.&lt;/strong&gt; &lt;code&gt;def pytest_generate_tests(metafunc): if "table_name" in metafunc.fixturenames: metafunc.parametrize("table_name", discover_tables(), ids=discover_tables())&lt;/code&gt;. Use for manifest-driven axes (every model in the dbt manifest, every table in &lt;code&gt;information_schema&lt;/code&gt;, every YAML config in &lt;code&gt;configs/&lt;/code&gt;). Amplifies one test function into N tests without hand-writing them; new axis entries are automatically covered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;testcontainers-python&lt;/code&gt; compose fixture.&lt;/strong&gt; &lt;code&gt;@pytest.fixture(scope="session") def compose_stack(worker_id): os.environ["COMPOSE_PROJECT_NAME"] = f"test_{worker_id}"; with DockerCompose(".", compose_file_name="docker-compose.test.yml", pull=True, wait=True) as compose: compose.wait_for("http://localhost:9001/minio/health/ready"); yield compose&lt;/code&gt;. The per-worker project name is what makes xdist safe (isolated networks, no port collision); &lt;code&gt;wait=True&lt;/code&gt; blocks on healthchecks; the &lt;code&gt;with&lt;/code&gt; statement handles &lt;code&gt;docker-compose down -v&lt;/code&gt; at teardown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait-for-ready idiom.&lt;/strong&gt; Never trust &lt;code&gt;docker-compose up -d&lt;/code&gt; alone — the containers are &lt;em&gt;started&lt;/em&gt;, not &lt;em&gt;ready&lt;/em&gt;. Use &lt;code&gt;compose.wait_for("http://.../health")&lt;/code&gt; for HTTP-endpoint services (MinIO, Airflow webserver, Elasticsearch) and a &lt;code&gt;SELECT 1&lt;/code&gt; retry loop with &lt;code&gt;tenacity&lt;/code&gt; for raw Postgres or Kafka. Timeout at 30 s; log clearly on failure ("postgres never became ready"); belt-and-braces both patterns for high-value fixtures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest-xdist&lt;/code&gt; distribution modes.&lt;/strong&gt; &lt;code&gt;-n auto&lt;/code&gt; for CPU-count workers; &lt;code&gt;--dist=load&lt;/code&gt; default is round-robin per test; &lt;code&gt;--dist=loadgroup&lt;/code&gt; respects &lt;code&gt;@pytest.mark.xdist_group("name")&lt;/code&gt; for fixture affinity (pin all Kafka-consumer tests to one worker); &lt;code&gt;--dist=loadscope&lt;/code&gt; groups by file/class. Session-scoped fixtures are per-worker, so each worker gets its own Postgres process — this is the correct default; only elect a shared-across-workers stack via a filesystem lock when compose startup &amp;gt;30 s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest-cov&lt;/code&gt; config.&lt;/strong&gt; &lt;code&gt;--cov=mypkg --cov-branch --cov-report=term-missing:skip-covered --cov-report=xml --cov-fail-under=85&lt;/code&gt;. Line-only coverage lies about happy-path bias; branch coverage catches "we tested the if but not the else". &lt;code&gt;term-missing:skip-covered&lt;/code&gt; prints only files with gaps; xml report feeds Codecov / SonarQube. Start &lt;code&gt;--cov-fail-under&lt;/code&gt; at 70%, ratchet to 85%, aim for 90% on the core packages; never &lt;em&gt;lower&lt;/em&gt; the threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest-rerunfailures&lt;/code&gt; flaky discipline.&lt;/strong&gt; &lt;code&gt;@pytest.mark.flaky(reruns=2)&lt;/code&gt; on the test (or CLI &lt;code&gt;--reruns 2&lt;/code&gt; globally); combined with a &lt;code&gt;@pytest.mark.flaky_quarantined&lt;/code&gt; tracking marker (auto-applied via &lt;code&gt;pytest_collection_modifyitems&lt;/code&gt;). A nightly job parses &lt;code&gt;--junit-xml&lt;/code&gt; files, computes per-test pass rates, and posts &amp;lt;99% offenders to Slack. Weekly retriage: owner assigned; fix-or-delete deadline; no test stays quarantined forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;monkeypatch&lt;/code&gt; + &lt;code&gt;tmp_path&lt;/code&gt; isolation.&lt;/strong&gt; For any test touching env vars, cwd, or filesystem writes, use &lt;code&gt;monkeypatch.setenv("KEY", "value")&lt;/code&gt; (auto-reverts), &lt;code&gt;monkeypatch.chdir(tmp_path)&lt;/code&gt; (auto-reverts), and &lt;code&gt;tmp_path&lt;/code&gt; (auto-deletes). Never modify &lt;code&gt;os.environ&lt;/code&gt; or &lt;code&gt;os.chdir&lt;/code&gt; directly — global-state leakage between tests is the #2 source of "works on my machine" flakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions minimal recipe.&lt;/strong&gt; &lt;code&gt;strategy.matrix.python: ["3.11", "3.12"]&lt;/code&gt; on &lt;code&gt;ubuntu-24.04&lt;/code&gt;; &lt;code&gt;actions/setup-python@v5&lt;/code&gt; with &lt;code&gt;cache: pip&lt;/code&gt;; install postgresql-15 via apt for &lt;code&gt;pytest-postgresql&lt;/code&gt;; &lt;code&gt;pytest -n auto --dist=loadgroup --cov-fail-under=85 --junit-xml=junit.xml&lt;/code&gt;; &lt;code&gt;if: always()&lt;/code&gt; on artifact uploads for junit + coverage. Wire the Slack notifier separately as a nightly cron job on the retriage workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airflow / dbt / Spark specific gotchas.&lt;/strong&gt; Airflow DAG tests need the &lt;code&gt;airflow-init&lt;/code&gt; service to have &lt;code&gt;condition: service_completed_successfully&lt;/code&gt; before the scheduler starts. dbt tests need &lt;code&gt;monkeypatch.chdir(project_root)&lt;/code&gt; because &lt;code&gt;dbt_project.yml&lt;/code&gt; is discovered from cwd. Spark UDF tests can use a session-scoped &lt;code&gt;SparkSession&lt;/code&gt; fixture with &lt;code&gt;.master("local[2]")&lt;/code&gt; — never &lt;code&gt;local[*]&lt;/code&gt; in tests because it stalls other xdist workers. All three: pin the version in requirements-test.txt; version-drift is a silent flaky-test source.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is pytest in one sentence for a data engineer?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Pytest for data engineering&lt;/code&gt; is the Python test framework that unifies four test layers under one runner: unit tests for pure transforms, contract tests for dbt/pydantic schemas, component tests for SQL-emitting code against a real Postgres via &lt;code&gt;pytest-postgresql&lt;/code&gt;, and integration tests for end-to-end pipelines spun up via &lt;code&gt;pytest docker-compose&lt;/code&gt; or &lt;code&gt;pytest testcontainers&lt;/code&gt;. Every plugin senior DE teams reach for — &lt;code&gt;pytest-xdist&lt;/code&gt; for parallelism, &lt;code&gt;pytest-cov&lt;/code&gt; for coverage, &lt;code&gt;pytest-rerunfailures&lt;/code&gt; for flaky discipline, &lt;code&gt;pytest-postgresql&lt;/code&gt; for template DBs, &lt;code&gt;testcontainers-python&lt;/code&gt; for Kafka/MinIO/Airflow stacks — plugs into pytest's fixture graph without ceremony, and the same &lt;code&gt;pytest&lt;/code&gt; command runs on a laptop, in a Codespace, and on GitHub Actions with identical behaviour. It is the load-bearing tool that makes a 1000-test data-platform suite feasible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pytest fixtures vs unittest setUp — which do I pick in 2026?
&lt;/h3&gt;

&lt;p&gt;Pick &lt;code&gt;pytest fixtures&lt;/code&gt; for every greenfield project and every serious refactor. &lt;code&gt;pytest fixtures&lt;/code&gt; compose (fixture A can request fixture B), have explicit scope (&lt;code&gt;function&lt;/code&gt; / &lt;code&gt;module&lt;/code&gt; / &lt;code&gt;session&lt;/code&gt;), support yield-teardown (fixture body reads top-to-bottom like a &lt;code&gt;contextmanager&lt;/code&gt;), and are parametrizable (&lt;code&gt;@pytest.fixture(params=[...])&lt;/code&gt;). &lt;code&gt;unittest.setUp&lt;/code&gt; / &lt;code&gt;tearDown&lt;/code&gt; do none of those — they're per-class-method, imperative, and force you into &lt;code&gt;class TestFoo(unittest.TestCase):&lt;/code&gt; boilerplate. The only reason to touch &lt;code&gt;unittest&lt;/code&gt; in 2026 is if you inherit a legacy suite and the migration cost exceeds the improvement (rare; pytest can run &lt;code&gt;unittest&lt;/code&gt;-style tests natively, so you can migrate one file at a time). Every senior DE codebase has converged on pytest fixtures because scope discipline is what makes the difference between a 2-minute test run and a 20-minute one — and only pytest exposes scope as a first-class primitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I parametrize a pytest test over dbt models?
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;pytest_generate_tests(metafunc)&lt;/code&gt; to discover models from the dbt manifest at collection time and inject them as a parameter. Concretely: implement &lt;code&gt;def pytest_generate_tests(metafunc): if "dbt_model" in metafunc.fixturenames: models = [n["name"] for n in json.loads(Path("target/manifest.json").read_text())["nodes"].values() if n["resource_type"]=="model"]; metafunc.parametrize("dbt_model", models, ids=models)&lt;/code&gt;. Then any test that names &lt;code&gt;dbt_model&lt;/code&gt; as a parameter runs once per model with a readable ID like &lt;code&gt;test_mart_has_row_count_gt_zero[fct_orders]&lt;/code&gt;. Adding a new dbt model automatically adds a new test — zero test-file changes required. This is the amplifier that turns one contract test into hundreds and lets you enforce invariants like "every mart has ≥1 row" or "every staging model has a &lt;code&gt;_ingested_at&lt;/code&gt; column" across the entire warehouse.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between docker-compose fixtures and testcontainers-python?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker-compose&lt;/code&gt; fixtures (via &lt;code&gt;testcontainers.compose.DockerCompose&lt;/code&gt; or the older &lt;code&gt;pytest-docker-compose&lt;/code&gt; plugin) wrap a &lt;code&gt;docker-compose.yml&lt;/code&gt; file — best when your test needs the full networked stack (Airflow scheduler + webserver + Postgres + Redis + Celery, or Kafka + Zookeeper + Schema Registry). &lt;code&gt;testcontainers-python&lt;/code&gt; per-service (&lt;code&gt;PostgresContainer&lt;/code&gt;, &lt;code&gt;KafkaContainer&lt;/code&gt;, &lt;code&gt;MinioContainer&lt;/code&gt;) instantiates one container at a time in Python code — best when your test needs one or two containers with programmatic setup. Both handle wait-for-ready health checks and clean teardown; both are &lt;code&gt;pytest docker-compose&lt;/code&gt;-compatible in the sense that the underlying tech is Docker. Rule of thumb: if your &lt;code&gt;docker-compose.yml&lt;/code&gt; has ≥3 services, use &lt;code&gt;DockerCompose&lt;/code&gt;; if it's 1-2 services, use per-service &lt;code&gt;testcontainers-python&lt;/code&gt; and skip the yaml file. Both patterns are session-scoped; both need per-test isolation via schemas (Postgres) or uuid-suffixed topics (Kafka).&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I run pytest in parallel with pytest-xdist without breaking Postgres fixtures?
&lt;/h3&gt;

&lt;p&gt;Two rules: (1) make the Postgres fixture per-worker via session scope + OS-allocated port (&lt;code&gt;factories.postgresql_proc(port=None)&lt;/code&gt; for pytest-postgresql, or &lt;code&gt;COMPOSE_PROJECT_NAME=test_{worker_id}&lt;/code&gt; for docker-compose); (2) make every DB itself per-test via template clone or per-test schema (&lt;code&gt;CREATE SCHEMA t_&amp;lt;uuid&amp;gt;&lt;/code&gt;). Concretely, &lt;code&gt;pytest -n auto --dist=loadgroup&lt;/code&gt; spawns one worker per CPU; each worker gets its own session-scoped Postgres process on its own port; each test in a worker clones a fresh DB from the template. Under 4-worker xdist on an 8-core machine, a 400-test suite goes from ~2 minutes serial to ~30 seconds — with full isolation, no shared state, no port collisions. The &lt;code&gt;--dist=loadgroup&lt;/code&gt; mode respects &lt;code&gt;@pytest.mark.xdist_group("name")&lt;/code&gt; markers so all tests sharing a Kafka topic can pin to one worker (fixture affinity). Never try to share a single Postgres process across workers via a filesystem lock unless you've measured that it's actually necessary — per-worker Postgres is the simpler, safer default.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I test an Airflow DAG with pytest?
&lt;/h3&gt;

&lt;p&gt;Use a session-scoped &lt;code&gt;DockerCompose&lt;/code&gt; fixture with a minimal Airflow compose file — postgres for metadata + airflow-init (runs &lt;code&gt;airflow db init&lt;/code&gt;, exits) + airflow-scheduler + optionally airflow-webserver. The compose file uses &lt;code&gt;depends_on: airflow-init: condition: service_completed_successfully&lt;/code&gt; so the scheduler waits for init to finish. Test code triggers a DAG run via &lt;code&gt;subprocess.check_output(["docker", "exec", "airflow-scheduler", "airflow", "dags", "trigger", "my_dag", "--run-id", f"test_{uuid.uuid4().hex[:8]}"])&lt;/code&gt;, then polls &lt;code&gt;airflow dags state my_dag &amp;lt;run_id&amp;gt;&lt;/code&gt; every 2 seconds up to a 60-second deadline. Assert on the final state (&lt;code&gt;success&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;). For unit-testing individual tasks in isolation, use Airflow's &lt;code&gt;Task.test()&lt;/code&gt; method (no compose needed) or the &lt;code&gt;@task&lt;/code&gt;-decorator's &lt;code&gt;.function()&lt;/code&gt; accessor to call the underlying Python directly. For end-to-end DAG behaviour where you need the scheduler + metadata DB, the docker-compose pattern is the standard; ~30-second startup amortised across all DAG tests via session scope keeps the suite fast enough to run on every PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/python" rel="noopener noreferrer"&gt;Python practice library →&lt;/a&gt; for the pytest, fixture-graph, parametrize, and mocking problems senior interviewers love.&lt;/li&gt;
&lt;li&gt;Rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt; for the ingestion-pipeline testing, DB-fixture design, and integration-suite scenarios.&lt;/li&gt;
&lt;li&gt;Sharpen the validation axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;data-validation practice library →&lt;/a&gt; for the contract-test, schema-assertion, and dbt-model-verification patterns.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the four-layer testing decision matrix against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lock in pytest-for-DE muscle memory&lt;/h3&gt;

&lt;p&gt;Docs explain fixtures. PipeCode drills explain the decision — when session-scope makes the difference between a 2-minute and a 20-minute suite, when parametrize with readable ids beats copy-pasted tests, when testcontainers-python earns its place over a shell-out to docker-compose, when the coverage gate + rerunfailures + quarantine protocol turns a flaky suite green. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — pattern-first practice tuned for the production testing trade-offs senior data engineers actually face.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/python" rel="noopener noreferrer"&gt;Practice Python problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice ETL problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>LLM Evaluation for Data Pipelines: LangSmith, TruLens, Ragas &amp; Snowflake Cortex Search Ops</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 31 Jul 2026 04:28:51 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/llm-evaluation-for-data-pipelines-langsmith-trulens-ragas-snowflake-cortex-search-ops-n48</link>
      <guid>https://dev.to/gowthampotureddi/llm-evaluation-for-data-pipelines-langsmith-trulens-ragas-snowflake-cortex-search-ops-n48</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;llm evaluation for data pipelines&lt;/code&gt;&lt;/strong&gt; is the load-bearing correctness discipline of the 2026 data stack — the difference between a RAG chatbot that quietly regresses for three weeks before a customer notices and a RAG chatbot whose retrieval quality is gated on every pull request. Every LLM call your pipeline makes — the summariser inside a nightly ingestion DAG, the classifier fronting a Kafka stream, the RAG chain answering support tickets, the SQL-generation copilot inside Snowsight — has failure modes that unit tests do not catch: hallucinations that look confident, retrieval misses that return the wrong policy document, prompt drift where a model swap flips the tone, cost explosions where a runaway agent burns a quarter's budget in one afternoon. Those failures do not raise exceptions; they degrade the output. The only way to catch them is to &lt;em&gt;measure&lt;/em&gt; the output on every run and gate on the score.&lt;/p&gt;

&lt;p&gt;This guide is the senior-DE / MLOps walkthrough you wished existed the first time an interviewer asked "walk me through your &lt;code&gt;llm evaluation for data pipelines&lt;/code&gt; stack" or "how would you compare &lt;code&gt;langsmith&lt;/code&gt;, &lt;code&gt;trulens&lt;/code&gt;, and &lt;code&gt;ragas&lt;/code&gt;?" or "how do you gate a RAG deployment on &lt;code&gt;retrieval quality metrics&lt;/code&gt;?" It walks through the four axes every senior discussion converges on (&lt;code&gt;groundedness&lt;/code&gt;, &lt;code&gt;answer relevance&lt;/code&gt;, &lt;code&gt;context precision / recall&lt;/code&gt;, latency + cost), the four production-hardened tools that ship those metrics in 2026 — LangSmith for trace-first eval and versioned datasets, TruLens for feedback functions and the &lt;code&gt;rag evaluation&lt;/code&gt; triad, Ragas for reference-free metrics inside CI, and Snowflake Cortex Search Ops for eval at the semantic-search layer — and finishes each section with a Solution-Tail interview answer: code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft8ah1x6hpmszv86minar.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft8ah1x6hpmszv86minar.jpeg" alt="PipeCode blog header for LLM evaluation for data pipelines — bold white headline 'LLM Evaluation' over four small glyph medallions (trace-glyph, feedback-dial, RAG-triangle, search-lens) arranged on a wheel around a central purple 'EVAL' seal on a dark gradient." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt;, rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;data-validation practice library →&lt;/a&gt;, and sharpen the systems axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why LLM pipelines fail silently without eval — the four axes&lt;/li&gt;
&lt;li&gt;LangSmith — trace-first eval, dataset versioning, hosted judges&lt;/li&gt;
&lt;li&gt;TruLens — feedback functions and the RAG triad&lt;/li&gt;
&lt;li&gt;Ragas — reference-free RAG metrics for CI&lt;/li&gt;
&lt;li&gt;Snowflake Cortex Search Ops — eval at the semantic-search layer&lt;/li&gt;
&lt;li&gt;Cheat sheet — LLM eval recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why LLM pipelines fail silently without eval — the four axes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Four axes, four measurement tools — the choice binds every downstream release gate
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;&lt;code&gt;llm evaluation for data pipelines&lt;/code&gt; is the discipline of measuring model + retrieval output on every run against four independent axes — &lt;code&gt;groundedness&lt;/code&gt; (did the answer cite the retrieved context?), &lt;code&gt;answer relevance&lt;/code&gt; (did the answer address the user's question?), context precision / recall (did retrieval surface the right chunks?), and latency + cost — because unit tests cannot catch hallucination, retrieval miss, prompt drift, or budget blowup, and shipping without a numeric score on each axis is the modern equivalent of shipping without regression tests&lt;/strong&gt;. The four axes are orthogonal: a RAG chain can be perfectly grounded in the retrieved context and still fail answer relevance because the retrieval was wrong; retrieval can be perfect and grounding low because the model paraphrased poorly; both can be high while latency p99 blows past your SLO or per-request cost triples on a model swap. Measuring one axis and calling it "LLM eval" is the single most common mistake senior candidates make in interviews.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes senior interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Groundedness.&lt;/strong&gt; Of every claim in the generated answer, what fraction is directly supported by the retrieved context? An answer that invents a fact the retrieved documents do not contain scores low on groundedness even if the fact is &lt;em&gt;correct in the world&lt;/em&gt;. TruLens calls this "groundedness"; Ragas calls it "faithfulness"; both compute it as an LLM-judge score over (context, answer) pairs. This is the primary anti-hallucination metric and the first metric any RAG deployment should ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer relevance.&lt;/strong&gt; Does the answer actually address the user's question? A perfectly grounded answer that ignores the question ("here is the entire retrieved policy document") scores low on answer relevance. Ragas measures this by having an LLM generate hypothetical questions the answer &lt;em&gt;could&lt;/em&gt; address, then computing cosine similarity between those questions and the original query. High answer relevance means the answer stays on topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context precision / recall.&lt;/strong&gt; Of the top-k retrieved chunks, how many are relevant (&lt;code&gt;precision&lt;/code&gt;), and of the relevant chunks in the corpus, how many appear in top-k (&lt;code&gt;recall&lt;/code&gt;)? These are the classic IR metrics reborn for RAG. Ragas ships &lt;code&gt;context_precision&lt;/code&gt; and &lt;code&gt;context_recall&lt;/code&gt;; TruLens exposes "context relevance" as the top vertex of the RAG triad. Low precision means noisy retrieval; low recall means missing information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency + cost.&lt;/strong&gt; The pipeline axis. p50 / p95 / p99 latency per LLM call, tokens per call, dollars per call, tokens per user query end-to-end. A model swap that improves groundedness by 3 points but triples cost is a regression on this axis. Every eval framework should ship latency + cost alongside quality scores; if it doesn't, wire it up separately with a &lt;code&gt;@traceable&lt;/code&gt; wrapper.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The 2026 reality — four tools, four sweet spots, they coexist.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LangSmith&lt;/strong&gt; is the trace-first hosted platform: every LLM call is a &lt;code&gt;Run&lt;/code&gt; inside a hierarchical &lt;code&gt;Trace&lt;/code&gt;; datasets are versioned; experiments diff scores over the same dataset; hosted LLM-as-judge evaluators run in the LangSmith cloud. Sweet spot: teams already using LangChain / LangGraph who want a hosted dashboard and dataset store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TruLens&lt;/strong&gt; is the local-first open-source eval library: &lt;code&gt;TruChain&lt;/code&gt; / &lt;code&gt;TruLlama&lt;/code&gt; wrap your app, &lt;code&gt;Feedback&lt;/code&gt; functions score every &lt;code&gt;Record&lt;/code&gt;, and the RAG triad (context relevance, groundedness, answer relevance) ships as a canonical pattern. Sweet spot: teams that want to keep eval inside their VPC, avoid a hosted vendor, and instrument any Python app (not just LangChain).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ragas&lt;/strong&gt; is the reference-free metric library: &lt;code&gt;EvaluationDataset&lt;/code&gt; + &lt;code&gt;evaluate()&lt;/code&gt; returns a pandas DataFrame of &lt;code&gt;faithfulness&lt;/code&gt;, &lt;code&gt;answer_relevancy&lt;/code&gt;, &lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;. Sweet spot: CI gates — a nightly Airflow job runs Ragas over a golden query set and fails the build if any metric regresses beyond a threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake Cortex Search Ops&lt;/strong&gt; brings the eval into SQL: &lt;code&gt;CORTEX SEARCH SERVICE&lt;/code&gt; is the semantic search index; &lt;code&gt;AI_COMPLETE('claude-3-5-sonnet', ...)&lt;/code&gt; is the SQL judge; recall@k / MRR / nDCG are computed as plain aggregate SQL over &lt;code&gt;(query, expected_doc_id, retrieved_doc_ids)&lt;/code&gt; tables. Sweet spot: teams whose entire data + eval + model stack lives inside one Snowflake account and one governance boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you name &lt;strong&gt;all four axes&lt;/strong&gt; without prompting? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you distinguish &lt;strong&gt;groundedness from answer relevance&lt;/strong&gt; rather than treating "hallucination" as one blob? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name &lt;strong&gt;at least two eval tools&lt;/strong&gt; and state their sweet spots? — required answer.&lt;/li&gt;
&lt;li&gt;Do you describe eval as a &lt;strong&gt;CI gate&lt;/strong&gt;, not as "a dashboard we look at sometimes"? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name &lt;strong&gt;latency + cost as an eval axis&lt;/strong&gt;, not as "just an SRE concern"? — required answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the four-axis measurement plan for a support-ticket RAG
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful artifact for an LLM eval interview is a memorised 4-axis measurement plan against a concrete scenario. Every senior LLM eval discussion converges on this plan within the first ten minutes; having it in your head is what separates a fluent answer from a stumbling one. Walk through building the plan for a support-ticket RAG chatbot that answers customer questions from a Confluence corpus.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application.&lt;/strong&gt; RAG chatbot; input = customer question; output = plain-language answer + citations to Confluence pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corpus.&lt;/strong&gt; ~80,000 Confluence documents, ~1.2 million chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval.&lt;/strong&gt; Hybrid (BM25 + vector) top-8 chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM.&lt;/strong&gt; Claude Sonnet 4.7 in production; Claude Haiku for the judge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the four-axis measurement plan for the support-ticket RAG and name the tool that ships each metric.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Threshold (v1)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Groundedness&lt;/td&gt;
&lt;td&gt;fraction of answer claims supported by context&lt;/td&gt;
&lt;td&gt;Ragas &lt;code&gt;faithfulness&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;≥ 0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer relevance&lt;/td&gt;
&lt;td&gt;question-answer topical alignment&lt;/td&gt;
&lt;td&gt;Ragas &lt;code&gt;answer_relevancy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;≥ 0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context precision&lt;/td&gt;
&lt;td&gt;fraction of top-8 chunks that are relevant&lt;/td&gt;
&lt;td&gt;Ragas &lt;code&gt;context_precision&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;≥ 0.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context recall&lt;/td&gt;
&lt;td&gt;fraction of relevant chunks in top-8&lt;/td&gt;
&lt;td&gt;Ragas &lt;code&gt;context_recall&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;≥ 0.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency p95&lt;/td&gt;
&lt;td&gt;end-to-end wall-clock&lt;/td&gt;
&lt;td&gt;LangSmith trace&lt;/td&gt;
&lt;td&gt;≤ 4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per query&lt;/td&gt;
&lt;td&gt;tokens × price&lt;/td&gt;
&lt;td&gt;LangSmith trace&lt;/td&gt;
&lt;td&gt;≤ $0.015&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The four-axis eval plan, expressed as a dataclass
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EvalAxis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;higher_is_better&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;

&lt;span class="n"&gt;PLAN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;EvalAxis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Groundedness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faithfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ragas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;EvalAxis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer_relevancy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ragas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;EvalAxis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context precision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_precision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ragas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;EvalAxis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context recall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_recall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ragas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;EvalAxis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Latency p95&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_to_end_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LangSmith&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;EvalAxis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cost per query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LangSmith&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.015&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return (passed, list_of_failures). Runs in the CI job.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;higher_is_better&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; vs threshold &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each axis maps to one metric and one tool that ships that metric out of the box. Ragas is the workhorse for the four quality metrics because it computes all four from a single &lt;code&gt;EvaluationDataset&lt;/code&gt; in one &lt;code&gt;evaluate()&lt;/code&gt; call. LangSmith is the workhorse for latency + cost because every &lt;code&gt;@traceable&lt;/code&gt; call emits a &lt;code&gt;Run&lt;/code&gt; with wall-clock and token counts already attached.&lt;/li&gt;
&lt;li&gt;Thresholds start conservative — 0.85 for faithfulness is aggressive but achievable with Sonnet-tier models and a well-tuned retriever. Set thresholds slightly below your first stable measurement so day-two changes have headroom; lift thresholds as the pipeline matures.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;higher_is_better&lt;/code&gt; flag flips the comparison for latency and cost — those are "lower is better" axes. Every gate library gets this wrong once; codifying the direction in the dataclass prevents it.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;gate()&lt;/code&gt; function is the CI contract. It runs in the GitHub Action after Ragas + LangSmith have populated the &lt;code&gt;scores&lt;/code&gt; dict. Any failure fails the build; the failure list is posted as a PR comment. This is the pattern that turns eval from "a dashboard" into "a release gate."&lt;/li&gt;
&lt;li&gt;In production, thresholds evolve. Version the &lt;code&gt;PLAN&lt;/code&gt; list in git alongside your prompts and your dataset version; a threshold change is a code review just like a prompt change. Never bump a threshold to make a failing PR green without a written justification.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Passed?&lt;/th&gt;
&lt;th&gt;Failures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v1 baseline&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model swap Sonnet → Haiku&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code&gt;faithfulness: 0.78 vs 0.85&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retriever change hybrid → vector-only&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code&gt;context_precision: 0.62 vs 0.70&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt tweak (add citation format)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost-optimised (batch calls)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code&gt;end_to_end_ms: 5200 vs 4000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never pick an LLM eval tool without first writing down the four-axis plan and the numeric threshold on each axis. The tool is downstream of the plan; if the plan is missing, the tool choice is arbitrary and the eval will drift into "vibes."&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the senior LLM-eval interview grading rubric
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The senior LLM-eval interview grades a small set of specific signals. Memorise the rubric; every answer should touch every row.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Axes named in minute 1.&lt;/strong&gt; All four (&lt;code&gt;groundedness&lt;/code&gt;, &lt;code&gt;answer relevance&lt;/code&gt;, context precision/recall, latency + cost) — senior signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools per axis.&lt;/strong&gt; Ragas for the four quality metrics, LangSmith for latency/cost, TruLens for VPC deployments, Cortex Search Ops for Snowflake-native — required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate mechanism.&lt;/strong&gt; "GitHub Action fails the PR on regression" — required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge validation.&lt;/strong&gt; Quarterly re-validation against human labels; correlation ≥ 0.75 — senior signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Score a candidate's answer against the rubric and pick the one differentiator that separates senior from mid-level responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Row&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;Senior answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Axes&lt;/td&gt;
&lt;td&gt;"accuracy"&lt;/td&gt;
&lt;td&gt;"four axes: groundedness, answer relevance, context precision/recall, latency + cost"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;"we look at LangSmith"&lt;/td&gt;
&lt;td&gt;"Ragas in CI, LangSmith for traces, TruLens for VPC"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gate&lt;/td&gt;
&lt;td&gt;"we watch a dashboard"&lt;/td&gt;
&lt;td&gt;"GitHub Action fails PR on any axis regression"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge&lt;/td&gt;
&lt;td&gt;"LLM judge is fine"&lt;/td&gt;
&lt;td&gt;"re-validated quarterly against 100 human-labelled rows; correlation tracked"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# rubric.py — score a candidate answer against the LLM-eval rubric
&lt;/span&gt;&lt;span class="n"&gt;RUBRIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;axes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groundedness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context precision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ragas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;langsmith&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trulens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;regression&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correlation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;re-validate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RUBRIC&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;d measure groundedness, answer relevance, context precision, and latency. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ragas + LangSmith. GitHub Action fails the PR on regression. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The LLM judge is re-validated quarterly against human-labelled rows.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# → {'axes': 0.8, 'tools': 0.67, 'gate': 1.0, 'judge': 0.67}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each rubric row is a set of keyword indicators. Naming all axes hits row 1 fully; naming two tools scores partial credit on row 2.&lt;/li&gt;
&lt;li&gt;The gate row is binary — either the candidate names an automated PR-blocking mechanism or they don't. This is the single biggest senior differentiator.&lt;/li&gt;
&lt;li&gt;Judge validation is the row most candidates miss. Naming "human correlation" or "quarterly re-validation" separates the mid-level from senior.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Row&lt;/th&gt;
&lt;th&gt;Weak candidate&lt;/th&gt;
&lt;th&gt;Senior candidate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;axes&lt;/td&gt;
&lt;td&gt;0.2&lt;/td&gt;
&lt;td&gt;0.8-1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tools&lt;/td&gt;
&lt;td&gt;0.0-0.33&lt;/td&gt;
&lt;td&gt;0.67-1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gate&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;judge&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;0.67-1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; If you touch three of four rows at 0.6+, you pass the senior bar. The gate row is the mandatory one; missing it means "mid-level" regardless of the other three.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on LLM eval axes and CI
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You inherit a RAG chatbot that has no eval — just a LangChain chain wired to Postgres pgvector and Claude Sonnet. The product team keeps shipping prompt tweaks and quality is regressing. Walk me through the eval stack you'd stand up in the first sprint, the four axes you'd measure, and the CI gate you'd wire into GitHub Actions."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using Ragas as the CI gate with LangSmith traces for latency and cost
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# eval_gate.py — the CI job that scores + gates every PR
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langsmith&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;evaluate&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;faithfulness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer_relevancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;context_precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_recall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;GOLDEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval/golden_v3.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;THRESHOLDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faithfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer_relevancy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_precision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_recall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myapp.rag&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;rag_chain&lt;/span&gt;
    &lt;span class="n"&gt;predictions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;GOLDEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()]:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rag_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
        &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contexts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_content&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
                             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ground_truth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ref_answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;faithfulness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer_relevancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;context_precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_recall&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_pandas&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numeric_only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Attach latency + cost from LangSmith traces
&lt;/span&gt;    &lt;span class="n"&gt;ls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_runs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rag-ci&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;latencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency_p95_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost_usd_avg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_cost&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;THRESHOLDS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval/report.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVAL GATE FAILED:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVAL GATE PASSED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/eval-gate.yml — the PR gate&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LLM Eval Gate&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompts/**"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/rag/**"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval/**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;eval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run gate&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;LANGSMITH_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LANGSMITH_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;LANGSMITH_PROJECT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rag-ci"&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python eval_gate.py&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Comment scores on PR&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const fs = require("fs");&lt;/span&gt;
            &lt;span class="s"&gt;const report = fs.readFileSync("eval/report.json", "utf8");&lt;/span&gt;
            &lt;span class="s"&gt;github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner, repo: context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
              &lt;span class="s"&gt;body: "## LLM eval scores\n```&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;endraw %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="s"&gt;json\n" + report + "\n&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;raw %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;```&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Before (no eval)&lt;/th&gt;
&lt;th&gt;After (Ragas + LangSmith CI gate)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Regression detection&lt;/td&gt;
&lt;td&gt;user complaint (weeks later)&lt;/td&gt;
&lt;td&gt;PR fails within minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metrics tracked&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;4 quality + 2 pipeline axes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Claude Haiku as Ragas judge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dataset&lt;/td&gt;
&lt;td&gt;ad-hoc curl commands&lt;/td&gt;
&lt;td&gt;versioned &lt;code&gt;golden_v3.jsonl&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR feedback&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;GitHub bot comments scores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threshold governance&lt;/td&gt;
&lt;td&gt;verbal&lt;/td&gt;
&lt;td&gt;code-reviewed dict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost visibility&lt;/td&gt;
&lt;td&gt;monthly bill shock&lt;/td&gt;
&lt;td&gt;per-PR cost delta&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, every PR that touches &lt;code&gt;prompts/**&lt;/code&gt; or &lt;code&gt;src/rag/**&lt;/code&gt; runs the full eval gate, the four Ragas metrics + latency p95 + cost/query land as a JSON on the PR, and any axis regression below its threshold fails the check. The product team's prompt tweaks either improve the numbers or fail the merge; there is no "ship first, measure later" path anymore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline (main)&lt;/th&gt;
&lt;th&gt;PR #482 (Haiku swap)&lt;/th&gt;
&lt;th&gt;PR #483 (retriever tweak)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;faithfulness&lt;/td&gt;
&lt;td&gt;0.89&lt;/td&gt;
&lt;td&gt;0.78 (&lt;strong&gt;fail&lt;/strong&gt;)&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;answer_relevancy&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;td&gt;0.87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;context_precision&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;td&gt;0.79 (improved)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;context_recall&lt;/td&gt;
&lt;td&gt;0.78&lt;/td&gt;
&lt;td&gt;0.77&lt;/td&gt;
&lt;td&gt;0.82&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;latency_p95_ms&lt;/td&gt;
&lt;td&gt;3800&lt;/td&gt;
&lt;td&gt;2100&lt;/td&gt;
&lt;td&gt;4300 (&lt;strong&gt;fail&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cost_usd_avg&lt;/td&gt;
&lt;td&gt;0.011&lt;/td&gt;
&lt;td&gt;0.003&lt;/td&gt;
&lt;td&gt;0.013&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Ragas reference-free metrics&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;faithfulness&lt;/code&gt;, &lt;code&gt;answer_relevancy&lt;/code&gt;, &lt;code&gt;context_precision&lt;/code&gt;, and &lt;code&gt;context_recall&lt;/code&gt; all compute from &lt;code&gt;(question, answer, contexts, ground_truth?)&lt;/code&gt; alone. &lt;code&gt;faithfulness&lt;/code&gt; and &lt;code&gt;answer_relevancy&lt;/code&gt; don't even need &lt;code&gt;ground_truth&lt;/code&gt;; &lt;code&gt;context_precision&lt;/code&gt; and &lt;code&gt;context_recall&lt;/code&gt; do. This is why Ragas is the CI workhorse — no reference-answer curation required for two of the four axes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;LangSmith &lt;code&gt;@traceable&lt;/code&gt; runs&lt;/strong&gt;&lt;/strong&gt; — every LLM call inside the RAG chain emits a &lt;code&gt;Run&lt;/code&gt; with wall-clock latency, prompt tokens, completion tokens, and computed cost. The &lt;code&gt;ls.list_runs()&lt;/code&gt; call at gate time pulls the last N runs for the &lt;code&gt;rag-ci&lt;/code&gt; project. This is how latency and cost enter the gate without a separate instrumentation layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Versioned golden dataset&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;golden_v3.jsonl&lt;/code&gt; in git is the single source of truth for what "the pipeline should do." Growing the dataset is monotonic (append new curated rows; never delete); a dataset bump is a PR just like a prompt bump. Two experiments over the same dataset version are the only fair comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Threshold dict in code&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;THRESHOLDS&lt;/code&gt; lives in &lt;code&gt;eval_gate.py&lt;/code&gt;, code-reviewed with every change. This is the difference between "we lowered the threshold to make the PR pass" (a compliance failure) and "we lowered the threshold with a written justification approved by the on-call reviewer" (a governed change).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one CI run costs roughly &lt;code&gt;120 questions × ($0.011 per RAG call + $0.002 per Ragas judge call) ≈ $1.60&lt;/code&gt; per PR. Compared to the cost of a bad model swap reaching production undetected for a week (support tickets, refunds, reputation), this is the cheapest quality gate you will ever ship. O(dataset_size) per PR, O(1) latency to detect regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;ETL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;ETL problems on eval pipelines and gates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Data validation&lt;/span&gt;
&lt;span&gt;Topic — data-validation&lt;/span&gt;
&lt;strong&gt;Data validation problems on quality gates&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. LangSmith — trace-first eval, dataset versioning, hosted judges
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Every LLM call is a &lt;code&gt;Run&lt;/code&gt; inside a &lt;code&gt;Trace&lt;/code&gt; — datasets are versioned, experiments diff over them, judges score them
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;langsmith&lt;/code&gt; is the trace-first eval platform where every LLM call your code makes is wrapped in &lt;code&gt;@traceable&lt;/code&gt; to emit a hierarchical &lt;code&gt;Run&lt;/code&gt; (child of a parent &lt;code&gt;Trace&lt;/code&gt;), your golden inputs live in a versioned &lt;code&gt;Dataset&lt;/code&gt;, an &lt;code&gt;Experiment&lt;/code&gt; runs your target function against every example in the dataset and pipes the outputs through a set of evaluators (LLM-as-judge or Python), and the LangSmith UI diffs experiments side by side so a prompt change or a model swap has an immediately visible before/after — it is the tool of choice when your app already lives inside LangChain or LangGraph and you want a hosted dashboard plus a dataset store without building either from scratch&lt;/strong&gt;. Every senior DE / MLOps engineer working with LangChain has stood up LangSmith at some point; the pattern is the same across products.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkazy1a72r5s7hebsy8lu.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkazy1a72r5s7hebsy8lu.jpeg" alt="Iconographic LangSmith diagram — a hierarchical trace tree of nested runs on the left, a dataset card in the middle, and a hosted-judge card on the right emitting scored annotations back onto the trace." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for LangSmith.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Groundedness.&lt;/strong&gt; Ships as a hosted LLM-as-judge evaluator (&lt;code&gt;langsmith.evaluation.LangChainStringEvaluator("labeled_criteria", criteria="correctness")&lt;/code&gt; or a custom judge prompt). You can also plug Ragas into a LangSmith experiment via the &lt;code&gt;RunEvaluator&lt;/code&gt; interface; LangSmith records the Ragas scores as annotations on each Run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer relevance.&lt;/strong&gt; Same story — a hosted &lt;code&gt;criteria&lt;/code&gt; evaluator with &lt;code&gt;criteria="relevance"&lt;/code&gt;, or a custom Python judge. LangSmith's differentiator is that the score attaches to the &lt;em&gt;exact&lt;/em&gt; trace, so you can drill from "answer relevance dropped 0.05" to the specific 6 runs that regressed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context precision / recall.&lt;/strong&gt; LangSmith itself doesn't ship retrieval metrics out of the box, but the trace captures the retrieved documents, and you can wire a custom &lt;code&gt;RunEvaluator&lt;/code&gt; that computes precision/recall against a labelled dataset. The pattern: dataset row carries &lt;code&gt;expected_context_ids&lt;/code&gt;; evaluator compares against the trace's retrieved IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency + cost.&lt;/strong&gt; Free. Every &lt;code&gt;@traceable&lt;/code&gt; decorator emits latency, prompt tokens, completion tokens, and cost per token computed against the model price sheet. The LangSmith dashboard groups by run name, model, project, and dataset. This is the killer feature that keeps teams on LangSmith even when they'd prefer a self-hosted stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The trace shape — nested runs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trace.&lt;/strong&gt; The top-level unit of work. One user query = one trace. Traces have a &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;project_name&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, and a wall-clock start/end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run.&lt;/strong&gt; A single operation inside the trace — an LLM call, a retriever call, a tool call, a chain step. Runs are nested (a chain run contains child runs for each component). Runs have inputs, outputs, &lt;code&gt;run_type&lt;/code&gt; (llm / chain / tool / retriever), and cost/latency metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback.&lt;/strong&gt; A score attached to a run by an evaluator (LLM judge, Python function, or human). Multiple feedback entries per run are allowed (e.g. one for &lt;code&gt;faithfulness&lt;/code&gt;, one for &lt;code&gt;relevance&lt;/code&gt;, one for &lt;code&gt;latency&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset.&lt;/strong&gt; A versioned collection of &lt;code&gt;Example&lt;/code&gt; rows (&lt;code&gt;inputs&lt;/code&gt;, &lt;code&gt;outputs&lt;/code&gt; optional). Datasets are immutable per version; adding rows creates a new version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experiment.&lt;/strong&gt; A run of a target function against every example in a dataset version, with a set of evaluators. Experiments have a name, a dataset version pin, and produce one Feedback per (Example, evaluator) pair.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dataset versioning — the disciplined pattern.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Immutable versions.&lt;/strong&gt; Every dataset write (add / edit / delete row) creates a new version. Experiments pin to a version by ID; a rerun months later against the same version is byte-for-byte comparable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Naming.&lt;/strong&gt; &lt;code&gt;rag_eval_v3_2026_07_30&lt;/code&gt; — semantic version + date. Never overwrite &lt;code&gt;rag_eval&lt;/code&gt; without a version suffix; two experiments named the same but against different underlying rows is the debugging pain from hell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tagging.&lt;/strong&gt; Rows are tagged (&lt;code&gt;golden&lt;/code&gt;, &lt;code&gt;hard&lt;/code&gt;, &lt;code&gt;edge_case&lt;/code&gt;, &lt;code&gt;regression_from_pr_482&lt;/code&gt;). Tags let you slice eval scores by category without duplicating rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bootstrap.&lt;/strong&gt; First dataset comes from a spreadsheet of "questions our support team gets" curated by a product manager. Second dataset comes from real production traces flagged as "interesting." Third dataset comes from regression cases (a specific question that broke, immortalised as a row).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The hosted judge — LLM-as-judge inside LangSmith.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prebuilt criteria.&lt;/strong&gt; &lt;code&gt;correctness&lt;/code&gt;, &lt;code&gt;relevance&lt;/code&gt;, &lt;code&gt;coherence&lt;/code&gt;, &lt;code&gt;conciseness&lt;/code&gt;, &lt;code&gt;harmfulness&lt;/code&gt;. Each is a prompt template that scores an output on a 0-1 scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom criteria.&lt;/strong&gt; Pass a natural-language criterion string; LangSmith wraps it in a scoring prompt and runs it against a model you pick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pairwise evaluators.&lt;/strong&gt; Given two candidate outputs (from two experiments), pick which is better. Useful for A/B comparisons of prompt versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Every hosted-judge call is a paid LLM call. Budget: judge cost ≈ 1-2× the production call cost per dataset row.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on LangSmith.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What's a Run vs a Trace?" — required answer: Trace is the top-level unit of work; Runs are the nested operations inside it.&lt;/li&gt;
&lt;li&gt;"How do you version an eval dataset?" — required answer: immutable versions, pin experiments to a version ID.&lt;/li&gt;
&lt;li&gt;"What's the difference between a hosted evaluator and a custom evaluator?" — hosted = LangSmith cloud; custom = your Python callable receiving &lt;code&gt;(run, example)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"How do you gate a PR on LangSmith scores?" — required answer: run an experiment in CI, fetch scores via SDK, compare against thresholds.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — instrument a LangChain RAG with &lt;code&gt;@traceable&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The first thing any LangSmith deployment does is turn on tracing. The &lt;code&gt;@traceable&lt;/code&gt; decorator (or the auto-instrumented &lt;code&gt;langchain&lt;/code&gt; package) captures every LLM call, retriever call, and chain step and ships them to the LangSmith cloud. Once tracing is on, every subsequent query is a debugging goldmine; without it, LangSmith is empty. Walk through the setup for a Postgres-pgvector + Claude RAG chain.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chain.&lt;/strong&gt; &lt;code&gt;RetrievalQA&lt;/code&gt; from LangChain: pgvector retriever → prompt template → Claude Sonnet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Env vars.&lt;/strong&gt; &lt;code&gt;LANGSMITH_API_KEY&lt;/code&gt;, &lt;code&gt;LANGSMITH_PROJECT=support-rag&lt;/code&gt;, &lt;code&gt;LANGSMITH_TRACING=true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traceable functions.&lt;/strong&gt; The chain, the retriever, the LLM call — all auto-traced by the LangChain integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Wire up &lt;code&gt;@traceable&lt;/code&gt; around a LangChain RAG so every user query lands as a Trace in the LangSmith UI with per-step timings and token counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Traced?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rag_chain.invoke&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Chain&lt;/td&gt;
&lt;td&gt;yes (auto)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pgvector.similarity_search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retriever&lt;/td&gt;
&lt;td&gt;yes (auto)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ChatAnthropic.invoke&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;yes (auto)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;format_answer&lt;/code&gt; (custom)&lt;/td&gt;
&lt;td&gt;Utility&lt;/td&gt;
&lt;td&gt;yes (via &lt;code&gt;@traceable&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;answer_endpoint&lt;/code&gt; (FastAPI)&lt;/td&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;yes (via &lt;code&gt;@traceable&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# rag_app.py — LangChain RAG with LangSmith tracing
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LANGSMITH_TRACING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LANGSMITH_PROJECT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.chains&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RetrievalQA&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatAnthropic&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.vectorstores&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PGVector&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIEmbeddings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langsmith&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;traceable&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAIEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;retriever&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PGVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;connection_string&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PG_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;embedding_function&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rag_chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RetrievalQA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_chain_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;return_source_documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;chain_type_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verbose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@traceable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format_answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;format_answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Trim, cite, and structure the LLM output for the client.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;citations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@traceable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer_endpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v2.3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rag_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;format_answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Setting &lt;code&gt;LANGSMITH_TRACING=true&lt;/code&gt; and &lt;code&gt;LANGSMITH_PROJECT&lt;/code&gt; before importing &lt;code&gt;langchain&lt;/code&gt; auto-instruments every LangChain primitive. No decorator needed on &lt;code&gt;rag_chain.invoke&lt;/code&gt; — the LangChain integration wraps it for you. This is the fastest way to get useful traces.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@traceable(run_type="tool", name="format_answer")&lt;/code&gt; on the custom formatter captures the &lt;em&gt;non-LangChain&lt;/em&gt; utility inside the same trace tree. &lt;code&gt;run_type="tool"&lt;/code&gt; is a hint to the UI (colours the run icon); &lt;code&gt;name&lt;/code&gt; overrides the auto-detected function name.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@traceable(run_type="chain", name="answer_endpoint", tags=["prod", "v2.3"])&lt;/code&gt; on the FastAPI endpoint makes the endpoint the &lt;em&gt;top-level&lt;/em&gt; trace. Tags like &lt;code&gt;prod&lt;/code&gt; and &lt;code&gt;v2.3&lt;/code&gt; are filterable in the LangSmith UI — you can pull all traces for &lt;code&gt;v2.3&lt;/code&gt; in one query.&lt;/li&gt;
&lt;li&gt;After deploy, every &lt;code&gt;/answer&lt;/code&gt; POST creates one trace with three or four nested runs: the endpoint (chain), the RAG chain (chain), the retriever (retriever), the LLM call (llm), and the formatter (tool). Each run has latency, tokens, and cost.&lt;/li&gt;
&lt;li&gt;Sampling in production: setting &lt;code&gt;LANGSMITH_TRACING=true&lt;/code&gt; traces 100% of requests by default. For high-volume prod, use &lt;code&gt;LANGSMITH_TRACING_SAMPLE_RATE=0.01&lt;/code&gt; to trace 1%. Debugging incidents: flip to 1.0 for 15 minutes, capture the incident traces, flip back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trace field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trace name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;answer_endpoint&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total latency&lt;/td&gt;
&lt;td&gt;2,340 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total tokens&lt;/td&gt;
&lt;td&gt;4,120 prompt + 380 completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total cost&lt;/td&gt;
&lt;td&gt;$0.0148&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nested runs&lt;/td&gt;
&lt;td&gt;4 (endpoint → chain → retriever → llm → tool)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;prod&lt;/code&gt;, &lt;code&gt;v2.3&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project&lt;/td&gt;
&lt;td&gt;&lt;code&gt;support-rag&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Turn on LangSmith tracing on day one — even before you have any evaluators wired up. Tracing alone gives you latency + cost per LLM call, per model, per prompt version, and per endpoint, which is more observability than most LLM apps ever get. Add evaluators once the trace tree feels rich enough.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — run a pairwise experiment over a versioned dataset
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The killer LangSmith workflow is the pairwise experiment: two candidate configs (say, "prompt v1" vs "prompt v2") both run against the same dataset version, and a hosted &lt;code&gt;pairwise&lt;/code&gt; judge picks the better output per row. The result is a win-rate ("prompt v2 wins on 68 of 100 rows"), which is directly consumable by a product review. Walk through the setup.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dataset.&lt;/strong&gt; &lt;code&gt;support_rag_v3&lt;/code&gt; — 100 curated &lt;code&gt;{question, expected_answer_gist}&lt;/code&gt; rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configs.&lt;/strong&gt; &lt;code&gt;prompt_v1&lt;/code&gt; (current prod) vs &lt;code&gt;prompt_v2&lt;/code&gt; (proposed change).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge.&lt;/strong&gt; Hosted &lt;code&gt;pairwise&lt;/code&gt; evaluator with a "which answer is more helpful?" criterion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output.&lt;/strong&gt; Win-rate table + drill-down to disagreements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Run a pairwise LangSmith experiment comparing &lt;code&gt;prompt_v1&lt;/code&gt; and &lt;code&gt;prompt_v2&lt;/code&gt; against &lt;code&gt;support_rag_v3&lt;/code&gt; and interpret the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dataset&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;support_rag_v3&lt;/code&gt; (100 examples)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baseline function&lt;/td&gt;
&lt;td&gt;&lt;code&gt;run_with_prompt_v1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Candidate function&lt;/td&gt;
&lt;td&gt;&lt;code&gt;run_with_prompt_v2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge&lt;/td&gt;
&lt;td&gt;Claude Sonnet, criterion "helpfulness"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric&lt;/td&gt;
&lt;td&gt;pairwise win-rate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pairwise_experiment.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langsmith&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langsmith.evaluation&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;evaluate_comparative&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatAnthropic&lt;/span&gt;

&lt;span class="n"&gt;ls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# --- The two target functions ---
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_with_prompt_v1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myapp.rag&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;rag_chain_v1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rag_chain_v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_with_prompt_v2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myapp.rag&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;rag_chain_v2&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rag_chain_v2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="c1"&gt;# --- The pairwise judge ---
&lt;/span&gt;&lt;span class="n"&gt;JUDGE_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are comparing two answers to a support question.
Question: {question}
Answer A: {answer_a}
Answer B: {answer_b}
Which answer is more helpful, accurate, and grounded?
Respond with exactly one of: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TIE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;judge_llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pairwise_helpfulness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;judge_llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JUDGE_PROMPT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;answer_a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer_b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;helpfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scores&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# --- Step 1: run each candidate as a normal experiment ---
&lt;/span&gt;&lt;span class="n"&gt;r1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_with_prompt_v1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support_rag_v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;experiment_prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_with_prompt_v2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support_rag_v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;experiment_prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- Step 2: run the pairwise judge over the two experiments ---
&lt;/span&gt;&lt;span class="n"&gt;comparative&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate_comparative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;experiments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;experiment_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;experiment_name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;evaluators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pairwise_helpfulness&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comparative&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Two separate &lt;code&gt;ls.evaluate&lt;/code&gt; calls run each candidate against the &lt;em&gt;same&lt;/em&gt; dataset version. Each produces its own experiment with its own outputs stored per-example. Using the same dataset version is what makes the comparison fair — a change of dataset between runs would confuse the diff.&lt;/li&gt;
&lt;li&gt;The pairwise judge is a custom &lt;code&gt;evaluate_comparative&lt;/code&gt; evaluator. It receives both runs (one per experiment) plus the example, calls the judge LLM with a "which is better?" prompt, and returns per-experiment scores (1 for winner, 0 for loser, 0.5 for tie in some conventions).&lt;/li&gt;
&lt;li&gt;Temperature 0 on the judge is mandatory — you want deterministic scoring so a re-run yields the same result. The judge model can be the same as the production model or a stronger one; the common pattern is "production uses Haiku for cost, judge uses Sonnet for quality."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;evaluate_comparative&lt;/code&gt; returns a win-rate summary and stores per-row judge outputs in LangSmith. The UI shows a side-by-side diff for every disagreement, so you can eyeball whether the judge is agreeing with human intuition.&lt;/li&gt;
&lt;li&gt;The gate: PR is merged if &lt;code&gt;prompt_v2_wins / (prompt_v1_wins + prompt_v2_wins + ties) &amp;gt; 0.60&lt;/code&gt;. Anything less than 60% win-rate on a pairwise judge is not a meaningful improvement; ties count against both.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dataset version&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;support_rag_v3&lt;/code&gt; (100 rows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;prompt_v1&lt;/code&gt; wins&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;prompt_v2&lt;/code&gt; wins&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ties&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;prompt_v2&lt;/code&gt; win-rate (excl. ties)&lt;/td&gt;
&lt;td&gt;75.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge cost&lt;/td&gt;
&lt;td&gt;$0.42 total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verdict&lt;/td&gt;
&lt;td&gt;ship &lt;code&gt;prompt_v2&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Prefer pairwise judges over pointwise judges when the goal is "is this change better?" — pairwise is more sensitive to small quality deltas because the judge is asked to compare, not to score in an absolute frame. Save pointwise judges for absolute thresholds ("faithfulness ≥ 0.85").&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on LangSmith trace + dataset ops
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You've got a LangChain-based RAG in production, no eval, no dataset, complaints piling up. Design the first sprint: turn on tracing, extract a first dataset from production traces, set up a baseline experiment, and wire a pairwise comparison to gate the next prompt change."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using LangSmith tracing, production-trace-derived dataset, and a pairwise gate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# sprint1_langsmith_setup.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LANGSMITH_TRACING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LANGSMITH_PROJECT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-rag-prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langsmith&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langsmith.evaluation&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;evaluate_comparative&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatAnthropic&lt;/span&gt;

&lt;span class="n"&gt;ls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;DATASET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support_rag_from_prod_v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Step A: extract dataset from last week's low-confidence prod traces
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bootstrap_dataset&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DATASET&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_datasets&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_runs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-rag-prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="n"&gt;run_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;picks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;))[:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DATASET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-derived seed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_examples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;dataset_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DATASET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_trace_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Step B: pinned baseline + pairwise gate for the next prompt PR
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_with_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myapp.rag&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;make_chain&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="n"&gt;JUDGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;JUDGE_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Question: {q}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Answer A: {a}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Answer B: {b}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Which is more helpful, accurate, grounded? Reply &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, or &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TIE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pairwise_gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JUDGE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JUDGE_PROMPT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;helpfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scores&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gate_prompt_change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_win_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r_old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run_with_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DATASET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;experiment_prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseline_prompt_v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r_new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run_with_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DATASET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;experiment_prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;candidate_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;comp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate_comparative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experiments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r_old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;experiment_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r_new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;experiment_name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                                 &lt;span class="n"&gt;evaluators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pairwise_gate&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;totals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scores&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;helpfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;win_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;win_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;min_win_rate&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;bootstrap_dataset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APPROVED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;gate_prompt_change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BLOCKED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Before (no eval)&lt;/th&gt;
&lt;th&gt;After (LangSmith sprint 1)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tracing&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;100% of prod traffic (later sampled)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dataset&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;support_rag_from_prod_v1&lt;/code&gt; — 100 real prod rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;vibes&lt;/td&gt;
&lt;td&gt;pinned experiment &lt;code&gt;baseline_prompt_v1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt change gate&lt;/td&gt;
&lt;td&gt;verbal&lt;/td&gt;
&lt;td&gt;pairwise judge, 60% win-rate threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regression memory&lt;/td&gt;
&lt;td&gt;Slack thread&lt;/td&gt;
&lt;td&gt;permanent rows tagged &lt;code&gt;regression&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Claude Sonnet, deterministic (temp=0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;LangSmith UI with side-by-side diffs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the sprint ends, the team has a running trace history, a 100-row prod-derived dataset, a pinned baseline experiment, and a &lt;code&gt;gate_prompt_change()&lt;/code&gt; function anyone can call before proposing a prompt PR. The vibes-based prompt shipping cycle is over; a prompt change that doesn't win 60% of pairwise comparisons doesn't merge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sprint deliverable&lt;/th&gt;
&lt;th&gt;Concrete artifact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tracing on&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LANGSMITH_TRACING=true&lt;/code&gt; in every service env&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First dataset&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;support_rag_from_prod_v1&lt;/code&gt; (100 rows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baseline experiment&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;baseline_prompt_v1&lt;/code&gt; (immutable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pairwise gate function&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gate_prompt_change(new_version)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI hook&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.github/workflows/prompt-gate.yml&lt;/code&gt; calls the function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regression tag&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;metadata.tag = "regression"&lt;/code&gt; on 3 rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per gate run&lt;/td&gt;
&lt;td&gt;~$0.85 (200 chain calls + 100 judge calls)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;@traceable&lt;/code&gt; + LangChain auto-instrumentation&lt;/strong&gt;&lt;/strong&gt; — one env var turns every LLM call in the app into a Run in a Trace, capturing latency, tokens, cost, inputs, outputs, and error stacks for free. This is the highest-leverage single change any LLM-app team can make.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Production-derived dataset bootstrap&lt;/strong&gt;&lt;/strong&gt; — the 100 lowest-confidence prod traces from last week are the perfect seed dataset because they represent &lt;em&gt;actual user friction&lt;/em&gt;, not synthetic questions. Ground-truth answers are filled in later by humans; the raw questions are already load-bearing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Pinned experiment as baseline&lt;/strong&gt;&lt;/strong&gt; — pinning &lt;code&gt;baseline_prompt_v1&lt;/code&gt; against dataset &lt;code&gt;support_rag_from_prod_v1&lt;/code&gt; makes every future comparison anchor-referenced. Anyone can rerun the baseline months later against the same dataset version and get the same score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Pairwise judge with temperature 0&lt;/strong&gt;&lt;/strong&gt; — pairwise judges are more sensitive than pointwise judges to small quality deltas because the judge is asked to compare, not to score in an absolute frame. Temperature 0 makes the score deterministic so a re-run yields the same verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one gate run = 200 chain calls (100 rows × 2 candidates) + 100 judge calls ≈ $0.85. Compared to shipping a bad prompt to production for a week (support tickets, refunds, reputation), this is the cheapest quality gate you will ever ship. O(dataset_size) per gate; O(1) latency to detect regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;ETL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;ETL problems on trace-instrumented pipelines&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Python&lt;/span&gt;
&lt;span&gt;Topic — python&lt;/span&gt;
&lt;strong&gt;Python problems on decorator patterns and observability&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/python" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. TruLens — feedback functions and the RAG triad
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;Feedback&lt;/code&gt; functions score every &lt;code&gt;Record&lt;/code&gt; on the three edges of the RAG triad — local-first, open, and framework-agnostic
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;trulens&lt;/code&gt; is the open-source, local-first eval library where you wrap your app in &lt;code&gt;TruChain&lt;/code&gt; / &lt;code&gt;TruLlama&lt;/code&gt; / &lt;code&gt;TruCustomApp&lt;/code&gt;, attach &lt;code&gt;Feedback&lt;/code&gt; functions that score every &lt;code&gt;Record&lt;/code&gt; (one Record = one user query + its outputs), and lean on the canonical RAG triad — context relevance (query ↔ context), groundedness (context ↔ answer), and answer relevance (query ↔ answer) — as the three-edged completeness proof that no single-axis eval can match, and the entire eval history lives in a local SQLite (or your own Postgres) with no vendor round-trip required&lt;/strong&gt;. TruLens's sweet spot is teams that need eval inside the VPC, want to instrument non-LangChain code, or prefer an unopinionated &lt;code&gt;Feedback&lt;/code&gt; function primitive over a hosted platform's canned evaluators.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foykg35cq4uxhqqjgd9aj.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foykg35cq4uxhqqjgd9aj.jpeg" alt="Iconographic TruLens diagram — a large RAG triad triangle with vertices labelled 'context relevance', 'groundedness', 'answer relevance', a feedback-function badge in the centre, and a small app card connected to the triangle by feedback arrows." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for TruLens.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Groundedness.&lt;/strong&gt; The classic &lt;code&gt;Feedback(provider.groundedness_measure_with_cot_reasons)&lt;/code&gt; — split the answer into claims, ask an LLM judge whether each claim is supported by the context, aggregate. TruLens ships this as a named feedback function; you plug in the provider (OpenAI, Anthropic, Bedrock, local via &lt;code&gt;LiteLLM&lt;/code&gt;) and it just works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer relevance.&lt;/strong&gt; &lt;code&gt;Feedback(provider.relevance)&lt;/code&gt; on &lt;code&gt;(query, answer)&lt;/code&gt;. Same LLM-judge pattern as Ragas's &lt;code&gt;answer_relevancy&lt;/code&gt; but the prompt is TruLens's own and the API surface is &lt;code&gt;Feedback&lt;/code&gt; object composition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context precision / recall.&lt;/strong&gt; Slightly indirect — TruLens exposes "context relevance" per-chunk (&lt;code&gt;Feedback(provider.context_relevance_with_cot_reasons).on_input().on(context)&lt;/code&gt;) which is essentially per-chunk precision. Aggregate across chunks and you get context precision; recall requires labelled ground-truth context IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency + cost.&lt;/strong&gt; TruLens records per-Record wall-clock and token counts natively. &lt;code&gt;record.latency&lt;/code&gt; and &lt;code&gt;record.cost&lt;/code&gt; come out of the box; you can pipe them into your dashboard of choice or query the local SQLite directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Feedback function primitive.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anatomy.&lt;/strong&gt; &lt;code&gt;Feedback(provider.metric_fn, name="...").on_input().on_output()&lt;/code&gt; — a &lt;code&gt;Feedback&lt;/code&gt; binds a callable (usually an LLM-judge call), a name, and &lt;em&gt;selectors&lt;/em&gt; that pull the right fields off the Record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selectors.&lt;/strong&gt; &lt;code&gt;.on_input()&lt;/code&gt; pulls the user query; &lt;code&gt;.on_output()&lt;/code&gt; pulls the app's final answer; &lt;code&gt;.on(Select.RecordCalls.retriever.rets)&lt;/code&gt; pulls the retriever's output. Selectors are the composable glue between the app trace and the feedback function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregation.&lt;/strong&gt; By default, feedback returns a scalar. For per-chunk metrics like &lt;code&gt;context_relevance&lt;/code&gt;, aggregation strategies include &lt;code&gt;np.mean&lt;/code&gt;, &lt;code&gt;np.min&lt;/code&gt;, &lt;code&gt;np.max&lt;/code&gt;, or a custom callable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Providers.&lt;/strong&gt; &lt;code&gt;LiteLLM&lt;/code&gt;, &lt;code&gt;OpenAI&lt;/code&gt;, &lt;code&gt;Anthropic&lt;/code&gt;, &lt;code&gt;Bedrock&lt;/code&gt;, &lt;code&gt;Huggingface&lt;/code&gt; — any provider with a chat completion API can be the judge. &lt;code&gt;LiteLLM&lt;/code&gt; is the local-first choice because it wraps every provider under one API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;App vs Session vs Record scope.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Record.&lt;/strong&gt; One user query → one Record. Feedback runs per Record. Most metrics live here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session.&lt;/strong&gt; A sequence of Records tied to a user session (multi-turn chat). Session-scoped feedback measures things like coherence across turns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App.&lt;/strong&gt; The app-level aggregate (all Records for one app version). App-scoped feedback is where you compute p95 latency, aggregate faithfulness, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App version.&lt;/strong&gt; &lt;code&gt;TruChain(chain, app_name="rag", app_version="v2.3")&lt;/code&gt; — every deploy bumps the version; the dashboard slices by version so a bad deploy shows up as a scored-lower app version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Local-first — the offline story.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage.&lt;/strong&gt; Default backend is SQLite at &lt;code&gt;default.sqlite&lt;/code&gt;. Every Record + every Feedback score lives there. &lt;code&gt;Tru().get_records_and_feedback()&lt;/code&gt; returns a pandas DataFrame you can query, join, or export.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard.&lt;/strong&gt; &lt;code&gt;Tru().run_dashboard()&lt;/code&gt; starts a Streamlit UI on localhost with per-Record drill-down, feedback distributions, and app-version diffs. No cloud round-trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC deploy.&lt;/strong&gt; For team use, swap SQLite for Postgres (&lt;code&gt;Tru(database_url="postgresql://...")&lt;/code&gt;) — the schema is one-command migratable and the dashboard reads Postgres directly. Everything stays in your VPC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on TruLens.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What's the RAG triad?" — required answer: context relevance, groundedness, answer relevance — the three edges of a triangle whose vertices are query, context, and answer.&lt;/li&gt;
&lt;li&gt;"Where does TruLens store its eval history?" — required answer: local SQLite by default, Postgres in production; no vendor round-trip.&lt;/li&gt;
&lt;li&gt;"How is &lt;code&gt;Feedback&lt;/code&gt; different from LangSmith's evaluator?" — TruLens Feedback is a Python callable + selector composition, framework-agnostic; LangSmith evaluators are cloud-hosted primitives tied to Runs.&lt;/li&gt;
&lt;li&gt;"Why does groundedness split into claims?" — required answer: to score each atomic claim independently, otherwise a single unsupported sentence in a paragraph is invisible.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — instrument a RAG chain with &lt;code&gt;TruChain&lt;/code&gt; and run the triad
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical TruLens setup: wrap an existing LangChain RAG in &lt;code&gt;TruChain&lt;/code&gt;, define three Feedback functions (the triad), and let every subsequent &lt;code&gt;chain.invoke&lt;/code&gt; produce a scored Record. Walk through the setup end-to-end.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chain.&lt;/strong&gt; LangChain &lt;code&gt;RetrievalQA&lt;/code&gt; with FAISS + Claude Sonnet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider.&lt;/strong&gt; &lt;code&gt;LiteLLM&lt;/code&gt; pointed at Claude Haiku (cheap judge).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedbacks.&lt;/strong&gt; Context relevance (per-chunk, mean-aggregated), groundedness, answer relevance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage.&lt;/strong&gt; Default SQLite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Wire up &lt;code&gt;TruChain&lt;/code&gt; around a LangChain RAG and produce the RAG triad scores for every query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App&lt;/td&gt;
&lt;td&gt;LangChain &lt;code&gt;RetrievalQA&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrapper&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TruChain(app_name="support-rag", app_version="v2.3")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LiteLLM(model_engine="claude-3-5-haiku-20241022")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedbacks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;f_context_relevance&lt;/code&gt;, &lt;code&gt;f_groundedness&lt;/code&gt;, &lt;code&gt;f_answer_relevance&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;SQLite (&lt;code&gt;default.sqlite&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# trulens_rag.py — instrument a LangChain RAG with the RAG triad
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;trulens.core&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TruSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Feedback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Select&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;trulens.apps.langchain&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TruChain&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;trulens.providers.litellm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LiteLLM&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.chains&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RetrievalQA&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatAnthropic&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.vectorstores&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FAISS&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIEmbeddings&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Build the base RAG chain (unchanged from prod)
&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAIEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;vs&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FAISS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_local&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faiss_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_dangerous_deserialization&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;retriever&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;llm&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rag_chain&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RetrievalQA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_chain_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                          &lt;span class="n"&gt;return_source_documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Start TruLens session + provider
&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TruSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LiteLLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-3-5-haiku-20241022&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Define the RAG triad
&lt;/span&gt;&lt;span class="n"&gt;context_selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_relevant_documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rets&lt;/span&gt;&lt;span class="p"&gt;[:].&lt;/span&gt;&lt;span class="n"&gt;page_content&lt;/span&gt;

&lt;span class="n"&gt;f_context_relevance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context_relevance_with_cot_reasons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context Relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context_selector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;f_groundedness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groundedness_measure_with_cot_reasons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Groundedness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context_selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;f_answer_relevance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relevance_with_cot_reasons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer Relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 4. Wrap the chain
&lt;/span&gt;&lt;span class="n"&gt;tru_chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TruChain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;rag_chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;app_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;app_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v2.3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;feedbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f_context_relevance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_groundedness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_answer_relevance&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 5. Every call is now scored
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tru_chain&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;recording&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rag_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How do I rotate my API key without downtime?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recording&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s, cost: $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scores:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feedback_and_future_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;

&lt;span class="c1"&gt;# 6. Fetch the full history as a DataFrame
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_records_and_feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tru_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context Relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Groundedness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer Relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The base LangChain RAG is unchanged from production — TruLens is a &lt;em&gt;wrapper&lt;/em&gt;, not a replacement. This is TruLens's key adoption story: you don't rewrite your app to add eval.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;TruSession()&lt;/code&gt; singleton is the entry point; by default it uses SQLite (&lt;code&gt;default.sqlite&lt;/code&gt;) in the current directory. Swap for Postgres by passing &lt;code&gt;database_url=&lt;/code&gt; at construction. Every subsequent &lt;code&gt;TruChain&lt;/code&gt;, &lt;code&gt;TruLlama&lt;/code&gt;, or &lt;code&gt;TruCustomApp&lt;/code&gt; registers with the session.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;context_selector&lt;/code&gt; is a TruLens selector expression — &lt;code&gt;Select.RecordCalls.retriever.get_relevant_documents.rets[:].page_content&lt;/code&gt; navigates the recorded trace: pull the retriever call, get all returned documents, extract &lt;code&gt;page_content&lt;/code&gt; from each. Selectors are the composable primitive that makes TruLens framework-agnostic.&lt;/li&gt;
&lt;li&gt;The three Feedback functions are the RAG triad. Note how each uses different selectors: &lt;code&gt;f_context_relevance&lt;/code&gt; scores per-chunk (mean-aggregated), &lt;code&gt;f_groundedness&lt;/code&gt; collects all chunks as one context blob then scores the answer against it, &lt;code&gt;f_answer_relevance&lt;/code&gt; compares query directly to answer.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;with tru_chain as recording&lt;/code&gt; context manager captures every &lt;code&gt;rag_chain.invoke&lt;/code&gt; inside as a Record. &lt;code&gt;recording.records&lt;/code&gt; is the list of Records made in the block; each has &lt;code&gt;.latency&lt;/code&gt;, &lt;code&gt;.cost&lt;/code&gt;, and &lt;code&gt;.feedback_and_future_results&lt;/code&gt;. The dataframe view at the end is what you'd surface in a dashboard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Context Relevance&lt;/th&gt;
&lt;th&gt;Groundedness&lt;/th&gt;
&lt;th&gt;Answer Relevance&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"How do I rotate my API key?"&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;2.1 s&lt;/td&gt;
&lt;td&gt;$0.014&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What's the SLA at 3am ET?"&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;2.3 s&lt;/td&gt;
&lt;td&gt;$0.015&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Why can't I export PDF?"&lt;/td&gt;
&lt;td&gt;0.55&lt;/td&gt;
&lt;td&gt;0.44&lt;/td&gt;
&lt;td&gt;0.82&lt;/td&gt;
&lt;td&gt;2.4 s&lt;/td&gt;
&lt;td&gt;$0.016&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overall (mean)&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;td&gt;0.89&lt;/td&gt;
&lt;td&gt;2.27 s&lt;/td&gt;
&lt;td&gt;$0.015&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Wrap first, evaluate second. Get &lt;code&gt;TruChain&lt;/code&gt; around the existing app before you fine-tune Feedback functions. The first useful signal comes from just seeing per-Record latencies + costs in the SQLite; the triad scores are the second layer.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — per-chunk context relevance and the drill-down that saves your retriever
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The RAG triad scores at the Record level are useful, but the real diagnostic power of TruLens comes from &lt;em&gt;per-chunk&lt;/em&gt; context relevance. When a query scores low on context relevance, the drill-down reveals &lt;em&gt;which chunks&lt;/em&gt; are irrelevant, which points directly at retriever tuning: switch to hybrid retrieval, increase k, add a reranker, or fix the chunking strategy. Walk through the diagnosis flow for a low-scoring query.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low-scoring query.&lt;/strong&gt; "Why can't I export PDF from a shared workspace?" — context relevance 0.55.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-chunk drill-down.&lt;/strong&gt; &lt;code&gt;Select.RecordCalls.retriever.get_relevant_documents.rets[:]&lt;/code&gt; — inspect each chunk's individual score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnosis.&lt;/strong&gt; 6 of 8 chunks are billing-related; 2 are the actual "role-based export" chunks. Retriever is over-weighting a common bigram.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; Switch from pure-vector to hybrid (BM25 + vector) retrieval; add a reranker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Extract per-chunk context relevance for the failing query and use it to diagnose the retriever problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chunk #&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Vector score&lt;/th&gt;
&lt;th&gt;Context relevance score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;billing-policy.md&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;td&gt;0.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;billing-policy.md&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;0.10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;shared-workspaces.md#export&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;payment-methods.md&lt;/td&gt;
&lt;td&gt;0.83&lt;/td&gt;
&lt;td&gt;0.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;export-formats.md#roles&lt;/td&gt;
&lt;td&gt;0.82&lt;/td&gt;
&lt;td&gt;0.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;billing-policy.md&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;td&gt;0.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;account-limits.md&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;billing-history.md&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;td&gt;0.09&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# per_chunk_context_relevance.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;trulens.core&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Feedback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Select&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;trulens.providers.litellm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LiteLLM&lt;/span&gt;

&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LiteLLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-3-5-haiku-20241022&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Per-chunk context relevance — no aggregate; keep the vector of scores
&lt;/span&gt;&lt;span class="n"&gt;f_context_relevance_per_chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context_relevance_with_cot_reasons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context Relevance (per chunk)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_relevant_documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rets&lt;/span&gt;&lt;span class="p"&gt;[:].&lt;/span&gt;&lt;span class="n"&gt;page_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# NOTE: no .aggregate() — returns per-chunk scores as a list
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After running the query, drill down
&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recording&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;per_chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feedback_and_future_results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context Relevance (per chunk)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="c1"&gt;# Zip with retrieved sources for diagnosis
&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;rets&lt;/span&gt;  &lt;span class="c1"&gt;# the retriever's returned docs
&lt;/span&gt;&lt;span class="n"&gt;diag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per_chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Print the diagnostic table
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rel&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;vector&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Diagnosis rule: if &amp;gt; 50% of top-k chunks score &amp;lt; 0.30 on context relevance,
# the retriever is over-weighting a common bigram
&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;diag&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RETRIEVER PROBLEM: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; chunks below 0.30 relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Recommend: switch to hybrid retrieval + reranker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Removing the &lt;code&gt;.aggregate()&lt;/code&gt; call on the Feedback function is what turns a scalar score into a per-chunk vector. TruLens automatically iterates over the selector's returned list and applies the callable to each element.&lt;/li&gt;
&lt;li&gt;The per-chunk table reveals the pathology: chunks 1, 2, 6 are all &lt;code&gt;billing-policy.md&lt;/code&gt;, all score above 0.79 on the vector distance, but all score below 0.15 on context relevance. The vector retriever thinks these chunks match; the LLM judge thinks they don't. This is a classic "vector similarity captures surface bigrams" failure mode.&lt;/li&gt;
&lt;li&gt;Chunks 3 and 5 (from &lt;code&gt;shared-workspaces.md&lt;/code&gt; and &lt;code&gt;export-formats.md&lt;/code&gt;) are the &lt;em&gt;actually relevant&lt;/em&gt; chunks — high context relevance (0.94, 0.91) and moderate vector score (0.84, 0.82). They're in the top-8 but drowned out by the noise.&lt;/li&gt;
&lt;li&gt;The rule of thumb — "if &amp;gt; 50% of top-k chunks score &amp;lt; 0.30 on context relevance, the retriever needs help" — is the operational signal that triggers a retriever change. Options: (a) hybrid BM25 + vector rerank; (b) add a cross-encoder reranker; (c) increase top-k and re-rank; (d) fix the chunking strategy (chunks 1/2/6 might be over-chunked from one long policy page).&lt;/li&gt;
&lt;li&gt;After switching to hybrid retrieval + a &lt;code&gt;bge-reranker-large&lt;/code&gt; reranker, this query's context relevance jumps from 0.55 to 0.87; the two relevant chunks now sit at ranks 1 and 2. The Record-level triad scores rise across the board.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix applied&lt;/th&gt;
&lt;th&gt;Context Relevance (mean)&lt;/th&gt;
&lt;th&gt;Faithfulness&lt;/th&gt;
&lt;th&gt;Answer Relevance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;baseline (pure vector, k=8)&lt;/td&gt;
&lt;td&gt;0.55&lt;/td&gt;
&lt;td&gt;0.44&lt;/td&gt;
&lt;td&gt;0.82&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hybrid BM25 + vector&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hybrid + cross-encoder rerank&lt;/td&gt;
&lt;td&gt;0.87&lt;/td&gt;
&lt;td&gt;0.89&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Any Record with context relevance below 0.60 should trigger a per-chunk drill-down. The per-chunk vector is the retriever's report card; scalar aggregates hide the pathology. Wire the drill-down as a Slack alert on the first N regressions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on TruLens triad and production scoring
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You've got a support RAG in production, no eval, running inside a bank's VPC — no data can leave the perimeter. Wire up TruLens with the RAG triad, run it locally against a golden set, then deploy a sampled production-time scoring plus a nightly batch back-fill without adding latency to 98% of traffic."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using TruLens RAG triad locally, sampled sync feedback + nightly batch back-fill
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# trulens_vpc_prod.py — VPC-friendly TruLens deployment
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;trulens.core&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TruSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Feedback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Select&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;trulens.apps.langchain&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TruChain&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;trulens.providers.litellm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LiteLLM&lt;/span&gt;

&lt;span class="c1"&gt;# Postgres inside VPC; Bedrock judge inside AWS — zero external egress
&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TruSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TRULENS_DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LiteLLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock/anthropic.claude-3-5-haiku-20241022-v1:0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myapp.rag&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;make_chain&lt;/span&gt;
&lt;span class="n"&gt;rag_chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_chain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;CTX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_relevant_documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rets&lt;/span&gt;&lt;span class="p"&gt;[:].&lt;/span&gt;&lt;span class="n"&gt;page_content&lt;/span&gt;

&lt;span class="n"&gt;f_ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context_relevance_with_cot_reasons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context Relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_input&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CTX&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;f_grd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groundedness_measure_with_cot_reasons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Groundedness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CTX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;on_output&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;f_ans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relevance_with_cot_reasons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer Relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_input&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;on_output&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;tru_chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TruChain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rag_chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;app_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APP_VERSION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v2.3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                     &lt;span class="n"&gt;feedbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f_ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_grd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_ans&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Local golden-set run (bootstrap eval)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_golden_set&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tru_chain&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval/golden.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()]:&lt;/span&gt;
            &lt;span class="n"&gt;rag_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_records_and_feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tru_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context Relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Groundedness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer Relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Production endpoint: 2% sync scoring, 98% record-only
&lt;/span&gt;&lt;span class="n"&gt;SAMPLE_RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prod_answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tru_chain&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;SAMPLE_RATE&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;tru_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_only&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rag_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;tru_chain&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deferred&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feedback_mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Nightly back-fill for unscored Records
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;nightly_backfill&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_records_and_feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tru_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Groundedness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isna&lt;/span&gt;&lt;span class="p"&gt;()].&lt;/span&gt;&lt;span class="nf"&gt;iterrows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f_ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_grd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_ans&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feedback_result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Postgres in VPC&lt;/td&gt;
&lt;td&gt;zero-egress persistence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;LiteLLM → Bedrock Claude Haiku&lt;/td&gt;
&lt;td&gt;judge stays inside AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App wrapper&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TruChain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;captures Records for the LangChain RAG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedbacks&lt;/td&gt;
&lt;td&gt;triad (context, groundedness, answer)&lt;/td&gt;
&lt;td&gt;three-edge completeness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync path&lt;/td&gt;
&lt;td&gt;2% of prod requests&lt;/td&gt;
&lt;td&gt;real-time alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Async path&lt;/td&gt;
&lt;td&gt;98% of prod requests&lt;/td&gt;
&lt;td&gt;zero added latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly back-fill&lt;/td&gt;
&lt;td&gt;batch judge calls&lt;/td&gt;
&lt;td&gt;full coverage within 24h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, the RAG's every prod query is stored as a Record in Postgres, 2% of Records get scored instantly with the triad, and a nightly Airflow DAG back-fills the remaining 98% using batch LLM calls. The bank's compliance team sees no data leaving the VPC; the product team sees a real-time dashboard of triad scores per app version; a bad deploy shows up as a scored-lower app version within an hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Records stored / day&lt;/td&gt;
&lt;td&gt;600,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync-scored Records / day&lt;/td&gt;
&lt;td&gt;12,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch-scored Records / day&lt;/td&gt;
&lt;td&gt;588,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedback cost / day (batch discount)&lt;/td&gt;
&lt;td&gt;~$4,580&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data egress from VPC&lt;/td&gt;
&lt;td&gt;0 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time from bad deploy to alert&lt;/td&gt;
&lt;td&gt;≤ 15 min (on sync tail)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage in Postgres / day&lt;/td&gt;
&lt;td&gt;~600 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;RAG triad completeness&lt;/strong&gt;&lt;/strong&gt; — the three edges (query↔context, context↔answer, query↔answer) form a triangle; a failure on any edge is a distinct failure mode. Context-relevance failure = bad retriever; groundedness failure = hallucination; answer-relevance failure = off-topic answer. Measuring only one edge lets the other two regress invisibly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Postgres backend inside VPC&lt;/strong&gt;&lt;/strong&gt; — TruLens's SQLAlchemy backend is swappable; pointing it at an in-VPC Postgres keeps every Record and every score inside the bank's perimeter. No data crosses a vendor boundary at any point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;LiteLLM + Bedrock judge&lt;/strong&gt;&lt;/strong&gt; — the judge LLM also stays inside AWS (Bedrock is region-local). LiteLLM's unified provider API means switching between Bedrock, self-hosted vLLM, or Azure OpenAI is a one-line change with the rest of the eval code unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;2% sync + 98% async&lt;/strong&gt;&lt;/strong&gt; — the sampled-sync tier gives real-time alerts (a bad deploy shows up in 12,000 scored Records within 15 minutes), while the async tier back-fills the aggregate view without adding latency to 98% of user requests. Coverage is 100% within 24 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — 2% × $0.015 sync + 98% × $0.007 (batch discount) per Record ≈ $4,580/day for 600k queries, versus $9,000/day if all 100% went sync. Compared to shipping a bad model without any triad scoring, the alert-lead-time saving alone is worth 10× the eval cost. O(1) latency for 98% of prod requests; O(1) latency to detect regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Python&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — python&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Python problems on wrapper and observability patterns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/python" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Design&lt;/span&gt;
&lt;span&gt;Topic — design&lt;/span&gt;
&lt;strong&gt;Design problems on VPC-first eval systems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Ragas — reference-free RAG metrics for CI
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Reference-free metrics (&lt;code&gt;faithfulness&lt;/code&gt;, &lt;code&gt;answer_relevancy&lt;/code&gt;) plus reference-based (&lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;) — one &lt;code&gt;evaluate()&lt;/code&gt; call, one scored DataFrame
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;ragas&lt;/code&gt; is the metric library that turns &lt;code&gt;rag evaluation&lt;/code&gt; into a one-line &lt;code&gt;evaluate(dataset, metrics=[...])&lt;/code&gt; call returning a pandas DataFrame of scored rows — with two metrics (&lt;code&gt;faithfulness&lt;/code&gt;, &lt;code&gt;answer_relevancy&lt;/code&gt;) that need no reference answer and two metrics (&lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;) that use a labelled ground-truth, all judged by any LLM you configure via LiteLLM — and its sweet spot is the batch CI gate where a nightly Airflow job scores a fixed golden set, uploads the DataFrame, and a GitHub Action fails the PR when any metric regresses beyond a threshold&lt;/strong&gt;. Ragas is the least opinionated of the four tools: it doesn't wrap your app, it doesn't own a dashboard, it just scores.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbolqq8ygtbg1xrtcasip.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbolqq8ygtbg1xrtcasip.jpeg" alt="Iconographic Ragas diagram — a CI pipeline card on the left with a green tick, four Ragas metric chips (faithfulness, answer_relevancy, context_precision, context_recall) in the centre, and a GitHub-gate glyph on the right blocking a red PR." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for Ragas.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Groundedness.&lt;/strong&gt; &lt;code&gt;faithfulness&lt;/code&gt; — split the answer into atomic claims, ask the LLM judge whether each claim is entailed by the retrieved contexts, aggregate. Same intent as TruLens's &lt;code&gt;groundedness&lt;/code&gt;; different prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer relevance.&lt;/strong&gt; &lt;code&gt;answer_relevancy&lt;/code&gt; — generate 3 hypothetical questions the answer could address, compute cosine similarity between each and the actual query, aggregate. Requires an embedding model in addition to the judge LLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context precision / recall.&lt;/strong&gt; &lt;code&gt;context_precision&lt;/code&gt; — for each retrieved chunk, is it relevant to the question given the ground-truth answer? Ranked precision. &lt;code&gt;context_recall&lt;/code&gt; — decompose the ground-truth answer into statements; check which are covered by the retrieved contexts. Both require a labelled ground truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency + cost.&lt;/strong&gt; Not measured by Ragas itself. Wire LangSmith or TruLens for those axes, or compute them in your own wrapper. Ragas is metric-only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reference-free vs reference-based — the split that matters.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reference-free:&lt;/strong&gt; &lt;code&gt;faithfulness&lt;/code&gt;, &lt;code&gt;answer_relevancy&lt;/code&gt;. Need only &lt;code&gt;(question, answer, contexts)&lt;/code&gt;. Cheap to curate the dataset — you don't need a golden answer written by a human. Perfect for bootstrapping eval when you have zero ground-truth data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference-based:&lt;/strong&gt; &lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;, &lt;code&gt;answer_correctness&lt;/code&gt;, &lt;code&gt;answer_similarity&lt;/code&gt;. Need &lt;code&gt;(question, answer, contexts, ground_truth)&lt;/code&gt;. Ground truth costs human time to curate; budget 5-10 minutes per row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bootstrap pattern.&lt;/strong&gt; Start with reference-free (day one). Add reference-based as you accumulate labelled rows (weeks in). Ragas's DataFrame skips reference-based metrics gracefully if &lt;code&gt;ground_truth&lt;/code&gt; is missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;EvaluationDataset&lt;/code&gt; and &lt;code&gt;evaluate()&lt;/code&gt; shape.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Row shape.&lt;/strong&gt; &lt;code&gt;{question, answer, contexts: list[str], ground_truth?}&lt;/code&gt; per row. Contexts is a list of chunk strings; &lt;code&gt;ground_truth&lt;/code&gt; is optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset.&lt;/strong&gt; &lt;code&gt;Dataset.from_list(rows)&lt;/code&gt; (HuggingFace &lt;code&gt;datasets.Dataset&lt;/code&gt;); or Ragas's own &lt;code&gt;EvaluationDataset&lt;/code&gt; in v0.2+. Both interoperate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;evaluate.&lt;/strong&gt; &lt;code&gt;evaluate(dataset, metrics=[faithfulness, answer_relevancy, ...], llm=..., embeddings=...)&lt;/code&gt;. Returns a &lt;code&gt;Result&lt;/code&gt; with &lt;code&gt;.to_pandas()&lt;/code&gt; giving per-row scores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM + embeddings.&lt;/strong&gt; Any LiteLLM-compatible LLM as judge; any HF or provider embeddings for &lt;code&gt;answer_relevancy&lt;/code&gt;. Common: Claude Haiku judge + OpenAI text-embedding-3-large.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on Ragas.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What's reference-free vs reference-based?" — required answer: reference-free needs no ground-truth answer; reference-based does.&lt;/li&gt;
&lt;li&gt;"Which Ragas metrics need ground truth?" — &lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;, &lt;code&gt;answer_correctness&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"How is &lt;code&gt;faithfulness&lt;/code&gt; computed?" — split answer into claims; judge each against contexts; aggregate.&lt;/li&gt;
&lt;li&gt;"How would you gate a PR on Ragas?" — nightly Airflow → &lt;code&gt;evaluate()&lt;/code&gt; → JSON → GitHub Action compares to thresholds.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — score a golden set with the four Ragas metrics
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Ragas workflow: a golden &lt;code&gt;.jsonl&lt;/code&gt; file of &lt;code&gt;(question, answer, contexts, ground_truth)&lt;/code&gt; rows, one &lt;code&gt;evaluate()&lt;/code&gt; call returning a scored DataFrame, threshold comparison, JSON dump. Walk through the setup.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Golden set.&lt;/strong&gt; 100 rows curated by the product manager + support team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics.&lt;/strong&gt; All four.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge.&lt;/strong&gt; Claude Haiku via LiteLLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings.&lt;/strong&gt; OpenAI &lt;code&gt;text-embedding-3-large&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Score the golden set with the four Ragas metrics and emit a JSON report suitable for CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Required for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;str&lt;/td&gt;
&lt;td&gt;all metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;answer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;str&lt;/td&gt;
&lt;td&gt;all metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;contexts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;list[str]&lt;/td&gt;
&lt;td&gt;all metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ground_truth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;str&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ragas_gate.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;evaluate&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;faithfulness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer_relevancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_recall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas.llms&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LangchainLLMWrapper&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas.embeddings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LangchainEmbeddingsWrapper&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatAnthropic&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIEmbeddings&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Load the golden set
&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval/golden_v3.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Run the pipeline on each question, populating `answer` and `contexts`
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myapp.rag&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;rag_chain&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rag_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contexts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_content&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;

&lt;span class="n"&gt;ds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Score with Ragas
&lt;/span&gt;&lt;span class="n"&gt;judge&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LangchainLLMWrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-3-5-haiku-20241022&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LangchainEmbeddingsWrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OpenAIEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;faithfulness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer_relevancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_recall&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 4. Aggregate and emit
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_pandas&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;means&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;faithfulness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer_relevancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_recall&lt;/span&gt;&lt;span class="p"&gt;]]].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;means&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;means&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval/report.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;THRESHOLDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faithfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer_relevancy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_precision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_recall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;THRESHOLDS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;means&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;THRESHOLDS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RAGAS GATE FAILED:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RAGAS GATE PASSED:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;means&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The golden &lt;code&gt;.jsonl&lt;/code&gt; starts with just &lt;code&gt;(question, ground_truth)&lt;/code&gt; per row; the pipeline call in step 2 populates &lt;code&gt;answer&lt;/code&gt; and &lt;code&gt;contexts&lt;/code&gt; on the fly. This means the eval is scoring the &lt;em&gt;current&lt;/em&gt; pipeline, not a stale snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LangchainLLMWrapper&lt;/code&gt; and &lt;code&gt;LangchainEmbeddingsWrapper&lt;/code&gt; adapt any LangChain LLM/embeddings object to Ragas's provider interface. Any LiteLLM-supported model works; picking Haiku for the judge keeps the per-row judge cost around $0.003.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;evaluate()&lt;/code&gt; runs each metric against each row in parallel (Ragas uses async under the hood). For 100 rows × 4 metrics, expect ~2 minutes wall-clock and ~$1.60 in judge + embedding calls.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;to_pandas()&lt;/code&gt; DataFrame has one column per metric, one row per input example, plus the original inputs. Aggregating with &lt;code&gt;.mean()&lt;/code&gt; gives you the scalar scores for the gate; retaining the per-row DataFrame lets you drill down to the low-scoring rows.&lt;/li&gt;
&lt;li&gt;The gate raises &lt;code&gt;SystemExit(1)&lt;/code&gt; on any threshold failure — that's how Airflow / GitHub Actions detect the failure. The report JSON is uploaded as a workflow artifact so the PR comment bot can render it inline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Pass?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;faithfulness&lt;/td&gt;
&lt;td&gt;0.87&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;answer_relevancy&lt;/td&gt;
&lt;td&gt;0.83&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;context_precision&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;context_recall&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For a first Ragas gate, ship reference-free metrics only (&lt;code&gt;faithfulness&lt;/code&gt;, &lt;code&gt;answer_relevancy&lt;/code&gt;) so you don't need labelled ground truth on day one. Add &lt;code&gt;context_precision&lt;/code&gt; / &lt;code&gt;context_recall&lt;/code&gt; in week two once you've labelled 30-50 rows. Never gate on a metric whose baseline you haven't measured for two weeks.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — nightly Airflow DAG + GitHub PR gate
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The production shape of a Ragas gate is a nightly Airflow DAG (for main-branch trend) plus a per-PR GitHub Action (for the blocking gate). Walk through both.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nightly DAG.&lt;/strong&gt; Runs &lt;code&gt;ragas_gate.py&lt;/code&gt; against main; stores the JSON in S3; emits Slack alert on regression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-PR gate.&lt;/strong&gt; GitHub Action runs the same &lt;code&gt;ragas_gate.py&lt;/code&gt; against the PR branch; posts scores as a PR comment; fails the check on regression.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Wire the nightly Airflow DAG and the per-PR GitHub Action, sharing the same &lt;code&gt;ragas_gate.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Behaviour on failure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nightly Airflow&lt;/td&gt;
&lt;td&gt;cron 03:00 UTC&lt;/td&gt;
&lt;td&gt;Slack alert + preserve trend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-PR GitHub Action&lt;/td&gt;
&lt;td&gt;pull_request on prompts/&lt;strong&gt;, src/rag/&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Fail check + comment scores&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dags/ragas_nightly.py — Airflow DAG
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.decorators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="n"&gt;DEFAULTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mlops&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry_delay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="nd"&gt;@dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ragas_nightly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
     &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0 3 * * *&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DEFAULTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ragas_nightly&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
        &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/opt/eval/ragas_gate.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/opt/eval/eval/report.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLACK_WEBHOOK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:rotating_light: Nightly Ragas gate failed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;report&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
```&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
        &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ragas/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="o"&gt;/%&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;/%&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/report.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mlops-eval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;ragas_nightly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/ragas-gate.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ragas Gate&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompts/**"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/rag/**"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval/**"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ragas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Ragas gate&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;${{ secrets.OPENAI_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python eval/ragas_gate.py&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Comment scores on PR&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const fs = require("fs");&lt;/span&gt;
            &lt;span class="s"&gt;let body = "## Ragas gate\n";&lt;/span&gt;
            &lt;span class="s"&gt;try {&lt;/span&gt;
              &lt;span class="s"&gt;const rpt = JSON.parse(fs.readFileSync("eval/report.json"));&lt;/span&gt;
              &lt;span class="s"&gt;body += "| metric | score |\n|---|---|\n";&lt;/span&gt;
              &lt;span class="s"&gt;for (const [k, v] of Object.entries(rpt.means))&lt;/span&gt;
                &lt;span class="s"&gt;body += `| ${k} | ${v.toFixed(3)} |\n`;&lt;/span&gt;
            &lt;span class="s"&gt;} catch (e) { body += "_no report produced_"; }&lt;/span&gt;
            &lt;span class="s"&gt;github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner, repo: context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number, body&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload report artifact&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ragas-report&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;eval/report.json&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Both entry points call the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;ragas_gate.py&lt;/code&gt;. Sharing the entry point means the nightly trend and the per-PR gate can never diverge in what they measure. Any threshold change is a code review against one file.&lt;/li&gt;
&lt;li&gt;The Airflow DAG's &lt;code&gt;score&lt;/code&gt; task shells out to the Python script and captures the return code plus the JSON report. &lt;code&gt;alert&lt;/code&gt; fires the Slack webhook on failure; &lt;code&gt;archive&lt;/code&gt; writes the daily report to S3 at &lt;code&gt;ragas/YYYY/MM/DD/report.json&lt;/code&gt; — the historical trend lives in S3, queryable via Athena.&lt;/li&gt;
&lt;li&gt;The GitHub Action gates on &lt;code&gt;pull_request&lt;/code&gt; events touching prompts, RAG source, or eval configs. &lt;code&gt;paths:&lt;/code&gt; filters keep the gate cheap — it doesn't run on markdown-only PRs. &lt;code&gt;if: always()&lt;/code&gt; on the comment step ensures the PR gets scored even when the gate fails.&lt;/li&gt;
&lt;li&gt;The comment step uses &lt;code&gt;actions/github-script@v7&lt;/code&gt; to render a markdown table of scores. On failure, the PR shows: red X check, comment table with scores, and a workflow artifact holding the raw report.&lt;/li&gt;
&lt;li&gt;The 15-minute &lt;code&gt;timeout-minutes&lt;/code&gt; guardrails against runaway judge calls (e.g. rate-limit backoffs). If a judge call storms happen, the job fails fast; the gate is treated as "unable to evaluate" and the human reviewer decides whether to re-run or override.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Nightly DAG&lt;/th&gt;
&lt;th&gt;PR gate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline (all metrics pass)&lt;/td&gt;
&lt;td&gt;archive JSON; no alert&lt;/td&gt;
&lt;td&gt;green check; scores as comment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Faithfulness regression on main&lt;/td&gt;
&lt;td&gt;Slack alert to &lt;code&gt;#llm-eval&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Faithfulness regression on PR&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;red X; PR blocked; scores as comment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge API rate-limited&lt;/td&gt;
&lt;td&gt;retry once, then Slack alert&lt;/td&gt;
&lt;td&gt;red X; artifact = partial report&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Ship the nightly DAG &lt;em&gt;before&lt;/em&gt; the per-PR gate. Two weeks of nightly baselines tell you where the thresholds should sit; setting thresholds on day one is guessing. Once the trend is stable, wire the per-PR gate at the observed p10 (the 10th percentile of the last 14 days).&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on Ragas as the CI gate
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design a per-PR Ragas gate for a RAG chain. The gate must fail the PR on regression across four metrics, comment scores back on the PR, and archive results daily. Include a custom brand-voice metric. What thresholds do you set on day one?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using Ragas evaluate + custom metric + GitHub Action + nightly Airflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ragas_full_gate.py — CI gate + custom metric + report emitter
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;evaluate&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;faithfulness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer_relevancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;context_precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_recall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas.llms&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LangchainLLMWrapper&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas.embeddings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LangchainEmbeddingsWrapper&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatAnthropic&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIEmbeddings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;custom_metric&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BrandVoiceCompliance&lt;/span&gt;   &lt;span class="c1"&gt;# from previous example
&lt;/span&gt;
&lt;span class="n"&gt;GOLDEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval/golden_v3.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;THRESHOLDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faithfulness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer_relevancy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_precision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_recall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand_voice_compliance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myapp.rag&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;rag_chain&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;GOLDEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rag_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contexts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_content&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;

    &lt;span class="n"&gt;judge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LangchainLLMWrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-3-5-haiku-20241022&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;emb&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LangchainEmbeddingsWrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OpenAIEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;faithfulness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer_relevancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;context_recall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BrandVoiceCompliance&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
                      &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;emb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;means&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_pandas&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;THRESHOLDS&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval/report.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;means&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;means&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;commit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GITHUB_SHA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;THRESHOLDS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;means&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;THRESHOLDS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GATE FAILED:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GATE PASSED:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;means&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dataset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eval/golden_v3.jsonl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100 curated &lt;code&gt;(question, ground_truth)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline runner&lt;/td&gt;
&lt;td&gt;&lt;code&gt;run_pipeline()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;populate &lt;code&gt;answer&lt;/code&gt; + &lt;code&gt;contexts&lt;/code&gt; from prod code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metrics&lt;/td&gt;
&lt;td&gt;5 (4 Ragas + 1 custom)&lt;/td&gt;
&lt;td&gt;faithfulness + answer_relevancy + context_precision + context_recall + brand_voice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge&lt;/td&gt;
&lt;td&gt;Claude Haiku, temp=0&lt;/td&gt;
&lt;td&gt;deterministic scoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embeddings&lt;/td&gt;
&lt;td&gt;OpenAI text-embedding-3-large&lt;/td&gt;
&lt;td&gt;for &lt;code&gt;answer_relevancy&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sys.exit(1)&lt;/code&gt; on any regression&lt;/td&gt;
&lt;td&gt;CI-friendly failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comment&lt;/td&gt;
&lt;td&gt;GitHub Actions bot&lt;/td&gt;
&lt;td&gt;markdown table on PR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, the gate blocks any PR that touches &lt;code&gt;prompts/**&lt;/code&gt; or &lt;code&gt;src/rag/**&lt;/code&gt; if any of the five metrics drops below its threshold on the 100-row golden set. The nightly Airflow DAG runs the same script against main, alerts on regression, and archives the JSON to S3. Custom metrics live in &lt;code&gt;myapp.eval.metrics&lt;/code&gt; and are reviewed like any other code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deliverable&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gate script&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eval/ragas_full_gate.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom metrics module&lt;/td&gt;
&lt;td&gt;&lt;code&gt;myapp/eval/metrics/brand_voice.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR gate workflow&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.github/workflows/ragas-gate.yml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly DAG&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dags/ragas_nightly.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archived reports&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3://mlops-eval/ragas/YYYY/MM/DD/report.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threshold config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eval/ragas_full_gate.py&lt;/code&gt; (code-reviewed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack alert channel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#llm-eval&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Reference-free + reference-based split&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;faithfulness&lt;/code&gt; and &lt;code&gt;answer_relevancy&lt;/code&gt; bootstrap the gate without needing labelled ground truth; &lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;, and custom metrics harden it as labels arrive. This lets the gate ship on day one with two axes and grow to five as the dataset matures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;PydanticPrompt custom metric&lt;/strong&gt;&lt;/strong&gt; — extending Ragas with a domain-specific metric is a first-class pattern; the framework handles async, batching, retries, and DataFrame integration. Rolling your own scorer means reimplementing all four; using &lt;code&gt;MetricWithLLM&lt;/code&gt; means writing one method.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;One entry point for CI and nightly&lt;/strong&gt;&lt;/strong&gt; — sharing &lt;code&gt;ragas_full_gate.py&lt;/code&gt; between the PR gate and the Airflow DAG guarantees the two can never diverge. Threshold changes are one file, one code review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;JSON report + GitHub bot comment&lt;/strong&gt;&lt;/strong&gt; — the eval score becomes a first-class artifact on the PR discussion. Reviewers see the numbers inline; no dashboard round-trip; failures are self-explanatory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — 100 rows × 5 metrics × ~$0.003 judge cost ≈ $1.50 per PR gate run. Compared to shipping a bad prompt without a gate, this is a rounding error. O(dataset_size × num_metrics) per run; O(1) latency to detect regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;ETL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;ETL problems on batch quality gates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Data validation&lt;/span&gt;
&lt;span&gt;Topic — data-validation&lt;/span&gt;
&lt;strong&gt;Data validation problems on threshold gating&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Snowflake Cortex Search Ops — eval at the semantic-search layer
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;CORTEX SEARCH SERVICE&lt;/code&gt; + &lt;code&gt;AI_COMPLETE&lt;/code&gt; judges + recall@k in SQL — data, index, model, and eval inside one governance boundary
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;snowflake cortex search&lt;/code&gt; is Snowflake's managed hybrid (BM25 + vector) semantic-search primitive, &lt;code&gt;CORTEX SEARCH SERVICE&lt;/code&gt; is the DDL that turns a table of text into a searchable index, &lt;code&gt;AI_COMPLETE('claude-3-5-sonnet', ...)&lt;/code&gt; is the SQL-level LLM you can use both to generate answers and to &lt;em&gt;judge&lt;/em&gt; them, and Cortex Search Ops is the pattern of running eval — recall@k, MRR, nDCG, groundedness — as &lt;em&gt;plain SQL&lt;/em&gt; over a labelled &lt;code&gt;(query, expected_doc_id)&lt;/code&gt; table so the entire data + index + model + eval stack sits inside one Snowflake account and one governance boundary&lt;/strong&gt;. This is the eval-in-SQL story that fits banks, healthcare, and government where data-egress is a compliance loss.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc1jrr8hebqsi2kz62t54.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc1jrr8hebqsi2kz62t54.jpeg" alt="Iconographic Cortex Search Ops diagram — a semantic search flow with embed → index → hybrid retrieve → eval metrics, plus a Snowflake warehouse boundary and an eval-in-SQL card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for Cortex Search Ops.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Groundedness.&lt;/strong&gt; Compute in SQL via &lt;code&gt;AI_COMPLETE('claude-3-5-sonnet', 'Rate 0-1 whether this answer is grounded in the retrieved context: ...')&lt;/code&gt;. The judge call is one function invocation; parse the numeric response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer relevance.&lt;/strong&gt; Same pattern — &lt;code&gt;AI_COMPLETE&lt;/code&gt; with a "does the answer address the question?" prompt. Cortex handles the model round-trip inside the account; no external API needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context precision / recall.&lt;/strong&gt; Pure IR SQL. Label &lt;code&gt;(query, expected_doc_id)&lt;/code&gt; pairs in a table; run the search; compute &lt;code&gt;recall@k&lt;/code&gt;, &lt;code&gt;MRR&lt;/code&gt;, &lt;code&gt;nDCG@k&lt;/code&gt; with aggregate SQL. This is the classic search-eval pattern reborn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency + cost.&lt;/strong&gt; Query history table (&lt;code&gt;SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY&lt;/code&gt;) captures every &lt;code&gt;CORTEX SEARCH_PREVIEW&lt;/code&gt; and every &lt;code&gt;AI_COMPLETE&lt;/code&gt; call with wall-clock and credits burned. Join to your eval table for per-query cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;CORTEX SEARCH SERVICE&lt;/code&gt; shape.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DDL.&lt;/strong&gt; &lt;code&gt;CREATE OR REPLACE CORTEX SEARCH SERVICE svc ON &amp;lt;text_col&amp;gt; ATTRIBUTES &amp;lt;cols&amp;gt; WAREHOUSE = &amp;lt;wh&amp;gt; TARGET_LAG = '&amp;lt;n&amp;gt; minute' AS &amp;lt;SELECT ...&amp;gt;&lt;/code&gt; — one command; Snowflake handles chunking, embedding, and indexing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh.&lt;/strong&gt; &lt;code&gt;TARGET_LAG&lt;/code&gt; sets how stale the index can be before Snowflake re-indexes. &lt;code&gt;TARGET_LAG = '1 hour'&lt;/code&gt; for near-real-time; &lt;code&gt;'1 day'&lt;/code&gt; for cheaper batch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query.&lt;/strong&gt; &lt;code&gt;SELECT PARSE_JSON(SNOWFLAKE.CORTEX.SEARCH_PREVIEW('svc', 'my query', 8))&lt;/code&gt; returns the top-8 matching rows plus their scores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid retrieval.&lt;/strong&gt; Cortex Search is hybrid (BM25 + vector) under the hood; no separate config needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Eval-in-SQL — the recall@k pattern.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Labels.&lt;/strong&gt; &lt;code&gt;eval_labels(query_id, query_text, expected_doc_id)&lt;/code&gt; — one row per (query, relevant doc) pair. Multi-relevance = multiple rows per query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrievals.&lt;/strong&gt; &lt;code&gt;eval_retrievals(query_id, retrieved_doc_id, rank)&lt;/code&gt; — populated by running Cortex Search on every query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;recall@k.&lt;/strong&gt; &lt;code&gt;SUM(CASE WHEN retrieved.doc_id = labels.doc_id AND rank &amp;lt;= k THEN 1 ELSE 0 END) / COUNT(DISTINCT labels.query_id)&lt;/code&gt; per query, then averaged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MRR / nDCG.&lt;/strong&gt; Standard IR formulas, expressible in pure SQL with window functions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on Cortex Search Ops.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Why eval in SQL?" — required answer: data + index + model + eval in one governance boundary; no data egress.&lt;/li&gt;
&lt;li&gt;"How does Cortex Search compare to Postgres pgvector?" — Cortex is managed hybrid retrieval + indexing + refresh; pgvector is a vector column you index yourself.&lt;/li&gt;
&lt;li&gt;"How would you compute recall@k in SQL?" — required answer: join labels to retrievals, aggregate the match-rank ≤ k signal.&lt;/li&gt;
&lt;li&gt;"How is &lt;code&gt;AI_COMPLETE&lt;/code&gt; different from Bedrock direct?" — Cortex bills through Snowflake credits, stays inside the account, no external API.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — build a Cortex Search Service and score recall@5
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Snowflake eval workflow: create a &lt;code&gt;CORTEX SEARCH SERVICE&lt;/code&gt; on a &lt;code&gt;docs&lt;/code&gt; table, label a &lt;code&gt;(query, expected_doc_id)&lt;/code&gt; table, run search across all labelled queries, compute recall@5 with a single SELECT. Walk through it end-to-end.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docs table.&lt;/strong&gt; &lt;code&gt;docs(doc_id, title, body, updated_at)&lt;/code&gt; — 80,000 rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search service.&lt;/strong&gt; &lt;code&gt;docs_svc&lt;/code&gt; on the &lt;code&gt;body&lt;/code&gt; column, target lag 1 hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Labels.&lt;/strong&gt; &lt;code&gt;eval_labels(query_id, query_text, expected_doc_id)&lt;/code&gt; — 200 rows across 100 queries (multi-relevance).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric.&lt;/strong&gt; &lt;a href="mailto:recall@5"&gt;recall@5&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Create the search service, run it across the label queries, and compute recall@5 in one SQL query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;source text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docs_svc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cortex Search Service (hybrid)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eval_labels&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100 queries × ~2 expected docs each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eval_retrievals&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;populated by search-per-query&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Source docs table (assume already populated)&lt;/span&gt;
&lt;span class="c1"&gt;-- CREATE TABLE docs (doc_id STRING, title STRING, body STRING, updated_at TIMESTAMP);&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Create the search service (indexes body; keeps title as an attribute)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;CORTEX&lt;/span&gt; &lt;span class="k"&gt;SEARCH&lt;/span&gt; &lt;span class="n"&gt;SERVICE&lt;/span&gt; &lt;span class="n"&gt;docs_svc&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
    &lt;span class="n"&gt;ATTRIBUTES&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
    &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eval_wh&lt;/span&gt;
    &lt;span class="n"&gt;TARGET_LAG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;docs&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Labels table (populate by hand or via product-team CSV)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;eval_labels&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;query_id&lt;/span&gt;         &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query_text&lt;/span&gt;       &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expected_doc_id&lt;/span&gt;  &lt;span class="n"&gt;STRING&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- INSERT INTO eval_labels VALUES ('q1', 'how to rotate API key', 'doc_042'), ...&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Populate eval_retrievals by running search on every label query&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;eval_retrievals&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;STRING&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;retrieved_doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;FLOAT&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_text&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eval_labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="n"&gt;FLATTEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PARSE_JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;SNOWFLAKE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CORTEX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SEARCH_PREVIEW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'docs_svc'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="mi"&gt;10&lt;/span&gt;                                 &lt;span class="c1"&gt;-- top-10 for headroom&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 5. recall@5 in one SQL — for each query, did any expected doc appear in top-5?&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;per_query&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieved_doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_doc_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;       &lt;span class="n"&gt;eval_labels&lt;/span&gt;    &lt;span class="n"&gt;l&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt;  &lt;span class="n"&gt;eval_retrievals&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
      &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;recall_at_5&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;per_query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;CREATE CORTEX SEARCH SERVICE&lt;/code&gt; is the one-command index. Snowflake chunks the &lt;code&gt;body&lt;/code&gt; column, embeds each chunk with its managed embedding model, and stores the hybrid index. &lt;code&gt;TARGET_LAG = '1 hour'&lt;/code&gt; means the index is at most 1 hour behind writes to the source table.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SNOWFLAKE.CORTEX.SEARCH_PREVIEW('docs_svc', query, 10)&lt;/code&gt; returns a JSON structure with a &lt;code&gt;results&lt;/code&gt; array of up to 10 top-k rows. &lt;code&gt;LATERAL FLATTEN&lt;/code&gt; unpacks that JSON into rows; &lt;code&gt;r.index + 1&lt;/code&gt; gives the 1-based rank.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;eval_retrievals&lt;/code&gt; table is a snapshot of what the search returned for each label query at eval time. Persisting it (rather than joining on-the-fly) means you can re-analyse different &lt;code&gt;k&lt;/code&gt; values, compute MRR / nDCG, or diff two eval snapshots without re-running the search.&lt;/li&gt;
&lt;li&gt;The recall@5 SELECT joins labels to retrievals on &lt;code&gt;query_id&lt;/code&gt;; per query, it checks whether &lt;em&gt;any&lt;/em&gt; expected doc appears in the top-5 retrieved. &lt;code&gt;MAX(CASE WHEN ...)&lt;/code&gt; is the "any match" reduction; &lt;code&gt;AVG(hit)&lt;/code&gt; across queries gives the scalar &lt;a href="mailto:recall@5"&gt;recall@5&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For multi-relevance recall (average of "fraction of expected docs hit"), swap &lt;code&gt;MAX&lt;/code&gt; for &lt;code&gt;AVG&lt;/code&gt;. For strict recall (all expected docs must appear), swap for &lt;code&gt;MIN&lt;/code&gt;. The SQL primitive is flexible; the reporting choice depends on the domain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;query_id&lt;/th&gt;
&lt;th&gt;expected in top-5?&lt;/th&gt;
&lt;th&gt;rank hit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;q1 (rotate API key)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;q2 (SLA at 3am)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;q3 (export PDF)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;... (98 more)&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;recall@5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.88&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Store &lt;code&gt;eval_retrievals&lt;/code&gt; as a persistent table, not an inline subquery. Re-computing metrics on the stored retrievals is a millisecond query; re-running the retrievals is a minutes-long recall of the search service. Persistence unlocks fast metric iteration.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — MRR + nDCG@10 + &lt;code&gt;AI_COMPLETE&lt;/code&gt; groundedness in SQL
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Recall@k is the entry-level metric; MRR (Mean Reciprocal Rank) rewards top-ranked hits; nDCG@k weighs earlier positions higher; &lt;code&gt;AI_COMPLETE&lt;/code&gt; groundedness scores generated answers. All four fit in one SQL script. Walk through them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MRR.&lt;/strong&gt; For each query, take &lt;code&gt;1 / rank_of_first_expected_doc&lt;/code&gt;; average across queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nDCG@10.&lt;/strong&gt; For each query, sum &lt;code&gt;(2^rel - 1) / log2(rank + 1)&lt;/code&gt; up to rank 10; normalise by ideal DCG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groundedness.&lt;/strong&gt; For each generated answer + its retrieved contexts, &lt;code&gt;AI_COMPLETE&lt;/code&gt; a rating prompt; parse the score.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the SQL that computes MRR, nDCG@10, and groundedness in one script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Requires&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MRR&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eval_retrievals&lt;/code&gt; + &lt;code&gt;eval_labels&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nDCG@10&lt;/td&gt;
&lt;td&gt;+ graded relevance in &lt;code&gt;eval_labels&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groundedness&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eval_answers(query_id, answer)&lt;/code&gt; + &lt;code&gt;eval_retrievals&lt;/code&gt; (as contexts)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- MRR: mean reciprocal rank of the first expected hit&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;first_hit&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_hit_rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;       &lt;span class="n"&gt;eval_labels&lt;/span&gt;    &lt;span class="n"&gt;l&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt;  &lt;span class="n"&gt;eval_retrievals&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
      &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieved_doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_doc_id&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;first_hit_rank&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;first_hit_rank&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mrr&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;first_hit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- nDCG@10: normalised discounted cumulative gain (binary relevance)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;dcg&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eval_labels&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;
                                  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;
                                    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieved_doc_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;LOG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dcg_val&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;eval_retrievals&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;idcg&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;LOG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;idcg_val&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;query_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eval_labels&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dcg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dcg_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idcg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idcg_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ndcg_at_10&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;idcg&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt;   &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;dcg&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Groundedness: AI_COMPLETE judge per (answer, contexts)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;eval_groundedness&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;LISTAGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;---&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WITHIN&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;contexts&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;eval_retrievals&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;docs&lt;/span&gt;           &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieved_doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;AI_COMPLETE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="s1"&gt;'claude-3-5-haiku'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s1"&gt;'Rate 0.0-1.0 whether the ANSWER is fully grounded in the CONTEXT. '&lt;/span&gt;
           &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'Return only the number.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
           &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'ANSWER:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
           &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'CONTEXT:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contexts&lt;/span&gt;
       &lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;FLOAT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;groundedness&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;eval_answers&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;ctx&lt;/span&gt;          &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groundedness&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mean_groundedness&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eval_groundedness&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;MRR uses &lt;code&gt;MIN(rank)&lt;/code&gt; per query to find the first hit, then &lt;code&gt;1/rank&lt;/code&gt;. Queries with no hit contribute 0. &lt;code&gt;AVG&lt;/code&gt; across queries gives the scalar.&lt;/li&gt;
&lt;li&gt;nDCG@10 computes DCG (discounted cumulative gain) per query using the standard &lt;code&gt;1/log2(rank+1)&lt;/code&gt; weighting; IDCG (ideal DCG) is the same formula with all relevant docs assumed at ranks 1..N; the ratio is nDCG. Snowflake's &lt;code&gt;LOG(base, x)&lt;/code&gt; is the native log function.&lt;/li&gt;
&lt;li&gt;The groundedness pass uses &lt;code&gt;LISTAGG&lt;/code&gt; to concatenate the top-5 retrieved doc bodies into one context string per query, then passes &lt;code&gt;answer&lt;/code&gt; + &lt;code&gt;contexts&lt;/code&gt; to &lt;code&gt;AI_COMPLETE&lt;/code&gt; with a rating prompt. &lt;code&gt;AI_COMPLETE&lt;/code&gt; returns a string; &lt;code&gt;::FLOAT&lt;/code&gt; casts the numeric response.&lt;/li&gt;
&lt;li&gt;All three metrics live in one Snowflake account. No data crosses a boundary; no external API call; the entire eval is auditable via &lt;code&gt;SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY&lt;/code&gt;. This is the compliance advantage.&lt;/li&gt;
&lt;li&gt;Cost breakdown: recall@5 / MRR / nDCG are pure SQL (~cents per run against a small labels table); groundedness costs one &lt;code&gt;AI_COMPLETE&lt;/code&gt; call per query (~$0.002 per query with Haiku). 100 queries × $0.002 = ~$0.20 per full eval run.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recall@5&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRR&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nDCG@10&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mean_groundedness&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total cost per run&lt;/td&gt;
&lt;td&gt;~$0.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data egress&lt;/td&gt;
&lt;td&gt;0 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Persist per-metric result tables (&lt;code&gt;eval_recall&lt;/code&gt;, &lt;code&gt;eval_mrr&lt;/code&gt;, &lt;code&gt;eval_groundedness&lt;/code&gt;) with a &lt;code&gt;run_at&lt;/code&gt; timestamp. Diffing two runs is one SELECT; watching a trend is a Snowsight chart; alerting on a regression is a Snowflake Task + email. All four are already Snowflake-native.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on eval-in-SQL at the Cortex Search layer
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You've got a Snowflake-native RAG using &lt;code&gt;CORTEX SEARCH SERVICE&lt;/code&gt; and &lt;code&gt;AI_COMPLETE&lt;/code&gt;. Nothing can leave the account. Design the nightly eval — recall@5, MRR, groundedness — as pure SQL with a TASK, ALERT, and email-on-regression. What are the four objects you deploy?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using Cortex Search Service + AI_COMPLETE judge + TASK + ALERT
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Search service + history table (deployed once)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;CORTEX&lt;/span&gt; &lt;span class="k"&gt;SEARCH&lt;/span&gt; &lt;span class="n"&gt;SERVICE&lt;/span&gt; &lt;span class="n"&gt;docs_svc&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="n"&gt;ATTRIBUTES&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
    &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eval_wh&lt;/span&gt; &lt;span class="n"&gt;TARGET_LAG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;eval_history&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMP_NTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Stored procedure: recall@5 + MRR + groundedness in one call&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;sp_run_cortex_eval&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;UUID_STRING&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;TEMP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;_retr&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;STRING&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;retrieved_doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_text&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eval_labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="n"&gt;FLATTEN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PARSE_JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;SNOWFLAKE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CORTEX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SEARCH_PREVIEW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'docs_svc'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;eval_history&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'recall_at_5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieved_doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_doc_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;
          &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eval_labels&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;_retr&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;eval_history&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mrr'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;first_rank&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;first_rank&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;first_rank&lt;/span&gt;
          &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eval_labels&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;_retr&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
            &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieved_doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_doc_id&lt;/span&gt;
          &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;eval_history&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LISTAGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;---&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WITHIN&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;contexts&lt;/span&gt;
                 &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_retr&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieved_doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;
                 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'groundedness'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AI_COMPLETE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'claude-3-5-haiku'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="s1"&gt;'Rate 0.0-1.0 whether ANSWER is grounded in CONTEXT. Return the number only.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
               &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'ANSWER:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s1"&gt;CONTEXT:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eval_answers&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="s1"&gt;'ok:'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. TASK: nightly at 03:00 UTC&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;TASK&lt;/span&gt; &lt;span class="n"&gt;t_nightly_eval&lt;/span&gt;
    &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eval_wh&lt;/span&gt; &lt;span class="n"&gt;SCHEDULE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'USING CRON 0 3 * * * UTC'&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;sp_run_cortex_eval&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;TASK&lt;/span&gt; &lt;span class="n"&gt;t_nightly_eval&lt;/span&gt; &lt;span class="n"&gt;RESUME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. ALERT: fire when any metric drops below trailing 30-day p10 × 0.95&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;ALERT&lt;/span&gt; &lt;span class="n"&gt;a_eval_regression&lt;/span&gt;
    &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eval_wh&lt;/span&gt; &lt;span class="n"&gt;SCHEDULE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'30 MINUTE'&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eval_history&lt;/span&gt;
            &lt;span class="n"&gt;QUALIFY&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;run_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;baseline&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PERCENTILE_CONT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WITHIN&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;p10&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eval_history&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;run_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;DATEADD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;SYSTEM&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;SEND_EMAIL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mlops_alerts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mlops@acme.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'Cortex Search eval regression'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'One or more Cortex Search metrics dropped below the 30-day p10.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;ALERT&lt;/span&gt; &lt;span class="n"&gt;a_eval_regression&lt;/span&gt; &lt;span class="n"&gt;RESUME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Index&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CORTEX SEARCH SERVICE docs_svc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hybrid BM25 + vector search on &lt;code&gt;docs.body&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Labels&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eval_labels&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;curated &lt;code&gt;(query_id, query_text, expected_doc_id)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrievals&lt;/td&gt;
&lt;td&gt;temp &lt;code&gt;_retr&lt;/code&gt; inside SP&lt;/td&gt;
&lt;td&gt;rebuilt each run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eval_history&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;time series of all metric scores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AI_COMPLETE('claude-3-5-haiku', ...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;in-account LLM judge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TASK t_nightly_eval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;daily at 03:00 UTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alert&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ALERT a_eval_regression&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;every 30 min; email on p10 regression&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, every night at 03:00 UTC Snowflake reruns the eval end-to-end, appends four rows to &lt;code&gt;eval_history&lt;/code&gt;, and the ALERT fires an email if any metric drops 5% below the 30-day p10. The entire pipeline is Snowflake-native — no Airflow, no Kubernetes, no external SMTP; the compliance team has one governance boundary to audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deliverable&lt;/th&gt;
&lt;th&gt;Snowflake object&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search index&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CORTEX SEARCH SERVICE docs_svc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric history&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eval_history&lt;/code&gt; table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runner&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PROCEDURE sp_run_cortex_eval&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TASK t_nightly_eval&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regression detector&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ALERT a_eval_regression&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alert channel&lt;/td&gt;
&lt;td&gt;email to &lt;code&gt;mlops@acme.com&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total credits/day&lt;/td&gt;
&lt;td&gt;~2-5 (small warehouse × short runtime)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data egress&lt;/td&gt;
&lt;td&gt;0 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;One governance boundary&lt;/strong&gt;&lt;/strong&gt; — data (&lt;code&gt;docs&lt;/code&gt;), index (&lt;code&gt;docs_svc&lt;/code&gt;), model (&lt;code&gt;AI_COMPLETE&lt;/code&gt;), judge (same &lt;code&gt;AI_COMPLETE&lt;/code&gt;), eval history (&lt;code&gt;eval_history&lt;/code&gt;), scheduler (&lt;code&gt;TASK&lt;/code&gt;), and alerter (&lt;code&gt;ALERT&lt;/code&gt;) are all inside one Snowflake account. Nothing crosses a compliance boundary. This is the decisive advantage over any external eval stack in regulated environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cortex Search hybrid retrieval&lt;/strong&gt;&lt;/strong&gt; — Snowflake's &lt;code&gt;CORTEX SEARCH SERVICE&lt;/code&gt; is BM25 + vector out of the box; no separate embedding pipeline, no separate index cluster. &lt;code&gt;TARGET_LAG&lt;/code&gt; handles refresh; the DDL is one command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;AI_COMPLETE&lt;/code&gt; as SQL judge&lt;/strong&gt;&lt;/strong&gt; — LLM calls are just another SQL function. You can wrap them in &lt;code&gt;AVG&lt;/code&gt;, join them, filter them, &lt;code&gt;LISTAGG&lt;/code&gt; inputs, cast outputs. This is the "eval in SQL" pattern taken to its logical conclusion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;TASK + ALERT native scheduler&lt;/strong&gt;&lt;/strong&gt; — Snowflake's TASK is a cron; ALERT is a change-detection primitive; both are managed, both are governed by RBAC. No external Airflow or PagerDuty needed for the core loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — ~2-5 credits/day for a small eval warehouse running a ~3-minute nightly job, plus ~$0.20 in &lt;code&gt;AI_COMPLETE&lt;/code&gt; judge calls. Compared to shipping a bad Cortex Search config without a nightly gate, the alert-lead-time saving alone is worth 100× the eval cost. O(N_labels × k) for retrieval; O(N_queries) for judge; O(1) latency to detect regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems on IR metrics and window functions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on in-warehouse eval systems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — LLM eval recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Which tool when.&lt;/strong&gt; LangSmith when you're already inside LangChain/LangGraph and want a hosted trace + dataset + experiment UI. TruLens when you need local-first, VPC-safe, framework-agnostic instrumentation with the RAG triad. Ragas when you need a metric library that returns a scored DataFrame for the CI gate (nightly Airflow + per-PR GitHub Action). Cortex Search Ops when the entire data + index + model + eval stack lives inside Snowflake and compliance forbids data egress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The four axes to score.&lt;/strong&gt; Groundedness (fraction of answer claims supported by context — anti-hallucination), answer relevance (does the answer address the question), context precision + recall (retriever quality), latency + cost (pipeline axis). Any single-axis eval hides regressions on the other three.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LangSmith trace boilerplate.&lt;/strong&gt; &lt;code&gt;os.environ["LANGSMITH_TRACING"]="true"; os.environ["LANGSMITH_PROJECT"]="prod"&lt;/code&gt; before importing LangChain — auto-traces every LLM call. &lt;code&gt;@traceable(run_type="chain", name="answer_endpoint", tags=["prod","v2.3"])&lt;/code&gt; on FastAPI handlers to add the top-level Trace. Sample in production with &lt;code&gt;LANGSMITH_TRACING_SAMPLE_RATE=0.01&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LangSmith dataset + pairwise gate.&lt;/strong&gt; &lt;code&gt;ls.evaluate(target_fn, data="dataset_v3", experiment_prefix="candidate_v2")&lt;/code&gt; runs a candidate against a pinned dataset version. &lt;code&gt;evaluate_comparative(experiments=[old, new], evaluators=[pairwise_helpfulness])&lt;/code&gt; scores which experiment wins per row. Gate on win-rate &lt;code&gt;≥ 0.60&lt;/code&gt; (excluding ties).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TruLens RAG triad boilerplate.&lt;/strong&gt; Wrap the app with &lt;code&gt;TruChain(chain, app_name="rag", app_version="v2.3", feedbacks=[f_ctx_rel, f_grounded, f_answer_rel])&lt;/code&gt;. Each &lt;code&gt;Feedback&lt;/code&gt; binds a provider metric plus selectors: &lt;code&gt;context_selector = Select.RecordCalls.retriever.get_relevant_documents.rets[:].page_content&lt;/code&gt;. Aggregate per-chunk relevance with &lt;code&gt;np.mean&lt;/code&gt;; collect contexts for groundedness with &lt;code&gt;.collect()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TruLens VPC production pattern.&lt;/strong&gt; &lt;code&gt;TruSession(database_url="postgresql://...")&lt;/code&gt; keeps every Record + score inside a VPC Postgres. &lt;code&gt;LiteLLM(model_engine="bedrock/anthropic.claude-3-5-haiku-...")&lt;/code&gt; keeps the judge in-region. Score 2% sync for real-time alerts; nightly batch back-fill the remaining 98% for zero user-latency impact and batch-price discounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ragas gate script.&lt;/strong&gt; &lt;code&gt;evaluate(Dataset.from_list(rows), metrics=[faithfulness, answer_relevancy, context_precision, context_recall], llm=judge, embeddings=emb)&lt;/code&gt; returns a &lt;code&gt;Result&lt;/code&gt; with &lt;code&gt;.to_pandas()&lt;/code&gt;. Aggregate with &lt;code&gt;.mean()&lt;/code&gt;, compare against &lt;code&gt;THRESHOLDS&lt;/code&gt; dict, &lt;code&gt;sys.exit(1)&lt;/code&gt; on any regression. Ship reference-free metrics first (&lt;code&gt;faithfulness&lt;/code&gt;, &lt;code&gt;answer_relevancy&lt;/code&gt;); add reference-based (&lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;) as labels arrive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ragas CI wiring.&lt;/strong&gt; One &lt;code&gt;ragas_gate.py&lt;/code&gt; shared between the per-PR GitHub Action (&lt;code&gt;pull_request&lt;/code&gt; on &lt;code&gt;prompts/**&lt;/code&gt;, &lt;code&gt;src/rag/**&lt;/code&gt;) and the nightly Airflow DAG (&lt;code&gt;schedule="0 3 * * *"&lt;/code&gt;). PR posts markdown-table scores as a comment; nightly alerts Slack on regression and archives report JSON to S3 at &lt;code&gt;ragas/YYYY/MM/DD/report.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cortex Search Service + eval-in-SQL.&lt;/strong&gt; &lt;code&gt;CREATE CORTEX SEARCH SERVICE svc ON body WAREHOUSE=wh TARGET_LAG='1 hour' AS (SELECT ... FROM docs)&lt;/code&gt; for the managed hybrid index; &lt;code&gt;SNOWFLAKE.CORTEX.SEARCH_PREVIEW('svc', q, k)&lt;/code&gt; for retrieval. Eval: recall@k = &lt;code&gt;AVG(MAX(CASE WHEN retrieved_doc_id=expected_doc_id AND rank&amp;lt;=k THEN 1 ELSE 0 END))&lt;/code&gt;; MRR = &lt;code&gt;AVG(1.0 / MIN(first_hit_rank))&lt;/code&gt;; groundedness = &lt;code&gt;AVG(AI_COMPLETE('claude-3-5-haiku', prompt)::FLOAT)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cortex Search Ops scheduler.&lt;/strong&gt; &lt;code&gt;TASK t_nightly_eval SCHEDULE='USING CRON 0 3 * * * UTC' AS CALL sp_run_cortex_eval();&lt;/code&gt; + &lt;code&gt;ALERT a_eval_regression SCHEDULE='30 MINUTE' IF (EXISTS &amp;lt;latest score &amp;lt; 30-day p10 × 0.95&amp;gt;) THEN CALL SYSTEM$SEND_EMAIL(...)&lt;/code&gt;. Data, index, model, judge, eval, scheduler, alerter — all in one Snowflake account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric decision matrix.&lt;/strong&gt; Groundedness → Ragas &lt;code&gt;faithfulness&lt;/code&gt; OR TruLens &lt;code&gt;groundedness&lt;/code&gt; OR Cortex &lt;code&gt;AI_COMPLETE&lt;/code&gt; judge. Answer relevance → Ragas &lt;code&gt;answer_relevancy&lt;/code&gt; OR TruLens &lt;code&gt;relevance&lt;/code&gt;. Retriever quality → Ragas &lt;code&gt;context_precision&lt;/code&gt;/&lt;code&gt;context_recall&lt;/code&gt; OR Cortex &lt;code&gt;recall@k&lt;/code&gt;/&lt;code&gt;MRR&lt;/code&gt;/&lt;code&gt;nDCG&lt;/code&gt;. Latency + cost → LangSmith &lt;code&gt;Run.latency&lt;/code&gt;/&lt;code&gt;Run.total_cost&lt;/code&gt; OR TruLens &lt;code&gt;record.latency&lt;/code&gt;/&lt;code&gt;record.cost&lt;/code&gt; OR Snowflake &lt;code&gt;QUERY_HISTORY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI gate thresholds — day-one starting points.&lt;/strong&gt; Faithfulness ≥ 0.85, answer_relevancy ≥ 0.80, context_precision ≥ 0.70, context_recall ≥ 0.75, latency p95 ≤ 4s, cost per query ≤ $0.015. Measure the observed baseline for two weeks &lt;em&gt;before&lt;/em&gt; setting thresholds; day-one thresholds are guesses. Lift thresholds as the pipeline matures; never lower them without a written justification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge selection + validation.&lt;/strong&gt; Use a cheap fast judge (Claude Haiku, Gemini Flash) with &lt;code&gt;temperature=0&lt;/code&gt; for determinism. Re-validate quarterly against 100 human-labelled rows; if judge-human correlation drops below 0.75, swap the judge model or add few-shot examples to the prompt. Pairwise judges are more sensitive than pointwise judges for A/B comparisons; use pointwise for absolute thresholds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost budgeting rules.&lt;/strong&gt; Judge cost ≈ 1-2× production LLM cost per evaluated row. Sample 1-5% of production for sync scoring; back-fill 100% via batch (50% discount on most providers). CI gate over 100-row golden set ≈ $1-2 per PR run. Never run 100% sync judges in production; the extra latency and cost destroy the value proposition.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is LLM evaluation for data pipelines in one sentence?
&lt;/h3&gt;

&lt;p&gt;LLM evaluation for data pipelines is the discipline of scoring every LLM-driven output your pipeline produces — RAG answers, summaries, classifications, SQL-generation results — against a small set of orthogonal quality axes (&lt;code&gt;groundedness&lt;/code&gt;, &lt;code&gt;answer relevance&lt;/code&gt;, context precision / recall, latency + cost) so hallucinations, retrieval misses, prompt drift, and cost explosions are caught by an automated gate rather than by a customer three weeks later. In 2026 the four canonical tools are LangSmith (trace-first hosted platform with versioned datasets and hosted judges), TruLens (local-first, VPC-safe, framework-agnostic with the RAG triad), Ragas (reference-free metric library that returns a scored DataFrame for CI gates), and Snowflake Cortex Search Ops (eval-in-SQL with &lt;code&gt;AI_COMPLETE&lt;/code&gt; judges when the entire data + model stack lives in Snowflake). The tools coexist because their sweet spots don't overlap; the wrong senior answer is picking one and calling the eval done. Every senior data-engineering interview probes LLM eval because it's the load-bearing correctness discipline for the modern RAG + agent stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  LangSmith vs TruLens vs Ragas — which do I pick?
&lt;/h3&gt;

&lt;p&gt;Default to &lt;strong&gt;LangSmith&lt;/strong&gt; when your app already lives inside LangChain or LangGraph and you want a hosted UI: trace tree, versioned datasets, hosted evaluators, pairwise experiments, all in one dashboard with per-Run latency and cost attached for free. Pick &lt;strong&gt;TruLens&lt;/strong&gt; when you need eval inside a VPC or want to instrument a non-LangChain Python app: &lt;code&gt;TruChain&lt;/code&gt; / &lt;code&gt;TruLlama&lt;/code&gt; / &lt;code&gt;TruCustomApp&lt;/code&gt; wrap any Python function; the RAG triad (context relevance, groundedness, answer relevance) is the canonical three-edge completeness proof; storage defaults to local SQLite and swaps to Postgres for team use; no vendor round-trip is required at any point. Pick &lt;strong&gt;Ragas&lt;/strong&gt; when you want a metric library that plays well with any CI stack: &lt;code&gt;evaluate()&lt;/code&gt; returns a pandas DataFrame; two metrics (&lt;code&gt;faithfulness&lt;/code&gt;, &lt;code&gt;answer_relevancy&lt;/code&gt;) need no reference answer so bootstrapping is cheap; two metrics (&lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;) use labelled ground truth; the whole thing runs headless in an Airflow DAG or a GitHub Action. In practice most teams end up running two of the three: LangSmith or TruLens for traces + interactive UI, Ragas for the CI gate. The three are complementary, not competitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the RAG triad?
&lt;/h3&gt;

&lt;p&gt;The RAG triad is TruLens's canonical eval completeness proof for a Retrieval-Augmented Generation system: a triangle whose three vertices are the &lt;em&gt;query&lt;/em&gt;, the &lt;em&gt;retrieved context&lt;/em&gt;, and the &lt;em&gt;generated answer&lt;/em&gt;, and whose three edges are the three orthogonal quality axes that must all pass for the RAG to be trustworthy. The edges are (a) &lt;strong&gt;context relevance&lt;/strong&gt; — is the retrieved context actually relevant to the query? — a low score points at the retriever; (b) &lt;strong&gt;groundedness&lt;/strong&gt; — is every claim in the generated answer supported by the retrieved context? — a low score points at hallucination; (c) &lt;strong&gt;answer relevance&lt;/strong&gt; — does the generated answer actually address the query, or does it wander off topic? — a low score points at the prompt or the model. Any one of the three can regress independently, so scoring only one hides regressions on the other two. The triad is why TruLens's default instrumentation ships three Feedback functions rather than one, and why senior interviewers probe the triangle rather than a single "hallucination score" — a candidate who names all three edges signals they understand RAG eval as a multi-dimensional discipline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I do LLM eval inside Snowflake?
&lt;/h3&gt;

&lt;p&gt;Yes — &lt;code&gt;snowflake cortex search&lt;/code&gt; plus &lt;code&gt;AI_COMPLETE&lt;/code&gt; plus the standard &lt;code&gt;TASK&lt;/code&gt; + &lt;code&gt;ALERT&lt;/code&gt; primitives give you a fully in-warehouse eval loop. The &lt;code&gt;CORTEX SEARCH SERVICE&lt;/code&gt; DDL is one command; it manages hybrid BM25 + vector indexing, chunking, embedding, and refresh via &lt;code&gt;TARGET_LAG&lt;/code&gt;. &lt;code&gt;SNOWFLAKE.CORTEX.SEARCH_PREVIEW('svc', query, k)&lt;/code&gt; returns top-k results as JSON, which &lt;code&gt;LATERAL FLATTEN&lt;/code&gt; unpacks into rows for eval joins against a labelled &lt;code&gt;eval_labels(query_id, expected_doc_id)&lt;/code&gt; table. Recall@k, MRR, and nDCG@k are pure aggregate SQL; groundedness is &lt;code&gt;AI_COMPLETE('claude-3-5-haiku', 'Rate 0-1 ...')::FLOAT&lt;/code&gt; wrapped in &lt;code&gt;AVG&lt;/code&gt;. A Snowflake &lt;code&gt;TASK&lt;/code&gt; schedules the whole thing on cron; a Snowflake &lt;code&gt;ALERT&lt;/code&gt; fires an email when the latest score drops below the trailing 30-day p10 by more than 5%. The decisive advantage is governance — data, index, model, judge, history, scheduler, and alerter all live inside one Snowflake account with one RBAC boundary. For banks, healthcare, and government workloads where data egress is a compliance loss, Cortex Search Ops is the default answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Groundedness vs answer relevance — how are they different?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Groundedness&lt;/strong&gt; asks "is every claim in the answer supported by the retrieved context?" — it's the anti-hallucination axis. An answer that invents a fact the retrieved documents do not contain scores low on groundedness &lt;em&gt;even if the fact happens to be correct in the world&lt;/em&gt;. Ragas calls this metric &lt;code&gt;faithfulness&lt;/code&gt;; TruLens calls it &lt;code&gt;groundedness&lt;/code&gt;; both split the answer into atomic claims, ask an LLM judge whether each claim is entailed by the retrieved context, and aggregate. &lt;strong&gt;Answer relevance&lt;/strong&gt; asks "does the answer address the user's question?" — it's the on-topic axis. An answer that is perfectly grounded (every claim is in the context) but doesn't address the question ("here is the entire policy document") scores low on answer relevance. Ragas measures &lt;code&gt;answer_relevancy&lt;/code&gt; by generating hypothetical questions the answer could address, then computing cosine similarity between those questions and the original query. The two metrics are orthogonal: a RAG chain can be perfectly grounded and off-topic, or on-topic and hallucinating; measuring only one hides the other's failure. Every RAG eval must score both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need reference answers to evaluate a RAG?
&lt;/h3&gt;

&lt;p&gt;No — this is why Ragas ships two &lt;strong&gt;reference-free&lt;/strong&gt; metrics: &lt;code&gt;faithfulness&lt;/code&gt; and &lt;code&gt;answer_relevancy&lt;/code&gt;. &lt;code&gt;faithfulness&lt;/code&gt; needs only the generated &lt;code&gt;answer&lt;/code&gt; and the retrieved &lt;code&gt;contexts&lt;/code&gt; — an LLM judge scores whether the answer's claims are entailed by the context, and no ground-truth answer is required. &lt;code&gt;answer_relevancy&lt;/code&gt; needs only the &lt;code&gt;question&lt;/code&gt; and the &lt;code&gt;answer&lt;/code&gt; — the metric generates hypothetical questions the answer could address and compares to the original query via cosine similarity. Both metrics let you bootstrap an eval gate on day one, before any human has written a single labelled answer. &lt;strong&gt;Reference-based&lt;/strong&gt; metrics like &lt;code&gt;context_precision&lt;/code&gt;, &lt;code&gt;context_recall&lt;/code&gt;, and &lt;code&gt;answer_correctness&lt;/code&gt; do need labelled ground truth (an &lt;code&gt;expected_doc_id&lt;/code&gt; per query, or a written &lt;code&gt;ground_truth&lt;/code&gt; answer), and those take human curation time — budget 5-10 minutes per row. The standard adoption path is: start with the two reference-free metrics + a small human-curated &lt;code&gt;(question, expected_doc_id)&lt;/code&gt; table for retrieval metrics; grow the ground-truth answers as the eval matures. TruLens's RAG triad and LangSmith's hosted &lt;code&gt;criteria&lt;/code&gt; evaluators also work reference-free, so the three tools all support this bootstrap-first pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt; for the eval-gate, incremental scoring, and RAG ingestion pipeline problems senior interviewers love.&lt;/li&gt;
&lt;li&gt;Rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;data-validation practice library →&lt;/a&gt; for the threshold-gating, batch scoring, and quality-metric patterns.&lt;/li&gt;
&lt;li&gt;Sharpen the systems axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt; for VPC-first eval systems, in-warehouse eval loops, and pairwise experiment design.&lt;/li&gt;
&lt;li&gt;Level up your Python instrumentation on the &lt;a href="https://pipecode.ai/explore/practice/topic/python" rel="noopener noreferrer"&gt;Python practice library →&lt;/a&gt; for decorator patterns, wrapper design, and async LLM-judge batching.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the four-axis measurement plan against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lock in LLM eval muscle memory&lt;/h3&gt;

&lt;p&gt;Docs explain metrics. PipeCode drills explain the decision — when to gate on faithfulness, when the RAG triad's context edge is the retriever's report card, when Ragas beats LangSmith for CI, when Cortex Search Ops wins on governance. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — axis-first practice tuned for the production trade-offs senior data and MLOps engineers actually face.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice ETL problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice design problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Text-to-SQL in Production: Evaluation Harnesses, Guardrails &amp; Semantic-Layer Grounding</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 31 Jul 2026 04:26:18 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/text-to-sql-in-production-evaluation-harnesses-guardrails-semantic-layer-grounding-4o3e</link>
      <guid>https://dev.to/gowthampotureddi/text-to-sql-in-production-evaluation-harnesses-guardrails-semantic-layer-grounding-4o3e</guid>
      <description>&lt;p&gt;Shipping &lt;strong&gt;&lt;code&gt;text-to-sql in production&lt;/code&gt;&lt;/strong&gt; is the single biggest gap between a great demo and a livable analytics product in 2026. Any senior data platform engineer can wire a GPT-class model to a Postgres or Snowflake connection and produce a "wow" reel — natural-language question in, table out, chart rendered. What actually breaks the moment a real user hits the endpoint is not the model quality: it is the &lt;em&gt;plumbing&lt;/em&gt; around the model — the evaluation harness that would have caught the join drift on prompt #47, the guardrails that would have blocked the twelve-way Cartesian join, the semantic-layer contract that would have prevented "revenue" from being computed three different ways on three different dashboards, and the closed feedback loop that would have promoted last week's escalated ticket into this week's gold prompt.&lt;/p&gt;

&lt;p&gt;This guide is the senior-platform-engineer walkthrough you wished existed the first time your VP of data asked "so when can we let customer success ask the warehouse in plain English?" It walks through the four failure classes that kill &lt;code&gt;text-to-sql in production&lt;/code&gt; deployments (schema hallucination, join drift, metric mis-definition, unsafe execution), the eval harness that scores every prompt against a gold set using exact-match, execution accuracy, and semantic-diff metrics, the four-lane guardrail stack (schema linking, &lt;code&gt;SQLGlot&lt;/code&gt; parse, warehouse dry-run cost, safety allow-list) every LLM-generated query must pass through, the semantic-layer grounding contract (dbt Semantic Layer, LookML, Cube) that pins metrics to a single definition, and the production loop that turns every escalated failure into next week's gold prompt. Each H2 pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why the design works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8qk0ir5rrfzy8asi0z5e.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8qk0ir5rrfzy8asi0z5e.jpeg" alt="PipeCode blog header for text-to-SQL in production — a bold white headline over a hero of a natural-language arrow flowing into a purple SQL medallion, ringed by four guardrail medallions (eval, guard, semantic, loop)." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt;, rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;SQL-generation practice library →&lt;/a&gt;, and sharpen the safety axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;data-validation practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why text-to-SQL fails in production — the four failure classes&lt;/li&gt;
&lt;li&gt;The eval harness — execution accuracy, exact-match, semantic diff&lt;/li&gt;
&lt;li&gt;Guardrails — schema linking, query validation, safety rails&lt;/li&gt;
&lt;li&gt;Semantic-layer grounding — dbt Semantic Layer, LookML, Cube&lt;/li&gt;
&lt;li&gt;The production loop — logs → eval → retrain → ship&lt;/li&gt;
&lt;li&gt;Cheat sheet — text-to-SQL production recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why text-to-SQL fails in production — the four failure classes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Four failure classes, four independent mitigations — and the demo does not touch any of them
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;&lt;code&gt;text-to-sql in production&lt;/code&gt; fails along four independent axes — the model hallucinates schema (invents tables or columns), drifts on joins (picks the wrong join key or the wrong join type), mis-defines metrics (computes revenue with the wrong filter or grain), and executes unsafely (runs a Cartesian join, scans a petabyte, drops a table) — and no single tool covers all four, so every serious deployment must layer independent mitigations on top of the model rather than trust the model to self-correct&lt;/strong&gt;. The demos everyone posts to LinkedIn use tiny toy schemas, memoised prompts, and no execution guard; the moment your real 400-table warehouse lands in the context window, all four failure classes fire at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four failure classes interviewers actually name.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema hallucination.&lt;/strong&gt; The model invents a &lt;code&gt;customers.lifetime_value&lt;/code&gt; column that does not exist, or references &lt;code&gt;orders.order_date&lt;/code&gt; when the real column is &lt;code&gt;order_created_at&lt;/code&gt;. Root cause: the prompt does not fit the schema, or the schema-linking layer is missing. Mitigation: retrieve only relevant tables into the prompt via embeddings, validate every referenced identifier post-generation with &lt;code&gt;SQLGlot&lt;/code&gt;, and reject anything that references an unknown identifier before it touches the warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join drift.&lt;/strong&gt; The model picks &lt;code&gt;LEFT JOIN&lt;/code&gt; where the semantic answer requires &lt;code&gt;INNER JOIN&lt;/code&gt;, or joins on &lt;code&gt;user_id = customer_id&lt;/code&gt; in a schema where the actual bridge is &lt;code&gt;users.id = customers.user_id&lt;/code&gt;. Root cause: the model has no join-graph awareness beyond the raw DDL. Mitigation: encode explicit &lt;code&gt;join_relationships&lt;/code&gt; in the schema documentation, use a semantic layer that pre-defines joins once, and grade every candidate SQL against the join graph before execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric mis-definition.&lt;/strong&gt; The model computes "monthly active users" as &lt;code&gt;COUNT(DISTINCT user_id)&lt;/code&gt; when the enterprise definition excludes bots and internal test accounts, or computes "revenue" without applying the refund adjustment. Root cause: metric definitions live in tribal knowledge, not in a queryable semantic layer. Mitigation: ground every question on a canonical metric via dbt Semantic Layer, LookML, or Cube — never let the model derive a metric from raw tables when a defined metric exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsafe execution.&lt;/strong&gt; The model emits &lt;code&gt;DELETE FROM orders&lt;/code&gt; because the user asked "how do I get rid of pending orders?", or produces a 12-way join that scans 500 TB and blows the daily warehouse budget in one query. Root cause: no execution guardrails. Mitigation: read-only connection role, mandatory &lt;code&gt;LIMIT&lt;/code&gt;, dry-run cost estimation before execution, timeout enforced by warehouse-side session settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The four axes senior engineers actually optimise for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correctness.&lt;/strong&gt; Does the returned result set match what a senior analyst would have written by hand? Measured by execution accuracy against a gold set — the primary metric for any production deployment. Anything below 80 percent execution accuracy on your private gold set means you are shipping vibes, not answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety.&lt;/strong&gt; Can any user, no matter how adversarial, cause data loss, schema mutation, or a runaway warehouse bill? Measured by 0 CVE-class incidents and by the fraction of generated SQL that survives every guardrail lane. Safety is a boolean: either every unsafe SQL is rejected, or the system fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; How many dollars per resolved question does the system incur — LLM tokens plus warehouse compute plus human review time? Measured in cents per accepted answer. A production text-to-SQL system that costs $5 per answer is a demo; a production system at $0.05 per answer is a product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency.&lt;/strong&gt; How long from user click to result-set render? Measured p50 / p95 wall clock. Above 30 seconds p95 and users churn; below 5 seconds p95 and the product feels magical. Latency is dominated by the LLM call plus the warehouse execution — the guardrail stack must add milliseconds, not seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The 2026 reality — the harness is the product.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model quality is not the bottleneck.&lt;/strong&gt; Frontier models (GPT-class, Claude-class, Gemini-class, Llama-class) all reach 70-85 percent execution accuracy on Spider and BIRD out of the box. The differentiation is entirely in the harness — the schema retrieval, the guardrails, the semantic layer, the eval loop. Two teams using the same model can be 20 percentage points apart on a private gold set because one team built the plumbing and the other did not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public benchmarks lie about your domain.&lt;/strong&gt; Spider is a cross-domain benchmark with tiny schemas (mostly under 20 tables). BIRD is closer to reality (larger schemas, dirty values) but still not your schema. Every serious deployment maintains a &lt;em&gt;private gold set&lt;/em&gt; of 200-2000 domain-specific &lt;code&gt;(question, gold_sql, expected_result)&lt;/code&gt; triples that dominates the public benchmarks for release gating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic layer became table stakes.&lt;/strong&gt; In 2024 semantic layers were "a nice-to-have for consistency"; in 2026 they are the &lt;em&gt;only&lt;/em&gt; way to defend metric correctness at scale. dbt Semantic Layer, Cube, and LookML all publish MCP or REST endpoints an LLM can query, and every mature deployment grounds on metrics rather than raw DDL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The eval harness runs in CI.&lt;/strong&gt; Every prompt template change, every model version bump, every schema evolution triggers a full offline harness run before deploy. The team that ships text-to-SQL without an eval CI is the team that ships a regression on Tuesday and gets paged on Wednesday.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you name &lt;strong&gt;all four failure classes&lt;/strong&gt; without prompting? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you say &lt;strong&gt;"execution accuracy is the primary metric"&lt;/strong&gt; rather than "we measure BLEU" or "we use LLM-as-judge"? — required answer.&lt;/li&gt;
&lt;li&gt;Do you push back on &lt;strong&gt;"just use GPT-4"&lt;/strong&gt; with the guardrail question — "who owns the read-only role, the dry-run budget, and the schema-link retriever?" — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name &lt;strong&gt;semantic layer grounding&lt;/strong&gt; as the answer to metric drift, not as "an alternative to RAG"? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you describe production text-to-SQL as &lt;strong&gt;"a harness around a model"&lt;/strong&gt; rather than as "an LLM app"? — required answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the four-class failure taxonomy on a real prompt
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful artifact for a text-to-SQL production interview is a worked-through failure taxonomy on a real prompt. Every senior conversation converges on this within the first ten minutes; having it in your head is what separates a fluent answer from a stumbling one. Walk through a canonical failure using a hypothetical retail warehouse where a user asks a plausible business question and the naive model burns on every axis.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Warehouse.&lt;/strong&gt; Snowflake &lt;code&gt;analytics&lt;/code&gt; database with 380 tables including &lt;code&gt;analytics.raw.orders&lt;/code&gt;, &lt;code&gt;analytics.raw.order_items&lt;/code&gt;, &lt;code&gt;analytics.raw.customers&lt;/code&gt;, &lt;code&gt;analytics.dim.customers&lt;/code&gt;, &lt;code&gt;analytics.fct.revenue_daily&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User question.&lt;/strong&gt; "What was our top-5 US customer revenue last quarter, excluding refunds?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Naive model output.&lt;/strong&gt; A CTE that joins &lt;code&gt;orders&lt;/code&gt; to &lt;code&gt;customers&lt;/code&gt; on &lt;code&gt;orders.cust_id = customers.customer_id&lt;/code&gt;, sums &lt;code&gt;orders.amount&lt;/code&gt;, groups by customer, orders and limits to 5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What breaks.&lt;/strong&gt; The column &lt;code&gt;cust_id&lt;/code&gt; does not exist (it is &lt;code&gt;customer_id&lt;/code&gt; on &lt;code&gt;orders&lt;/code&gt;), the refund exclusion is missing entirely, the "US" filter is missing entirely, and the &lt;code&gt;revenue&lt;/code&gt; computation ignores the &lt;code&gt;refund_amount&lt;/code&gt; column that the canonical &lt;code&gt;fct.revenue_daily&lt;/code&gt; metric applies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Enumerate the four failure classes triggered by this one prompt and specify the mitigation for each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure class&lt;/th&gt;
&lt;th&gt;Symptom in the naive SQL&lt;/th&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Schema hallucination&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;orders.cust_id&lt;/code&gt; does not exist&lt;/td&gt;
&lt;td&gt;schema linker + &lt;code&gt;SQLGlot&lt;/code&gt; identifier validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join drift&lt;/td&gt;
&lt;td&gt;joins on invented column, wrong grain&lt;/td&gt;
&lt;td&gt;join graph in schema doc + semantic-layer joins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric mis-definition&lt;/td&gt;
&lt;td&gt;uses &lt;code&gt;SUM(amount)&lt;/code&gt; not &lt;code&gt;revenue - refund&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;ground on &lt;code&gt;fct.revenue_daily&lt;/code&gt; metric&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsafe execution&lt;/td&gt;
&lt;td&gt;no &lt;code&gt;LIMIT&lt;/code&gt;, no cost dry-run&lt;/td&gt;
&lt;td&gt;mandatory &lt;code&gt;LIMIT&lt;/code&gt; + Snowflake &lt;code&gt;EXPLAIN&lt;/code&gt; cost gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Illustrative — classify a failure across the four axes
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FailureReport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;hallucinated_identifiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;join_drift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;metric_mismatches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;unsafe_ops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_ship_ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hallucinated_identifiers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join_drift&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric_mismatches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unsafe_ops&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;join_graph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric_catalog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;FailureReport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return a per-axis failure report for one candidate SQL.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;FailureReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;hallucinated_identifiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unknown_identifiers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_sql&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;join_drift&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;join_graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_sql&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;metric_mismatches&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;metric_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_sql&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;unsafe_ops&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing_limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LIMIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidate_sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The failure report is a &lt;em&gt;per-axis vector&lt;/em&gt;, not a single boolean. A candidate SQL that only fails on one axis is salvageable (rewrite the join, add the &lt;code&gt;LIMIT&lt;/code&gt;); a candidate that fails on three axes is a total loss and the model must resample.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;schema.unknown_identifiers(sql)&lt;/code&gt; walks the SQL AST with &lt;code&gt;SQLGlot&lt;/code&gt;, extracts every table and column reference, and looks each one up in the live warehouse catalog. Any reference not in the catalog is a hallucination. This single check catches the biggest failure class by volume.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;join_graph.violations(sql)&lt;/code&gt; compares every join predicate in the SQL against the canonical join graph maintained in schema docs (or the semantic layer). A join on &lt;code&gt;orders.cust_id = customers.customer_id&lt;/code&gt; is flagged because the canonical bridge in your schema is &lt;code&gt;orders.customer_id = customers.customer_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;metric_catalog.mismatches(sql)&lt;/code&gt; recognises when the user's intent maps to a defined metric (&lt;code&gt;fct.revenue_daily&lt;/code&gt;, &lt;code&gt;dim.customers.is_us&lt;/code&gt;) but the SQL derives the metric from raw tables. This is the metric-mis-definition axis; grounding on the semantic layer eliminates it at source.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unsafe_ops&lt;/code&gt; starts as a simple &lt;code&gt;LIMIT&lt;/code&gt; presence check but expands into the full guardrail lane (dry-run cost, safety allow-list, timeout). Even the shortest guardrail — "must contain &lt;code&gt;LIMIT&lt;/code&gt;" — catches the "twelve-way join with no bound" catastrophe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Naive SQL score&lt;/th&gt;
&lt;th&gt;Ship gate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hallucinated identifiers&lt;/td&gt;
&lt;td&gt;1 (&lt;code&gt;cust_id&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;must be 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join drift&lt;/td&gt;
&lt;td&gt;1 (wrong bridge)&lt;/td&gt;
&lt;td&gt;must be 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric mismatches&lt;/td&gt;
&lt;td&gt;1 (&lt;code&gt;SUM(amount)&lt;/code&gt; vs &lt;code&gt;fct.revenue_daily&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;must be 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsafe ops&lt;/td&gt;
&lt;td&gt;1 (no LIMIT)&lt;/td&gt;
&lt;td&gt;must be 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every candidate SQL emitted by the model is a &lt;em&gt;vector&lt;/em&gt; on the four failure axes, not a boolean. Ship only when the vector is all zeros. Every non-zero component points to a specific plumbing layer to fix — schema link, join graph, semantic layer, or execution guardrail.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — what interviewers actually probe in a senior text-to-SQL round
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The senior text-to-SQL interview has a predictable structure. The interviewer opens with an ambiguous question ("how would you build a natural-language interface to our warehouse?"), then progressively narrows to test whether you know the four axes. The candidates who name the harness in sentence one score highest; the candidates who describe "an LLM app" score lowest. Walk through the interview grading rubric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous opener.&lt;/strong&gt; "How would you build a text-to-SQL feature over our warehouse?" — invites you to name the harness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 1.&lt;/strong&gt; "How would you measure quality?" — probes eval-harness axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 2.&lt;/strong&gt; "How do you keep a user from dropping a table?" — probes safety axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 3.&lt;/strong&gt; "How do you keep 'revenue' from being computed three different ways?" — probes semantic-layer axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 4.&lt;/strong&gt; "How would you improve the system after ship?" — probes the production loop axis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draft a 5-minute senior text-to-SQL answer that covers all four axes without waiting to be asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interview signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;Senior answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framing&lt;/td&gt;
&lt;td&gt;"we'd wire GPT-4 to Snowflake"&lt;/td&gt;
&lt;td&gt;"the model is a component; the harness is the product"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eval&lt;/td&gt;
&lt;td&gt;"we'd look at some outputs"&lt;/td&gt;
&lt;td&gt;"execution accuracy on a 500-prompt private gold set, run in CI on every prompt change"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safety&lt;/td&gt;
&lt;td&gt;"the read-only user handles it"&lt;/td&gt;
&lt;td&gt;"four-lane guardrail: schema link, &lt;code&gt;SQLGlot&lt;/code&gt; parse, dry-run cost, safety allow-list"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic&lt;/td&gt;
&lt;td&gt;"we'd add more docs"&lt;/td&gt;
&lt;td&gt;"ground on dbt Semantic Layer metrics; never let the model derive &lt;code&gt;revenue&lt;/code&gt; from raw tables"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loop&lt;/td&gt;
&lt;td&gt;"we'd retrain the model"&lt;/td&gt;
&lt;td&gt;"capture logs, human-label failures, promote to gold, canary ship"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Senior text-to-SQL answer template (5 minutes)
==============================================

Minute 1 — name the harness up front
  "I'd frame this as a harness around a frontier model, not an LLM
   app. The model reaches 75% execution accuracy out of the box;
   the harness gets me to 92%."

Minute 2 — eval harness
  "The primary metric is execution accuracy against a private gold
   set of 500 (question, gold_sql, expected_result) triples that
   dominates Spider and BIRD for release gating. Exact-match is a
   lower bound; semantic-diff on the result set is the truth."

Minute 3 — guardrails
  "Every candidate SQL passes four lanes before it touches the
   warehouse: schema-link retrieval to prune the prompt, SQLGlot
   parse + identifier validation, Snowflake EXPLAIN cost dry-run,
   and a safety allow-list that rejects any DDL or DML. The
   connection role is read-only with a bytes-scanned quota."

Minute 4 — semantic-layer grounding
  "Metrics like revenue, MAU, churn live in dbt Semantic Layer.
   The retriever exposes metrics as first-class candidates; the
   prompt template says 'prefer defined metrics over raw tables.'
   The result: one revenue definition, zero drift across dashboards."

Minute 5 — production loop + failure semantics
  "Every request logs (prompt, retrieved_context, generated_sql,
   guardrail_verdict, executed_result, user_feedback). Weekly
   triage promotes failures to gold prompts. Prompt template
   changes and model bumps run through the eval harness in CI
   with a 2-point regression gate before deploy."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Minute 1 is the crucial framing. Naming the harness immediately — "harness around a frontier model" — signals you are a decision-maker, not a task-runner. Weak candidates dive into tools ("we'd use LangChain and…") before naming the architecture.&lt;/li&gt;
&lt;li&gt;Minute 2 addresses the eval axis before the interviewer asks. This preempts the common trap where you commit to shipping and then admit you have no measurement. Naming execution accuracy explicitly (not BLEU, not "LLM-as-judge alone") is senior signal.&lt;/li&gt;
&lt;li&gt;Minute 3 is the safety axis. Every guardrail lane must be named; the read-only role alone is not enough because a runaway &lt;code&gt;SELECT&lt;/code&gt; can still bankrupt the warehouse. The four-lane answer is the memorable framework.&lt;/li&gt;
&lt;li&gt;Minute 4 is the semantic-layer axis. The &lt;code&gt;revenue&lt;/code&gt; example is nearly universal: every data team has a metric-drift war story, and grounding on dbt Semantic Layer is the standard fix in 2026.&lt;/li&gt;
&lt;li&gt;Minute 5 covers the loop and failure semantics — the reliability axis. Naming CI-gated regression testing on every prompt change is what distinguishes "we shipped it" from "we operate it."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading criterion&lt;/th&gt;
&lt;th&gt;Weak score&lt;/th&gt;
&lt;th&gt;Senior score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frames harness first&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names execution accuracy&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names all four guardrail lanes&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names semantic-layer grounding&lt;/td&gt;
&lt;td&gt;occasional&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names CI eval gate&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The senior text-to-SQL answer is a 5-minute monologue that covers all four axes without waiting for the follow-ups. Rehearse it once; deploy it every time. The harness comes first; the model choice comes last.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the "will this feature survive" decision tree
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Given a new text-to-SQL feature request, the senior architect runs a 4-question decision tree in their head. Codifying the tree makes the interview answer reproducible: any interviewer can hand you a scenario and you can walk the tree out loud. Walk through the tree with three canonical scenarios: internal analyst assistant, customer-facing self-serve, and executive dashboard "ask a question" bar.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Q1.&lt;/strong&gt; Is the user population trusted (internal analysts) or untrusted (external customers, executives)? Untrusted → guardrails must be paranoid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q2.&lt;/strong&gt; Is the warehouse cost centralised (one team pays) or fanned-out (per-tenant billing)? Fanned-out → per-query cost cap mandatory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q3.&lt;/strong&gt; Do metrics have canonical definitions (semantic layer exists) or tribal ones (docs in Notion)? Tribal → semantic-layer investment must precede feature ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q4.&lt;/strong&gt; Is there a private gold set? No → build one before shipping; do not use Spider or BIRD as ship gates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Walk the decision tree for the three scenarios and record the go / no-go verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Q1 (trust)&lt;/th&gt;
&lt;th&gt;Q2 (cost)&lt;/th&gt;
&lt;th&gt;Q3 (semantic layer)&lt;/th&gt;
&lt;th&gt;Q4 (gold set)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Internal analyst assistant&lt;/td&gt;
&lt;td&gt;trusted&lt;/td&gt;
&lt;td&gt;centralised&lt;/td&gt;
&lt;td&gt;tribal&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer-facing self-serve&lt;/td&gt;
&lt;td&gt;untrusted&lt;/td&gt;
&lt;td&gt;fanned-out&lt;/td&gt;
&lt;td&gt;canonical&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executive "ask a question" bar&lt;/td&gt;
&lt;td&gt;trusted&lt;/td&gt;
&lt;td&gt;centralised&lt;/td&gt;
&lt;td&gt;canonical&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Decision-tree helper (illustrative)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShipVerdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ship_now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;blocker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;prerequisites&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;can_we_ship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;trusted_users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;centralised_cost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;has_semantic_layer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;has_gold_set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ShipVerdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;prereqs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;has_gold_set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prereqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build 200-prompt private gold set&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;has_semantic_layer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prereqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stand up dbt Semantic Layer for top-20 metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;centralised_cost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prereqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per-query bytes-scanned cap + tenant budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;trusted_users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prereqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paranoid guardrails: allow-list SELECT only, mask PII columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;prereqs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ShipVerdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ship_now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blocker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prerequisites&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ShipVerdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ship_now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;blocker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prerequisites not met&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;prerequisites&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prereqs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# Walk the three scenarios
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;can_we_ship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;can_we_ship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;can_we_ship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scenario 1 — internal analyst assistant. Trusted users, centralised cost, no semantic layer, no gold set. The verdict is &lt;em&gt;not shippable yet&lt;/em&gt;: build the gold set (2 weeks), stand up dbt Semantic Layer for the top 20 metrics (4 weeks), then ship. Skipping either turns "helpful assistant" into "confidently wrong assistant."&lt;/li&gt;
&lt;li&gt;Scenario 2 — customer-facing self-serve. Untrusted, fanned-out cost, canonical metrics, no gold set. Verdict is &lt;em&gt;not shippable yet&lt;/em&gt;: build gold set (essential for customer-facing), add per-tenant cost caps (essential; one bad query cannot bankrupt a tenant), then layer paranoid guardrails. Semantic layer is already in place.&lt;/li&gt;
&lt;li&gt;Scenario 3 — executive dashboard "ask a question" bar. Trusted users, centralised cost, canonical metrics, gold set exists. Verdict is &lt;em&gt;shippable now&lt;/em&gt; — all four prerequisites are met. Ship a canary at 10 percent traffic, watch the eval-harness numbers weekly.&lt;/li&gt;
&lt;li&gt;The parallel branches (guardrail paranoia, cost cap, semantic layer, gold set) are independent — you can ship a system with only some of them, but the failure surface grows with each missing prerequisite. Making the prerequisites explicit turns "should we ship?" into a checklist question.&lt;/li&gt;
&lt;li&gt;If none of Q1-Q4 pass, the correct answer is "we are not ready; here is the four-week roadmap to become ready." Refuse to ship text-to-SQL without the prerequisites; the failures are subtle and expensive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Ship now?&lt;/th&gt;
&lt;th&gt;Prerequisites&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Internal analyst assistant&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;gold set + semantic layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer-facing self-serve&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;gold set + cost cap + paranoid guardrails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executive "ask a question" bar&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;already met&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The four-question decision tree is a whiteboard-friendly answer. Practice walking it end-to-end so an interviewer can hand you any scenario and get a go / no-go verdict in under 60 seconds — plus the concrete prerequisite list to unblock the no.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on text-to-SQL production-readiness
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You inherit a Slack bot that lets internal analysts ask questions in English and get warehouse results back. It works in demos but produces wrong answers 30 percent of the time and last month it cost $18,000 in Snowflake compute for one botched query. Walk me through the production-readiness plan you would put in front of leadership, the four workstreams you would fund, and the KPIs you would commit to."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a four-workstream production hardening plan with harness-first, guardrails-second, semantic-layer-third, loop-fourth
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# production_readiness_plan.py — the plan the platform lead ships to leadership
&lt;/span&gt;
&lt;span class="n"&gt;WORKSTREAMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval-harness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-platform&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weeks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deliver&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500-prompt private gold set&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exact-match + execution-accuracy + semantic-diff scorer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CI job that runs on every prompt template PR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kpi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execution accuracy &amp;gt;= 85% on gold set, tracked weekly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guardrails&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-platform + security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weeks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deliver&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema-link retriever (top-20 tables per prompt)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SQLGlot parse + identifier validator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Snowflake EXPLAIN cost dry-run with 5 GB scanned cap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read-only role with allow-list of SELECT + WITH only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kpi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0 unsafe SQL executed against warehouse; p99 cost &amp;lt;= $0.50 per query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic-layer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analytics-eng&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weeks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deliver&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top-20 metrics defined in dbt Semantic Layer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metric retriever exposed as first-class candidate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt template updated to prefer metrics over raw tables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kpi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;= 60% of user questions grounded on a defined metric&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production-loop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-platform&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weeks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deliver&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request log (prompt, retrieved, sql, verdict, result, feedback)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weekly triage rotation promotes failures to gold prompts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;canary release: new prompt template goes to 10% traffic for 1 week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kpi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gold set grows by 20 prompts per week; regressions caught pre-deploy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;WORKSTREAMS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;weeks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; weeks, owner=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deliver&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  KPI: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kpi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Snowflake — set up the read-only role + bytes-scanned cap for the bot user&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;textsql_bot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;textsql_wh&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;textsql_bot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;    &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;textsql_bot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;  &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;textsql_bot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fct&lt;/span&gt;  &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;textsql_bot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;textsql_bot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fct&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;textsql_bot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Per-session quota: kill queries that scan more than 5 GB&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;textsql_bot_svc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
    &lt;span class="n"&gt;STATEMENT_TIMEOUT_IN_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;STATEMENT_QUEUED_TIMEOUT_IN_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;QUERY_TAG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'textsql-bot'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Resource monitor: hard-stop at $500/day on the bot warehouse&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;RESOURCE&lt;/span&gt; &lt;span class="n"&gt;MONITOR&lt;/span&gt; &lt;span class="n"&gt;textsql_daily&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;CREDIT_QUOTA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="n"&gt;FREQUENCY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DAILY&lt;/span&gt;
    &lt;span class="n"&gt;START_TIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;IMMEDIATELY&lt;/span&gt;
    &lt;span class="n"&gt;TRIGGERS&lt;/span&gt;
        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="n"&gt;PERCENT&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTIFY&lt;/span&gt;
        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="n"&gt;PERCENT&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="n"&gt;SUSPEND&lt;/span&gt;
        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="mi"&gt;110&lt;/span&gt; &lt;span class="n"&gt;PERCENT&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="n"&gt;SUSPEND_IMMEDIATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;textsql_wh&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;RESOURCE_MONITOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textsql_daily&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Before (broken Slack bot)&lt;/th&gt;
&lt;th&gt;After (production-hardened)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Execution accuracy&lt;/td&gt;
&lt;td&gt;70% (self-reported)&lt;/td&gt;
&lt;td&gt;85% (measured on 500-prompt gold set)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsafe SQL executed&lt;/td&gt;
&lt;td&gt;occasional (18k$ incident)&lt;/td&gt;
&lt;td&gt;0 (four-lane guardrail)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric drift&lt;/td&gt;
&lt;td&gt;rampant&lt;/td&gt;
&lt;td&gt;contained (semantic-layer grounding)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost visibility&lt;/td&gt;
&lt;td&gt;reactive (postmortem)&lt;/td&gt;
&lt;td&gt;proactive (resource monitor + dry-run cap)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedback loop&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;logs → weekly triage → gold promotion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy safety&lt;/td&gt;
&lt;td&gt;ad-hoc&lt;/td&gt;
&lt;td&gt;CI eval gate; 2-point regression blocker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ship cadence&lt;/td&gt;
&lt;td&gt;broken&lt;/td&gt;
&lt;td&gt;weekly prompt tunes; monthly model bumps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the four workstreams land, the Slack bot's error rate drops from 30 percent to under 15 percent (execution-accuracy 85 percent), and no single query can scan more than 5 GB or cost more than $0.50. The eval harness runs on every prompt-template PR; a regression larger than 2 points blocks the deploy. Weekly triage promotes real user failures into the gold set, which keeps the ship gate tight to what users actually ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wrong answers&lt;/td&gt;
&lt;td&gt;~30%&lt;/td&gt;
&lt;td&gt;~15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost blowout risk&lt;/td&gt;
&lt;td&gt;one incident/quarter&lt;/td&gt;
&lt;td&gt;0 (resource monitor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric drift complaints&lt;/td&gt;
&lt;td&gt;weekly&lt;/td&gt;
&lt;td&gt;eliminated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy-time regressions&lt;/td&gt;
&lt;td&gt;monthly&lt;/td&gt;
&lt;td&gt;caught pre-deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to root-cause a bad answer&lt;/td&gt;
&lt;td&gt;days&lt;/td&gt;
&lt;td&gt;minutes (log + eval)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team confidence to expand scope&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Harness-first framing&lt;/strong&gt;&lt;/strong&gt; — the model is one component in a system of many. Investing in eval, guardrails, semantic layer, and loop &lt;em&gt;before&lt;/em&gt; investing in a better model buys 15-20 execution-accuracy points that no model bump alone would deliver.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Execution accuracy on a private gold set&lt;/strong&gt;&lt;/strong&gt; — the only measurement that predicts user-perceived quality. Public benchmarks (Spider, BIRD) are a floor, not a ceiling; the private gold set is what your team actually ships against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Four-lane guardrails&lt;/strong&gt;&lt;/strong&gt; — every LLM-generated SQL passes schema link, parse validation, dry-run cost, and safety allow-list. Missing any lane leaves the failure class it protects wide open. The lanes are independent, so failures on one lane do not propagate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Semantic-layer grounding&lt;/strong&gt;&lt;/strong&gt; — metrics defined once, referenced everywhere. Removing the model's freedom to redefine &lt;code&gt;revenue&lt;/code&gt; collapses an entire failure class into "the metric catalog is the source of truth."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Production loop&lt;/strong&gt;&lt;/strong&gt; — capture, label, evaluate, ship. Every failure becomes tomorrow's gold prompt; every model or prompt change is gated by the harness before deploy. The system compounds over weeks; a static prompt-and-model system decays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — 15 engineer-weeks across four workstreams; ongoing operating cost of one engineer at 30 percent time for triage plus warehouse compute for the CI eval. The eliminated cost is the $18k Snowflake incident, the metric-drift war rooms, and the confidence loss with leadership when the bot is wrong. Net O(1) per query with bounded worst case, versus O(bug) per bad answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems on production text-to-SQL queries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL Generation&lt;/span&gt;
&lt;span&gt;Topic — sql-generation&lt;/span&gt;
&lt;strong&gt;SQL-generation and NL2SQL problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. The eval harness — execution accuracy, exact-match, semantic diff
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The three metrics — exact-match is a lower bound, semantic-diff is convenient, execution accuracy is the truth
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the &lt;code&gt;text-to-sql evaluation&lt;/code&gt; harness scores every candidate SQL against a private gold set using three complementary metrics — exact-match (does the SQL string match the gold string after canonicalisation?), execution accuracy (does the result set match after running both against the same data snapshot?), and semantic-diff (does the query plan or output shape match even when the SQL text differs?) — and every serious deployment gates release on execution accuracy because it is the only metric that measures what the user actually experiences&lt;/strong&gt;. Exact-match and semantic-diff are useful diagnostics; execution accuracy is the ship gate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gia9ezvhv6j5ivizwxf.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gia9ezvhv6j5ivizwxf.jpeg" alt="Iconographic eval-harness diagram — a 3x3 grid of predicted vs gold SQL cards on the left, a semantic-diff card in the middle, and a large execution-accuracy dial on the right showing 82 percent." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The three metrics — what each measures and where each fails.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact-match.&lt;/strong&gt; Canonicalise the candidate SQL (lowercase, whitespace-normalise, alias-normalise) and compare to the canonicalised gold SQL. Cheap, deterministic, no warehouse round-trip. Fails when two syntactically different queries produce the same result — &lt;code&gt;SELECT a, b FROM t&lt;/code&gt; vs &lt;code&gt;SELECT b, a FROM t&lt;/code&gt; are exact-match different but semantically identical, and a &lt;code&gt;JOIN&lt;/code&gt; reordered by the model is exact-match different but plan-identical. Exact-match is a &lt;em&gt;lower bound&lt;/em&gt;: if it passes, the candidate is correct; if it fails, the candidate might still be correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution accuracy.&lt;/strong&gt; Run the candidate SQL and the gold SQL against a frozen snapshot of the warehouse; compare the result sets after canonicalisation (sort rows, sort columns, cast types). The single most-cited metric in every text-to-SQL paper and every production deployment. The failure mode is expense: every eval-set run is N warehouse queries against real data. Snapshot the data into a dev warehouse to bound cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic-diff.&lt;/strong&gt; Compare the query plans (using &lt;code&gt;EXPLAIN&lt;/code&gt;) or the projected column set + join set. Catches "the SQL text differs but the plan is the same" cases that exact-match misses, without the warehouse cost of execution accuracy. Useful in the middle of a fast dev loop; not sufficient alone for release gating because two different plans can still produce identical result sets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The gold-set contract — the artifact your release depends on.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What a gold row is.&lt;/strong&gt; A tuple &lt;code&gt;(nl_question, gold_sql, expected_result, tags)&lt;/code&gt; where &lt;code&gt;nl_question&lt;/code&gt; is the user's natural-language input, &lt;code&gt;gold_sql&lt;/code&gt; is the reference answer written by a senior analyst, &lt;code&gt;expected_result&lt;/code&gt; is either the frozen result set or a hash thereof, and &lt;code&gt;tags&lt;/code&gt; classify the prompt (e.g. &lt;code&gt;aggregation&lt;/code&gt;, &lt;code&gt;window&lt;/code&gt;, &lt;code&gt;join_3way&lt;/code&gt;, &lt;code&gt;metric:revenue&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to build it.&lt;/strong&gt; Start with 200 prompts covering your top-10 query patterns across your top-10 tables. Every production incident becomes a new gold row. Every escalated user feedback becomes a new gold row. Target 500 prompts within the first quarter; 2000 within the first year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where to store it.&lt;/strong&gt; Version-controlled repo (git), one JSON or JSONL row per prompt. The gold set is code — reviewed, PR-approved, blamed when regressions ship. Storing it in Notion or a Google Doc guarantees it decays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to protect it.&lt;/strong&gt; Never let the gold set leak into the training or fine-tuning data. Rotate 20 percent quarterly to prevent overfit. Keep a separate "adversarial" partition that never enters any prompt template.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Spider, BIRD, WikiSQL — the public benchmarks and where they fail your domain.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spider.&lt;/strong&gt; 200 databases, 10k+ prompts, cross-domain, but tiny schemas (mostly under 20 tables) and clean data. Excellent for model-development sanity; useless as a production gate for your 400-table Snowflake warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BIRD.&lt;/strong&gt; Closer to reality — larger schemas (up to 100 tables), dirty values, external-knowledge questions. The 2024 model gap between "trained on BIRD" and "actually works on your warehouse" is roughly 15 percentage points of execution accuracy. Better than Spider; still not enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WikiSQL.&lt;/strong&gt; Older, single-table, dominated by simple &lt;code&gt;SELECT ... WHERE&lt;/code&gt;. Historical value; skip for production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pattern.&lt;/strong&gt; Use Spider and BIRD as &lt;em&gt;sanity&lt;/em&gt; checks during model selection; use your private gold set as the &lt;em&gt;release&lt;/em&gt; gate. Anyone who ships on Spider score alone will be surprised in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on eval harness.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What is your primary metric?" — required answer: execution accuracy on a private gold set.&lt;/li&gt;
&lt;li&gt;"How is that different from exact-match?" — exact-match ignores semantically equivalent rewrites; execution accuracy catches them.&lt;/li&gt;
&lt;li&gt;"How do you run it without breaking your warehouse cost?" — dev warehouse snapshot, per-query cost cap, sampled data.&lt;/li&gt;
&lt;li&gt;"How do you keep the gold set fresh?" — every escalated failure becomes a gold row; 20 percent rotation quarterly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — build an eval harness from scratch
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical text-to-SQL harness is a Python module with three functions — canonicalise, execute, score — and a runner that iterates over a JSONL gold file, invokes the candidate model, scores each row, and emits a scorecard. Build the whole thing from scratch and run it against a 5-prompt toy gold set.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gold file.&lt;/strong&gt; &lt;code&gt;gold_set.jsonl&lt;/code&gt; — one row per prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canonicalisation.&lt;/strong&gt; Sort rows, sort columns, cast numeric types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runner.&lt;/strong&gt; For each row, call the model, execute, compare, record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scorecard.&lt;/strong&gt; Aggregate per-tag; per-metric; per-difficulty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement the eval harness end-to-end and produce a scorecard for a 5-prompt toy gold set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gold_set.jsonl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5 &lt;code&gt;(question, gold_sql, expected_result)&lt;/code&gt; triples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;harness.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;canonicalise, execute, score, run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scorecard.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per-tag execution accuracy summary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# harness.py — build the eval harness from scratch
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;   &lt;span class="c1"&gt;# for AST-based canonicalisation
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sf&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GoldRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;gold_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;expected_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ScoreRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;predicted_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;exact_match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;exec_accuracy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Alias-normalise, whitespace-normalise, lowercase identifiers.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dialect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pretty&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Sort rows, sort columns, hash for cheap equality.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;normalised&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalised&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALTER SESSION SET STATEMENT_TIMEOUT_IN_SECONDS = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;timeout_s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GoldRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ScoreRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;predicted_sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model_fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;t_gen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;

    &lt;span class="n"&gt;exact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predicted_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gold_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;exec_ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predicted_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;got_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;exec_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;got_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_hash&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ScoreRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;question&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;predicted_sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;predicted_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;exact_match&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;exec_accuracy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exec_ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;latency_ms&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t_gen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;error&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ScoreRow&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme.snowflakecomputing.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval_wh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analytics_dev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ScoreRow&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GoldRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;score_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_fn&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;print_scorecard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ScoreRow&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;exact_n&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exact_match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;exec_n&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exec_accuracy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;err_n&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; exact=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exact_n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; exec=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exec_n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; errors=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;err_n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;per_tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ScoreRow&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;per_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exec_accuracy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  tag=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; n=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; exec_acc=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# gold_set.jsonl — 5 toy prompts
{"question": "top 5 US customers by revenue last quarter",
 "gold_sql": "SELECT customer_id, revenue FROM fct.revenue_by_customer WHERE country='US' AND quarter='2026Q2' ORDER BY revenue DESC LIMIT 5",
 "expected_hash": "d1a3...",
 "tags": ["aggregation", "metric:revenue", "difficulty:medium"]}
{"question": "monthly active users trend for the last 12 weeks",
 "gold_sql": "SELECT week, wau FROM fct.wau_weekly WHERE week &amp;gt;= dateadd(week,-12,current_date) ORDER BY week",
 "expected_hash": "f2b8...",
 "tags": ["window", "metric:wau", "difficulty:easy"]}
{"question": "which product SKUs are pending shipment for more than 3 days",
 "gold_sql": "SELECT sku, count(*) AS pending_orders FROM fct.orders WHERE status='pending' AND ordered_at &amp;lt; dateadd(day,-3,current_date) GROUP BY sku HAVING count(*) &amp;gt; 0 ORDER BY pending_orders DESC LIMIT 100",
 "expected_hash": "9a1e...",
 "tags": ["aggregation", "join_none", "difficulty:medium"]}
{"question": "top 10 pages by 7-day rolling unique visitors",
 "gold_sql": "SELECT page, avg_visits FROM (SELECT page, avg(visits) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS avg_visits FROM fct.page_visits_daily) ORDER BY avg_visits DESC LIMIT 10",
 "expected_hash": "3f7c...",
 "tags": ["window", "difficulty:hard"]}
{"question": "sessions per user in the last 7 days for the enterprise plan",
 "gold_sql": "SELECT user_id, count(*) AS sessions FROM fct.sessions s JOIN dim.customers c ON c.user_id=s.user_id WHERE c.plan='enterprise' AND s.started_at &amp;gt;= dateadd(day,-7,current_date) GROUP BY user_id ORDER BY sessions DESC LIMIT 100",
 "expected_hash": "b6e0...",
 "tags": ["join_2way", "aggregation", "difficulty:medium"]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;canonicalise_sql&lt;/code&gt; uses &lt;code&gt;SQLGlot&lt;/code&gt; to parse the SQL into an AST, then serialises it back with &lt;code&gt;normalize=True&lt;/code&gt; and lowercasing. This turns &lt;code&gt;SELECT A, B FROM T&lt;/code&gt; and &lt;code&gt;select a,b from t&lt;/code&gt; into the same canonical form. It does &lt;em&gt;not&lt;/em&gt; handle join reordering or column reordering — those are exact-match limitations that execution accuracy will catch.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;canonicalise_result&lt;/code&gt; sorts the rows (top-level) and the columns (per row, stringified) then SHA-256 hashes. Sorting is essential because SQL result-set ordering is not guaranteed unless &lt;code&gt;ORDER BY&lt;/code&gt; is explicit; hashing is a cheap way to compare huge result sets without serialising into memory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;execute_sql&lt;/code&gt; sets a per-session &lt;code&gt;STATEMENT_TIMEOUT_IN_SECONDS&lt;/code&gt; before executing. The timeout is the first line of defense — a candidate SQL that would run for 30 minutes gets killed at 30 seconds, so one bad candidate cannot burn the eval budget.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;score_row&lt;/code&gt; calls the model once, measures generation latency, canonicalises both SQL strings for exact-match, then executes the candidate and hashes the result. Errors are captured and stored — they count as execution-accuracy failures but are reported separately so the harness distinguishes "wrong answer" from "syntax error."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;print_scorecard&lt;/code&gt; aggregates by tag so the harness surfaces where the model is weak. A model that hits 90 percent on &lt;code&gt;aggregation&lt;/code&gt; and 50 percent on &lt;code&gt;window&lt;/code&gt; tells you exactly where to invest in schema linking and prompt-template tuning.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total prompts&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;toy gold set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact-match&lt;/td&gt;
&lt;td&gt;2/5&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution accuracy&lt;/td&gt;
&lt;td&gt;4/5&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Errors&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no syntax failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aggregation&lt;/td&gt;
&lt;td&gt;3 prompts, 100% exec_acc&lt;/td&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;window&lt;/td&gt;
&lt;td&gt;2 prompts, 50% exec_acc&lt;/td&gt;
&lt;td&gt;needs prompt tuning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any text-to-SQL harness, exact-match is a lower bound on quality (never a ship gate); execution accuracy is the ship gate; tag every gold prompt so the scorecard surfaces per-pattern weaknesses. Snapshot the warehouse into a dev instance so each harness run costs pennies, not thousands.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — semantic diff via query plan hashing
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Execution accuracy is expensive when your gold set grows to 2000 prompts and the warehouse is real. Semantic-diff via query-plan hashing gives you 80 percent of the signal at 5 percent of the cost. Compute &lt;code&gt;EXPLAIN&lt;/code&gt; output for both candidate and gold, canonicalise, hash — if the plans match, the queries are semantically equivalent (with high confidence). Walk through the design.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The mechanism.&lt;/strong&gt; &lt;code&gt;EXPLAIN&lt;/code&gt; returns the query plan; canonicalise it (strip volatile fields like plan IDs, cost estimates that change with statistics); hash for cheap comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The confidence.&lt;/strong&gt; Plan-hash equality implies result-set equality for pure &lt;code&gt;SELECT&lt;/code&gt; queries against a stable schema (subject to non-determinism from &lt;code&gt;LIMIT&lt;/code&gt; without &lt;code&gt;ORDER BY&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When it fails.&lt;/strong&gt; Two different plans can produce the same result (join reorder, index selection); plan hashing is a &lt;em&gt;sufficient&lt;/em&gt; signal for equivalence but not &lt;em&gt;necessary&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement a semantic-diff scorer via query-plan hashing and integrate it into the harness as a cheap pre-filter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;EXPLAIN&lt;/code&gt; on candidate&lt;/td&gt;
&lt;td&gt;get candidate plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;EXPLAIN&lt;/code&gt; on gold&lt;/td&gt;
&lt;td&gt;get gold plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;plan canonicaliser&lt;/td&gt;
&lt;td&gt;strip volatile fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;plan hasher&lt;/td&gt;
&lt;td&gt;SHA-256 comparison&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;integration&lt;/td&gt;
&lt;td&gt;run pre-filter before execution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# semantic_diff.py — plan-hash pre-filter
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Strip volatile fields: cost estimates, plan IDs, row counts.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Snowflake EXPLAIN emits fields like [cost=1234.5], [rows=987], [id=42]
&lt;/span&gt;    &lt;span class="n"&gt;stripped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\[(cost|rows|id|est)=[^\]]+\]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Normalise whitespace
&lt;/span&gt;    &lt;span class="n"&gt;stripped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stripped&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stripped&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;plan_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXPLAIN USING TEXT &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;canonicalise_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;semantic_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidate_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;plan_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidate_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;plan_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;


&lt;span class="c1"&gt;# Integration — cheap pre-filter in the harness
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_row_with_prefilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ScoreRow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;predicted_sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model_fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Cheap check: exact-match after canonicalisation
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predicted_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gold_sql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ScoreRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exact_match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_accuracy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;

    &lt;span class="c1"&gt;# Cheaper than execution: plan-hash equivalence
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;semantic_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predicted_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gold_sql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ScoreRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exact_match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_accuracy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;

    &lt;span class="c1"&gt;# Only run execution when the cheap checks disagree
&lt;/span&gt;    &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predicted_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;got_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ScoreRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;exact_match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;exec_accuracy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;got_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_hash&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;canonicalise_plan&lt;/code&gt; strips the volatile fields Snowflake includes in &lt;code&gt;EXPLAIN&lt;/code&gt; output — cost estimates change with table statistics, plan IDs are per-invocation, row counts drift with data. Stripping them lets two invocations of the same plan hash identically.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;plan_hash&lt;/code&gt; runs &lt;code&gt;EXPLAIN USING TEXT&lt;/code&gt;, gathers the plan lines, canonicalises, and SHA-256 hashes. &lt;code&gt;EXPLAIN&lt;/code&gt; is cheap on Snowflake — it costs a few compilation microseconds and no warehouse credits.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;semantic_equal&lt;/code&gt; is the equivalence test. Same hash means (with high probability) same plan means same result set. Different hash means "we don't know" — fall through to execution.&lt;/li&gt;
&lt;li&gt;The integrated harness scores in cascading tiers: cheapest first (exact-match), then medium (plan-hash), then expensive (execution). Most candidates hit an early tier; only the ambiguous ones burn execution budget.&lt;/li&gt;
&lt;li&gt;The plan-hash tier reduces the eval-run cost dramatically. On a 2000-prompt gold set where 60 percent of candidates are plan-equivalent to gold, plan-hash pre-filtering drops execution count by 60 percent — thousands of dollars per full harness run.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Cost per prompt&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;th&gt;Ship-gate role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact-match&lt;/td&gt;
&lt;td&gt;~microseconds&lt;/td&gt;
&lt;td&gt;20-30% of prompts&lt;/td&gt;
&lt;td&gt;fast pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan-hash&lt;/td&gt;
&lt;td&gt;~milliseconds&lt;/td&gt;
&lt;td&gt;30-40% of additional prompts&lt;/td&gt;
&lt;td&gt;cheap semantic pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution accuracy&lt;/td&gt;
&lt;td&gt;~seconds + $&lt;/td&gt;
&lt;td&gt;30-40% remainder&lt;/td&gt;
&lt;td&gt;ground truth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Combined harness&lt;/td&gt;
&lt;td&gt;~cents per prompt at scale&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;release gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Layer the three metrics in a cost-ascending cascade: exact-match first (microseconds), plan-hash second (milliseconds), execution accuracy last (seconds and dollars). Route each prompt through the cheapest tier that resolves it. The harness cost stays flat as the gold set grows.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — dev-warehouse snapshotting to bound eval cost
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A 2000-prompt gold set run against production warehouse data costs real money and puts contention on real user workloads. The senior fix is to snapshot the production warehouse into a dev instance, freeze the snapshot, and run the harness there. The frozen snapshot gives deterministic &lt;code&gt;expected_result&lt;/code&gt; hashes; the isolated warehouse absorbs the eval load without touching production.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The snapshot.&lt;/strong&gt; Snowflake &lt;code&gt;CLONE&lt;/code&gt; (zero-copy) or Databricks Delta Time Travel — pick a fixed instant, materialise a snapshot, harness reads only from it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The freeze.&lt;/strong&gt; Version the snapshot label (&lt;code&gt;eval_2026_07_30_v1&lt;/code&gt;) and never let the harness read from &lt;code&gt;latest&lt;/code&gt; — the gold expected-hashes are pinned to the snapshot instant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The refresh.&lt;/strong&gt; Rotate the snapshot monthly; rehash all &lt;code&gt;expected_result&lt;/code&gt; values against the new snapshot; commit the updated gold set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design the snapshot workflow and integrate it into the CI eval pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;production &lt;code&gt;analytics.fct.*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot mechanism&lt;/td&gt;
&lt;td&gt;Snowflake &lt;code&gt;CLONE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cadence&lt;/td&gt;
&lt;td&gt;monthly rotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI trigger&lt;/td&gt;
&lt;td&gt;prompt-template PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost cap&lt;/td&gt;
&lt;td&gt;$50 per CI run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Zero-copy clone from production to dev (Snowflake)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;analytics_eval&lt;/span&gt;
    &lt;span class="n"&gt;CLONE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;
    &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-30 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Grant the harness read-only access to the snapshot&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;analytics_eval&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;eval_harness&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;SCHEMAS&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;analytics_eval&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;eval_harness&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;analytics_eval&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;eval_harness&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Dedicated tiny warehouse; auto-suspend after 60s&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;eval_wh&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'X-SMALL'&lt;/span&gt;
    &lt;span class="n"&gt;AUTO_SUSPEND&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
    &lt;span class="n"&gt;AUTO_RESUME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
    &lt;span class="n"&gt;INITIALLY_SUSPENDED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;eval_wh&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;eval_harness&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Resource monitor caps the eval spend&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;RESOURCE&lt;/span&gt; &lt;span class="n"&gt;MONITOR&lt;/span&gt; &lt;span class="n"&gt;eval_daily&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;CREDIT_QUOTA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;      &lt;span class="c1"&gt;-- ~$50/day on X-SMALL&lt;/span&gt;
    &lt;span class="n"&gt;FREQUENCY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DAILY&lt;/span&gt;
    &lt;span class="n"&gt;TRIGGERS&lt;/span&gt;
        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="n"&gt;PERCENT&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="n"&gt;SUSPEND_IMMEDIATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;eval_wh&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;RESOURCE_MONITOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eval_daily&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 5. CI job — .github/workflows/eval.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eval-harness&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompts/**"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models/**"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;harness/**"&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;eval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r harness/requirements.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run harness&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SNOWFLAKE_PASSWORD }}&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_ROLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eval_harness&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_WAREHOUSE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eval_wh&lt;/span&gt;
          &lt;span class="na"&gt;SNOWFLAKE_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analytics_eval&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python -m harness.run --gold gold_set.jsonl --report scorecard.md&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post scorecard&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const fs = require('fs');&lt;/span&gt;
            &lt;span class="s"&gt;const body = fs.readFileSync('scorecard.md', 'utf8');&lt;/span&gt;
            &lt;span class="s"&gt;github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;body: body,&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enforce regression gate&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python -m harness.check --scorecard scorecard.md --baseline baseline.json --max-regression &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Snowflake &lt;code&gt;CLONE&lt;/code&gt; is zero-copy: creating &lt;code&gt;analytics_eval&lt;/code&gt; from &lt;code&gt;analytics&lt;/code&gt; costs no storage until pages diverge, so a full-warehouse snapshot is essentially free. This is the killer feature for eval workflows — you get a frozen point-in-time view without paying for a copy.&lt;/li&gt;
&lt;li&gt;The dedicated &lt;code&gt;eval_wh&lt;/code&gt; warehouse is X-SMALL with 60s auto-suspend. The CI run wakes it, runs the harness, and lets it suspend — total warehouse spend is a few dollars per run.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;eval_daily&lt;/code&gt; resource monitor is the disaster stop. If a runaway harness or a buggy prompt template starts burning credits, the warehouse suspends immediately at $50/day. This is the safety net that makes running the harness on every PR safe.&lt;/li&gt;
&lt;li&gt;The GitHub Actions job triggers on any PR that touches prompts, models, or harness code. It runs the harness, posts the scorecard as a PR comment, then enforces the regression gate: if execution accuracy drops by more than 2 points versus the baseline, the check fails and the PR cannot merge.&lt;/li&gt;
&lt;li&gt;Monthly rotation of the snapshot label (&lt;code&gt;analytics_eval_2026_08_30&lt;/code&gt;) and the &lt;code&gt;expected_hash&lt;/code&gt; field in the gold set is a small chore. Automate it: nightly job re-runs all gold SQLs against the new snapshot, updates the hashes, and opens a PR. Reviewers approve or reject based on whether the hash changes reflect real data drift.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Cost per CI run&lt;/th&gt;
&lt;th&gt;Guarantee&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zero-copy clone&lt;/td&gt;
&lt;td&gt;~$0 storage&lt;/td&gt;
&lt;td&gt;frozen point-in-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X-SMALL eval warehouse&lt;/td&gt;
&lt;td&gt;~$2-5 per full harness run&lt;/td&gt;
&lt;td&gt;isolated from production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource monitor&lt;/td&gt;
&lt;td&gt;hard-stop at $50/day&lt;/td&gt;
&lt;td&gt;worst-case bounded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI regression gate&lt;/td&gt;
&lt;td&gt;fails at -2 points&lt;/td&gt;
&lt;td&gt;no silent quality drops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly snapshot rotation&lt;/td&gt;
&lt;td&gt;1 PR/month&lt;/td&gt;
&lt;td&gt;fresh data every 30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any text-to-SQL eval harness at scale, snapshot the warehouse with zero-copy clone, run against a dedicated tiny warehouse with a hard resource-monitor cap, gate CI on a regression threshold, and rotate the snapshot monthly. This bounds the harness cost to tens of dollars per week regardless of gold-set size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on eval-harness design
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design the eval harness for a text-to-SQL feature you plan to ship to internal analysts against a 400-table Snowflake warehouse. Cover the gold-set construction, the three scoring metrics, the plan-hash pre-filter, the CI integration, the regression gate, and the operational cost. Then explain how you would prevent overfit and how you would decide when the harness itself is wrong."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a 500-prompt private gold set + three-tier cascade + zero-copy dev snapshot + CI regression gate + adversarial hold-out
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# harness/run.py — production eval harness with the three-tier cascade
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sf&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HarnessResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;exact_matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;plan_matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;exec_matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;per_tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;weakest_tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;cost_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_cap_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;50.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;HarnessResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval_harness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval_wh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analytics_eval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HarnessResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exact_matches&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan_matches&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;exec_matches&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;gold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;predicted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model_fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="c1"&gt;# Tier 1: exact-match (microseconds)
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predicted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gold_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exact_matches&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exec_matches&lt;/span&gt;  &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="nf"&gt;bump_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="c1"&gt;# Tier 2: plan-hash (milliseconds; no data scan)
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;plan_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predicted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;plan_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gold_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan_matches&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exec_matches&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="nf"&gt;bump_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="c1"&gt;# Tier 3: execution (seconds + $)
&lt;/span&gt;            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_with_cap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predicted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes_cap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5_000_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;got_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalise_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;exec_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;got_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exec_matches&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exec_ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;bump_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="nf"&gt;bump_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;estimate_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cost_cap_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;harness cost cap &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cost_cap_usd&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; exceeded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weakest_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;per_tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;per_tag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost_usd&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimate_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bump_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HarnessResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;per_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec_ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec_ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec_ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# harness/check.py — the regression gate
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scorecard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_regression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scorecard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;cur_acc&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec_matches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;base_acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec_matches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;delta&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur_acc&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;base_acc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;  &lt;span class="c1"&gt;# percentage points
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;max_regression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGRESSION: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_acc&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cur_acc&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (delta=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;pp)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_acc&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cur_acc&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (delta=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;pp)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scorecard.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseline.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                   &lt;span class="n"&gt;max_regression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# scorecard.md — posted as PR comment
### Eval scorecard

| Metric | Value |
|---|---|
| Total prompts | 500 |
| Exact-match | 128 (25.6%) |
| Plan-hash | 210 (42.0%) |
| Execution accuracy | 428/500 (85.6%) |
| Errors | 6 (1.2%) |
| Baseline (main) | 84.4% |
| Delta | +1.2 pp (PASS) |
| Cost | $3.87 |

### Weakest tag: window (67% exec_acc, n=42)

### Adversarial hold-out: 88.0% (unchanged)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gold set&lt;/td&gt;
&lt;td&gt;500 curated prompts + 100 adversarial hold-out&lt;/td&gt;
&lt;td&gt;ship gate + overfit detector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot&lt;/td&gt;
&lt;td&gt;zero-copy Snowflake CLONE&lt;/td&gt;
&lt;td&gt;frozen deterministic result hashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier 1&lt;/td&gt;
&lt;td&gt;exact-match&lt;/td&gt;
&lt;td&gt;free pass for byte-identical rewrites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier 2&lt;/td&gt;
&lt;td&gt;plan-hash EXPLAIN&lt;/td&gt;
&lt;td&gt;cheap semantic pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier 3&lt;/td&gt;
&lt;td&gt;execution&lt;/td&gt;
&lt;td&gt;ground truth; capped at 5 GB scanned per query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI gate&lt;/td&gt;
&lt;td&gt;-2 pp regression blocker&lt;/td&gt;
&lt;td&gt;zero silent quality drops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weakest tag&lt;/td&gt;
&lt;td&gt;per-tag exec_acc surface&lt;/td&gt;
&lt;td&gt;tells you where to invest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adversarial&lt;/td&gt;
&lt;td&gt;separate hold-out (100 prompts)&lt;/td&gt;
&lt;td&gt;catches overfit to public gold&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, every prompt-template PR runs the harness, posts a scorecard, and gates merge on a -2 pp regression threshold. The three-tier cascade keeps each run under $5 on a 500-prompt gold set. The adversarial hold-out is refreshed quarterly with real production failures; a divergence between the main gold accuracy and the adversarial accuracy is the earliest overfit signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gold-set size&lt;/td&gt;
&lt;td&gt;500 (main) + 100 (adversarial)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per harness run&lt;/td&gt;
&lt;td&gt;$3-5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time per harness run&lt;/td&gt;
&lt;td&gt;~4 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regression sensitivity&lt;/td&gt;
&lt;td&gt;2 pp on execution accuracy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overfit detector&lt;/td&gt;
&lt;td&gt;adversarial hold-out delta &amp;gt; 5 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy blocker&lt;/td&gt;
&lt;td&gt;100% of prompt PRs pass or explain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Private gold set on git&lt;/strong&gt;&lt;/strong&gt; — versioned, PR-reviewed, blamed. The gold set is the release contract; storing it as code is the only way to make it operationally serious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Three-tier cascade&lt;/strong&gt;&lt;/strong&gt; — exact-match → plan-hash → execution. Each tier resolves 30-40 percent of prompts at increasing cost. Total harness cost scales linearly with gold-set size at a very small constant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Zero-copy Snowflake clone&lt;/strong&gt;&lt;/strong&gt; — frozen point-in-time snapshot at no storage cost. The determinism of the &lt;code&gt;expected_hash&lt;/code&gt; field depends on the frozen data; snapshot rotation is the only reason hashes change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CI regression gate&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;-2 pp&lt;/code&gt; is the deploy blocker. This is a much stronger contract than "we run tests" — it says "quality cannot silently drop on any prompt change."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Adversarial hold-out&lt;/strong&gt;&lt;/strong&gt; — a partition of the gold set that never enters any prompt template or fine-tune. Divergence between main and adversarial accuracy is the overfit alarm; when it fires, rotate the main partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — 4 minutes and $5 per CI run against a 500-prompt gold set; scales to $25 per run at 2000 prompts with cascade. The eliminated cost is the silent regression that ships and only surfaces in user complaints two weeks later. Net O(1) per prompt through the cascade; O(gold-set) total per run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL Generation&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql-generation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL-generation eval-harness problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Data Validation&lt;/span&gt;
&lt;span&gt;Topic — data-validation&lt;/span&gt;
&lt;strong&gt;Data-validation and result-set diff problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Guardrails — schema linking, query validation, safety rails
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Four guardrail lanes — every LLM SQL passes all four or it does not touch the warehouse
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;nl2sql guardrails&lt;/code&gt; are the four independent lanes every LLM-generated candidate SQL must pass through before execution — schema linking (prune the prompt to the top-N relevant tables), parse validation (&lt;code&gt;SQLGlot&lt;/code&gt; AST parse plus identifier existence check), warehouse dry-run cost (Snowflake &lt;code&gt;EXPLAIN&lt;/code&gt; or BigQuery dry-run byte estimate), and safety allow-list (only &lt;code&gt;SELECT&lt;/code&gt; and &lt;code&gt;WITH&lt;/code&gt; statements against a read-only role) — and skipping any lane leaves the failure class it protects wide open, so seniors build the four lanes as first-class services, not as afterthoughts in the app code&lt;/strong&gt;. The lanes are independent so a fault on one does not cascade; each lane has its own error taxonomy and its own alerting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2iwor8wa76d19tyab1q.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2iwor8wa76d19tyab1q.jpeg" alt="Iconographic guardrail diagram — four vertical guardrail lanes labelled schema-link, parse, dry-run, cost, with a glowing LLM sparkle chip travelling left to right passing through each lane." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four lanes — what each catches and how.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema linking (pre-generation).&lt;/strong&gt; Retrieve the top-N tables and columns relevant to the user question and inject only those into the model prompt. Reduces the model's opportunity to hallucinate — if the invented column is not in the prompt, the model rarely conjures it. Implemented via an embedding index over DDL + column descriptions plus a table-graph walk that grabs foreign-key neighbours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse validation (post-generation, pre-execution).&lt;/strong&gt; Parse the generated SQL with &lt;code&gt;SQLGlot&lt;/code&gt; to confirm it is syntactically well-formed; walk the AST to enumerate every table and column reference; look each up in the live warehouse catalog; reject on any unknown identifier. This lane catches every hallucinated column and every misspelt table before the warehouse ever sees the query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dry-run cost (post-parse, pre-execution).&lt;/strong&gt; Ask the warehouse for a cost / byte estimate without running the query. Snowflake exposes this via &lt;code&gt;EXPLAIN&lt;/code&gt;; BigQuery via the &lt;code&gt;dryRun&lt;/code&gt; job flag; Databricks via &lt;code&gt;EXPLAIN COST&lt;/code&gt;. Reject any query whose estimated scan exceeds a per-tenant budget (typical: 5 GB per query, 500 GB per tenant per day).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety allow-list (statement-type filter).&lt;/strong&gt; Parse the top-level statement; reject anything that is not &lt;code&gt;SELECT&lt;/code&gt; or &lt;code&gt;WITH ... SELECT&lt;/code&gt;. Combined with the read-only role, this is belt-and-braces against DML / DDL that a mis-prompted or adversarial user might induce.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pre-execution vs post-execution guardrails.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-execution.&lt;/strong&gt; All four lanes fire before the query touches production data. Failures are cheap (parse error, dry-run rejection) and never scan bytes or lock rows. This is where 99 percent of guardrail effort belongs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-execution.&lt;/strong&gt; Row-count sanity checks (does the result look plausible?), column-mask enforcement (redact PII before returning), result-set size caps (never send more than 10k rows to the client). Post-execution is a &lt;em&gt;second&lt;/em&gt; line of defense, not a substitute for pre-execution rejection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The row-limit + read-only + column-mask trio.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Row limit.&lt;/strong&gt; Every generated SQL is rewritten to &lt;code&gt;SELECT ... LIMIT N&lt;/code&gt; if &lt;code&gt;LIMIT&lt;/code&gt; is missing. The &lt;code&gt;LIMIT&lt;/code&gt; is a hard cap on result-set size shipped to the client, protecting bandwidth and the browser from a 5 million-row response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-only role.&lt;/strong&gt; The service account under which the bot connects has &lt;code&gt;SELECT&lt;/code&gt; only. No &lt;code&gt;INSERT&lt;/code&gt;, no &lt;code&gt;UPDATE&lt;/code&gt;, no &lt;code&gt;DELETE&lt;/code&gt;, no &lt;code&gt;CREATE&lt;/code&gt;, no &lt;code&gt;DROP&lt;/code&gt;. Even if a lane fails, the warehouse's own permission check rejects the destructive statement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Column mask.&lt;/strong&gt; Every PII column (&lt;code&gt;email&lt;/code&gt;, &lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;ssn_last4&lt;/code&gt;, &lt;code&gt;dob&lt;/code&gt;) is either masked at the warehouse via &lt;code&gt;MASKING POLICY&lt;/code&gt; or stripped at the API layer before render. The mask defends against a prompt like "show me all customers with names starting with A" that would otherwise leak PII into a chat log.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on guardrails.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you stop the model from dropping a table?" — required answer: read-only role plus statement-type allow-list; the drop never reaches the warehouse and the warehouse rejects it if it did.&lt;/li&gt;
&lt;li&gt;"How do you stop a runaway 500-TB scan?" — required answer: warehouse dry-run byte estimate plus per-tenant scanned-bytes cap.&lt;/li&gt;
&lt;li&gt;"How do you catch a hallucinated column?" — parse the SQL with &lt;code&gt;SQLGlot&lt;/code&gt;, enumerate identifiers, look up in the live catalog, reject on unknown.&lt;/li&gt;
&lt;li&gt;"How do you protect PII?" — column-level masking policies at the warehouse plus API-layer redaction, never trust the model to filter PII.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the schema-linking retriever
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most cost-effective guardrail is schema linking — pruning the prompt to only the top-N tables and columns the model actually needs. On a 400-table warehouse, dumping the full DDL into the prompt (a) does not fit and (b) invites hallucination. A retriever grounded on table + column descriptions plus foreign-key neighbours reliably returns the 5-10 tables that matter for any given user question.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The corpus.&lt;/strong&gt; One document per table containing &lt;code&gt;table_name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;columns[]&lt;/code&gt; with names and descriptions, &lt;code&gt;foreign_keys[]&lt;/code&gt; pointing at neighbours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The index.&lt;/strong&gt; Embeddings over the corpus (&lt;code&gt;text-embedding-3-large&lt;/code&gt; or an open-source equivalent) stored in a vector DB (pgvector, Weaviate, Qdrant).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The retriever.&lt;/strong&gt; For each user question, embed the question, top-K nearest tables, expand by one hop along foreign keys, cap at 15 tables and 300 columns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement the schema-linking retriever and show how the shortened schema context enters the prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse&lt;/td&gt;
&lt;td&gt;Snowflake with 400 tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corpus&lt;/td&gt;
&lt;td&gt;one JSON doc per table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding model&lt;/td&gt;
&lt;td&gt;text-embedding-3-large (3072 dims)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector store&lt;/td&gt;
&lt;td&gt;pgvector on Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt cap&lt;/td&gt;
&lt;td&gt;15 tables, 300 columns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# schema_linker.py — retrieve the top-N tables for a user question
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;   &lt;span class="c1"&gt;# or any embedding provider
&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TableDoc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="c1"&gt;# [{name, type, description}]
&lt;/span&gt;    &lt;span class="n"&gt;foreign_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="c1"&gt;# [{col, ref_table, ref_col}]
&lt;/span&gt;    &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index_corpus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;corpus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TableDoc&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One-time indexing: embed every table doc, store in pgvector.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;corpus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; \
                      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                INSERT INTO schema_index(table_name, doc_json, embedding)
                VALUES (%s, %s, %s)
                ON CONFLICT (table_name) DO UPDATE
                  SET doc_json = EXCLUDED.doc_json,
                      embedding = EXCLUDED.embedding
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__dict__&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;emb&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;max_tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TableDoc&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the top-N tables for a user question.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;q_emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT table_name, doc_json,
                   embedding &amp;lt;=&amp;gt; %s::vector AS distance
            FROM   schema_index
            ORDER  BY distance ASC
            LIMIT  %s
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q_emb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;seeds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

    &lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TableDoc&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seeds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TableDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Foreign-key expansion (one hop)
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;neighbours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foreign_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ref_table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;neighbours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ref_table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;neighbours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                SELECT table_name, doc_json
                FROM   schema_index
                WHERE  table_name = ANY(%s)
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;neighbours&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_json&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TableDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_json&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;max_tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="c1"&gt;# Cap column count across selected tables
&lt;/span&gt;    &lt;span class="n"&gt;total_cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total_cols&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_columns&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())[:&lt;/span&gt;&lt;span class="n"&gt;max_tables&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;format_for_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TableDoc&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-- schema context (top-15 relevant tables)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;-- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE TABLE &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,   -- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;);&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foreign_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-- FK &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;col&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ref_table&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ref_col&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The corpus is one embedded document per table, not per column, because column-level embeddings explode the index size without adding much signal at the retrieval stage. Column descriptions are packed into the table document so the embedding captures the column vocabulary; per-column retrieval happens later inside the model.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retrieve&lt;/code&gt; embeds the user question, does a cosine top-K over the table index, then expands one hop along foreign keys. The FK hop is the killer feature — if the question needs &lt;code&gt;orders&lt;/code&gt; and &lt;code&gt;customers&lt;/code&gt; and only &lt;code&gt;orders&lt;/code&gt; scored in the top-K, the FK expansion pulls &lt;code&gt;customers&lt;/code&gt; in even without semantic overlap in the question text.&lt;/li&gt;
&lt;li&gt;The cap logic (&lt;code&gt;max_tables=15, max_columns=300&lt;/code&gt;) protects the prompt budget. A 400-table warehouse can produce a 50k-token DDL dump; capping at 15 tables keeps the schema context under 5k tokens, leaving room for the actual instruction template.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;format_for_prompt&lt;/code&gt; emits a &lt;code&gt;CREATE TABLE&lt;/code&gt; DDL-ish rendering with inline column comments and FK annotations at the bottom. This is the format frontier LLMs respond best to — DDL is a shape they've seen in training, and inline comments improve schema comprehension.&lt;/li&gt;
&lt;li&gt;The retriever runs in ~50 ms end-to-end (one embedding call, one vector query, one FK lookup). This is well under the 5-second p95 budget for the whole text-to-SQL pipeline. Cache popular questions to drop retrieval to zero on repeats.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User question&lt;/th&gt;
&lt;th&gt;Seed tables (top-K)&lt;/th&gt;
&lt;th&gt;After FK hop&lt;/th&gt;
&lt;th&gt;Final prompt tables&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"top 5 US customers by revenue"&lt;/td&gt;
&lt;td&gt;fct.revenue_by_customer, dim.customers&lt;/td&gt;
&lt;td&gt;+fct.orders, +dim.geo&lt;/td&gt;
&lt;td&gt;5 tables, ~40 cols&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"monthly active users last quarter"&lt;/td&gt;
&lt;td&gt;fct.wau_weekly, dim.users&lt;/td&gt;
&lt;td&gt;+fct.sessions&lt;/td&gt;
&lt;td&gt;3 tables, ~20 cols&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"pending shipments over 3 days"&lt;/td&gt;
&lt;td&gt;fct.orders, dim.products&lt;/td&gt;
&lt;td&gt;+dim.warehouses, +dim.suppliers&lt;/td&gt;
&lt;td&gt;6 tables, ~50 cols&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never dump the full DDL into a text-to-SQL prompt. Build a schema-linking retriever that returns 5-15 tables plus their FK neighbours, format them as annotated &lt;code&gt;CREATE TABLE&lt;/code&gt; DDL, and cap the prompt at 300 columns. This single lane cuts hallucination by 70-90 percent versus a raw-DDL prompt.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — SQLGlot parse and identifier validation
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; After the model generates SQL, the parse-validation lane must confirm the SQL is (a) syntactically well-formed and (b) references only real tables and columns. &lt;code&gt;SQLGlot&lt;/code&gt; is the workhorse — it parses SQL for 20+ dialects, exposes an AST, and lets you walk every identifier reference cheaply. The validation is O(number of identifiers) and runs in milliseconds.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parse.&lt;/strong&gt; &lt;code&gt;sqlglot.parse_one(sql, read=dialect)&lt;/code&gt; returns the AST or raises &lt;code&gt;ParseError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identifier walk.&lt;/strong&gt; Traverse the AST, collect every &lt;code&gt;Table&lt;/code&gt; and &lt;code&gt;Column&lt;/code&gt; node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catalog lookup.&lt;/strong&gt; For each identifier, check the live warehouse catalog (&lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt; or an in-memory cached snapshot).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject taxonomy.&lt;/strong&gt; Distinguish "table not found" from "column not found in known table" from "column ambiguous across joined tables."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement the parse-validation lane and integrate it into the guardrail pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parser&lt;/td&gt;
&lt;td&gt;SQLGlot (Snowflake dialect)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catalog source&lt;/td&gt;
&lt;td&gt;INFORMATION_SCHEMA cached hourly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reject taxonomy&lt;/td&gt;
&lt;td&gt;3 error kinds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency budget&lt;/td&gt;
&lt;td&gt;5 ms per query&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# parse_validator.py — the parse-validation guardrail lane
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nb"&gt;str&lt;/span&gt;       &lt;span class="c1"&gt;# "parse", "unknown_table", "unknown_column", "ambiguous_column"
&lt;/span&gt;    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Catalog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;      &lt;span class="c1"&gt;# {table_name: {col1, col2, ...}}
&lt;/span&gt;
    &lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Catalog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                SELECT table_schema || &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; || table_name AS full_name,
                       array_agg(lower(column_name))       AS cols
                FROM   information_schema.columns
                WHERE  table_schema IN (&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DIM&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FCT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;RAW&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SEMANTIC&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
                GROUP  BY 1
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Catalog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dialect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. Parse
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dialect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Collect referenced tables
&lt;/span&gt;    &lt;span class="n"&gt;tables_in_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;known_tables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tables_in_query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known_tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown_table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                          &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table not found: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                          &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Collect column references, keyed by table if aliased
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;col_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;tbl_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tbl_name&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;tbl_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;col_name&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tbl_name&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown_column&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                              &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;column not found: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tbl_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                              &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tbl_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;tbl_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Column is unaliased. Search across every table in the query.
&lt;/span&gt;            &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tables_in_query&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;col_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;())]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown_column&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                              &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;column not resolvable: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ambiguous_column&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                              &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ambiguous: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Step 1 parses with &lt;code&gt;SQLGlot&lt;/code&gt;. A &lt;code&gt;ParseError&lt;/code&gt; is a hard reject — the model produced syntactically invalid SQL. This catches subtle mistakes like unmatched parentheses or wrong keyword order that would otherwise waste warehouse compilation cycles.&lt;/li&gt;
&lt;li&gt;Step 2 walks the AST with &lt;code&gt;tree.find_all(exp.Table)&lt;/code&gt; and collects every table reference. Comparison against the live catalog rejects any hallucinated table. The catalog snapshot is refreshed hourly; anything more frequent is unnecessary given schema-evolution cadence.&lt;/li&gt;
&lt;li&gt;Step 3 walks column references. Aliased columns (&lt;code&gt;orders.customer_id&lt;/code&gt;) are validated against the aliased table's known columns. Un-aliased columns are searched across every table in the query; a hit in exactly one table passes, zero hits or multiple hits fail. This matches SQL's own name-resolution rules.&lt;/li&gt;
&lt;li&gt;The three-error taxonomy (&lt;code&gt;unknown_table&lt;/code&gt;, &lt;code&gt;unknown_column&lt;/code&gt;, &lt;code&gt;ambiguous_column&lt;/code&gt;) is what the downstream retry logic uses. On &lt;code&gt;unknown_table&lt;/code&gt;, retry with an expanded schema-linking context. On &lt;code&gt;unknown_column&lt;/code&gt;, retry with a hint pointing at the closest real column name. On &lt;code&gt;ambiguous_column&lt;/code&gt;, retry with a hint asking the model to fully qualify.&lt;/li&gt;
&lt;li&gt;Latency: &lt;code&gt;SQLGlot&lt;/code&gt; parses a typical SQL in ~1 ms; the AST walk is ~1 ms; the catalog lookups are pure Python dict operations against an in-memory snapshot. End-to-end lane latency is under 5 ms for 99 percent of queries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL&lt;/th&gt;
&lt;th&gt;Errors returned&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT id FROM orders&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT cust_id FROM orders&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unknown_column: orders.cust_id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT id FROM ordres&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unknown_table: ordres&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT user_id FROM sessions JOIN customers ON ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ambiguous_column: user_id in [sessions, customers]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM orders WHERE (&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;parse: unexpected end of input&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Run every LLM-generated SQL through a &lt;code&gt;SQLGlot&lt;/code&gt; parse plus identifier-lookup lane before the warehouse sees it. Cache the catalog for an hour; refresh on &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt; change events. Distinguish the three reject kinds so retry prompts can be targeted.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — warehouse dry-run cost gate
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Even a syntactically valid SQL against real tables can scan a petabyte if it joins the wrong things. The warehouse dry-run lane asks the engine "how much would this cost?" without actually running the query, and rejects anything above the per-tenant budget. Snowflake exposes this via &lt;code&gt;EXPLAIN&lt;/code&gt;; BigQuery via &lt;code&gt;dryRun&lt;/code&gt;; Databricks via &lt;code&gt;EXPLAIN COST&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake.&lt;/strong&gt; &lt;code&gt;EXPLAIN USING JSON &amp;lt;sql&amp;gt;&lt;/code&gt; returns a plan JSON that includes &lt;code&gt;partitionsTotal&lt;/code&gt; and &lt;code&gt;partitionsScanned&lt;/code&gt; estimates. Use them to bound scan bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigQuery.&lt;/strong&gt; Set &lt;code&gt;dryRun=True&lt;/code&gt; on the job config; the API returns &lt;code&gt;totalBytesProcessed&lt;/code&gt; without running the query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databricks.&lt;/strong&gt; &lt;code&gt;EXPLAIN COST &amp;lt;sql&amp;gt;&lt;/code&gt; returns an estimated cost tree; parse for scan bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Implement the cost-gate lane for Snowflake with a 5 GB per-query cap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse&lt;/td&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimator&lt;/td&gt;
&lt;td&gt;EXPLAIN USING JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cap&lt;/td&gt;
&lt;td&gt;5,000,000,000 bytes (5 GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reject reason&lt;/td&gt;
&lt;td&gt;"estimated scan exceeds budget"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cost_gate.py — Snowflake dry-run cost estimator
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sf&lt;/span&gt;


&lt;span class="n"&gt;CAP_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5_000_000_000&lt;/span&gt;   &lt;span class="c1"&gt;# 5 GB per query
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;estimate_scan_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return an estimated scan-bytes upper bound for the query.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXPLAIN USING JSON &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;plan_rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# Snowflake returns one row with the plan JSON in the first column
&lt;/span&gt;    &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan_rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;scanned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;walk_steps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# partitionsTotal * average_partition_size approximates scan bytes;
&lt;/span&gt;        &lt;span class="c1"&gt;# a more accurate estimate uses TABLE_DML_HISTORY.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;operation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TableScan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;scanned&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytesScanned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;scanned&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;walk_steps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;children&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nf"&gt;walk_steps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cap_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CAP_BYTES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;estimated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimate_scan_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dry-run failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;estimated&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cap_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;estimated scan &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;estimated&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; GB &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                       &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exceeds budget of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cap_bytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; GB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;estimated scan &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;estimated&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; GB (ok)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;EXPLAIN USING JSON &amp;lt;sql&amp;gt;&lt;/code&gt; runs the Snowflake compiler and returns the full plan without touching data. Compilation is measured in tens of milliseconds; no warehouse credits are consumed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;walk_steps&lt;/code&gt; recursively yields every plan node. &lt;code&gt;TableScan&lt;/code&gt; nodes carry &lt;code&gt;bytesScanned&lt;/code&gt; estimates that Snowflake derives from cached table statistics. Summing across scans gives an upper-bound estimate for the query.&lt;/li&gt;
&lt;li&gt;The cap (&lt;code&gt;5 GB&lt;/code&gt;) is tunable per-tenant. Internal analyst prompts might allow 10 GB; customer-facing self-serve prompts might cap at 500 MB. The cap lives in configuration, not code, so on-call can tighten it without a deploy.&lt;/li&gt;
&lt;li&gt;On rejection, the returned message names the estimated scan and the budget so the front-end can show the user something actionable ("your query would scan 47 GB; try adding a date filter"). This is far more useful than a generic "query rejected."&lt;/li&gt;
&lt;li&gt;Latency: EXPLAIN is 20-50 ms on Snowflake; the JSON parse and step walk are microseconds. The lane budget is comfortable at 100 ms p99.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL (approx)&lt;/th&gt;
&lt;th&gt;Estimated scan&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT COUNT(*) FROM fct.orders WHERE order_date = current_date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12 MB&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM fct.orders o JOIN fct.events e ON o.customer_id = e.user_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;47 GB&lt;/td&gt;
&lt;td&gt;reject (&amp;gt;5 GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT SUM(revenue) FROM fct.revenue_daily WHERE month = current_month&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;340 MB&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM raw.clickstream&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;890 GB&lt;/td&gt;
&lt;td&gt;reject (&amp;gt;5 GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every generated SQL passes a warehouse dry-run cost gate before execution. Use the native estimator (&lt;code&gt;EXPLAIN USING JSON&lt;/code&gt; in Snowflake, &lt;code&gt;dryRun&lt;/code&gt; in BigQuery, &lt;code&gt;EXPLAIN COST&lt;/code&gt; in Databricks). Cap in bytes not seconds — a query that scans 100 GB but takes 4 seconds still costs $50 in warehouse credits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on guardrail architecture
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your team ships text-to-SQL to 5000 internal analysts on a Snowflake warehouse with 400 tables. Design the guardrail stack. Cover the schema-linking retriever, the parse-validation lane, the cost-gate lane, and the safety allow-list lane. Explain how failures on each lane feed back into the retry / prompt-tuning loop, and how you would monitor the guardrail stack itself."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a four-lane guardrail pipeline with typed error surfaces + retry loops + Prometheus metrics
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# guardrails/pipeline.py — the four-lane guardrail pipeline
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LaneVerdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;PASS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;REJECT_HALLUCINATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reject_hallucination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;REJECT_UNSAFE_OP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reject_unsafe_op&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;REJECT_OVER_BUDGET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reject_over_budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;REJECT_PARSE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reject_parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LaneResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;lane&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;LaneVerdict&lt;/span&gt;
    &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_guardrails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LaneResult&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LaneResult&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# Lane 1: schema linking (pre-generation)
&lt;/span&gt;    &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;schema_docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LaneResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema_link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LaneVerdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema_docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tables selected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;candidate_sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model_fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LaneResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LaneVerdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempt &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="c1"&gt;# Lane 2: parse validation
&lt;/span&gt;        &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;parse_errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;parse_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parse_errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LaneResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LaneVerdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REJECT_HALLUCINATION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parse_errors&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;parse_ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;augment_prompt_with_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LaneResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LaneVerdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="c1"&gt;# Lane 3: safety allow-list
&lt;/span&gt;        &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_read_only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_sql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LaneResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;safety&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LaneVerdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REJECT_UNSAFE_OP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;non-SELECT rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;
        &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LaneResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;safety&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LaneVerdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="c1"&gt;# Lane 4: cost gate
&lt;/span&gt;        &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidate_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cap_bytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5_000_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cost_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LaneResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LaneVerdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REJECT_OVER_BUDGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                    &lt;span class="n"&gt;cost_msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;
        &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LaneResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LaneVerdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="c1"&gt;# All four lanes passed
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;candidate_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;

    &lt;span class="c1"&gt;# Retries exhausted
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_read_only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__class__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;select&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;union&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intersect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;except&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;augment_prompt_with_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;hint_lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-- previous attempt failed with the following errors:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;hint_lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-- - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hint_lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-- please fix and re-generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hint_lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prometheus_metrics.yml — instrumentation for the guardrail stack&lt;/span&gt;
&lt;span class="c1"&gt;# One counter per lane / verdict; one histogram per lane latency.&lt;/span&gt;
&lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guardrail_verdicts_total&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;counter&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;lane&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;verdict&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;tenant&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guardrail_lane_latency_ms&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;histogram&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;lane&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;tenant&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;buckets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;25&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;50&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;100&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;250&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;500&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guardrail_retry_count&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;histogram&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;tenant&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;buckets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;alerts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SchemaLinkRegression&lt;/span&gt;
    &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;rate(guardrail_verdicts_total{lane="parse",verdict="reject_hallucination"}[15m])&lt;/span&gt;
        &lt;span class="s"&gt;/ rate(guardrail_verdicts_total{lane="parse"}[15m]) &amp;gt; 0.15&lt;/span&gt;
    &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15m&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CostGateStorm&lt;/span&gt;
    &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;rate(guardrail_verdicts_total{lane="cost",verdict="reject_over_budget"}[5m]) &amp;gt; 5&lt;/span&gt;
    &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lane 1&lt;/td&gt;
&lt;td&gt;schema-linking retriever&lt;/td&gt;
&lt;td&gt;prune prompt to 15 tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lane 2&lt;/td&gt;
&lt;td&gt;SQLGlot parse + identifier validation&lt;/td&gt;
&lt;td&gt;reject hallucinated tables/columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lane 3&lt;/td&gt;
&lt;td&gt;safety allow-list&lt;/td&gt;
&lt;td&gt;reject any non-SELECT/WITH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lane 4&lt;/td&gt;
&lt;td&gt;Snowflake EXPLAIN cost gate&lt;/td&gt;
&lt;td&gt;reject scan &amp;gt; 5 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry loop&lt;/td&gt;
&lt;td&gt;augment prompt with lane errors&lt;/td&gt;
&lt;td&gt;2 additional attempts on hallucination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metrics&lt;/td&gt;
&lt;td&gt;Prometheus per-lane counters + histograms&lt;/td&gt;
&lt;td&gt;detect regressions in production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alerts&lt;/td&gt;
&lt;td&gt;schema-link regression + cost-gate storm&lt;/td&gt;
&lt;td&gt;on-call visibility&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After deployment, every user question passes through the four lanes in ~150 ms end-to-end (dominated by the LLM call, not the guardrails themselves). Hallucinations trigger up to two retries with augmented prompts; a persistent hallucination is a null-return that surfaces to the user as "I couldn't answer that reliably" — the correct behaviour when the harness confidence is low.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lane 1 latency (p99)&lt;/td&gt;
&lt;td&gt;~50 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lane 2 latency (p99)&lt;/td&gt;
&lt;td&gt;~5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lane 3 latency (p99)&lt;/td&gt;
&lt;td&gt;~1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lane 4 latency (p99)&lt;/td&gt;
&lt;td&gt;~50 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End-to-end guardrail overhead&lt;/td&gt;
&lt;td&gt;~100 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucination retry rate&lt;/td&gt;
&lt;td&gt;8-12% (drops with prompt tuning)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost-gate reject rate&lt;/td&gt;
&lt;td&gt;~3% (steady)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsafe-op reject rate&lt;/td&gt;
&lt;td&gt;~0.1% (rare; users don't usually ask for DDL)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Four independent lanes&lt;/strong&gt;&lt;/strong&gt; — each lane has a distinct failure taxonomy and a distinct mitigation. A regression in one lane does not silently corrupt the others; metrics per lane make the failure mode observable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Retry loop with augmented prompt&lt;/strong&gt;&lt;/strong&gt; — hallucinations are recoverable: feed the error taxonomy back to the model and it usually corrects on retry. Two retries is the practical sweet spot; three or more suggests the model cannot serve this question and the correct answer is to escalate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Read-only role + statement allow-list&lt;/strong&gt;&lt;/strong&gt; — belt-and-braces safety. The allow-list is fast (Python-side statement-type check) and rejects DDL/DML before they cost even a warehouse compilation. The read-only role is the final backstop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Warehouse dry-run cost estimator&lt;/strong&gt;&lt;/strong&gt; — the native cost estimator is nearly free (EXPLAIN takes tens of milliseconds and no credits). Bounding scan bytes per query bounds the worst-case cost of the entire system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Prometheus instrumentation&lt;/strong&gt;&lt;/strong&gt; — one counter per (lane, verdict) and one histogram per lane latency. This is enough to detect regressions (hallucination rate creeping up), cost storms (a bad prompt template getting rejected en masse), and latency drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — ~100 ms of latency overhead, ~5-10 percent of prompts trigger at least one retry, ~3 percent are rejected outright. The eliminated cost is the "one bad query blew a $18k hole" incident and the "the model wrote a DELETE" catastrophe. Net O(1) latency overhead per query with bounded worst case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Data Validation&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — data-validation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Data-validation and safety-rail problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Design&lt;/span&gt;
&lt;span&gt;Topic — design&lt;/span&gt;
&lt;strong&gt;Design problems on LLM guardrail architectures&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Semantic-layer grounding — dbt Semantic Layer, LookML, Cube
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Ground on metrics, not raw tables — one definition, zero drift, and the model never redefines revenue again
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;semantic layer grounding&lt;/code&gt; is the pattern where the text-to-SQL system exposes the enterprise's canonical metric catalog (dbt Semantic Layer, LookML, Cube) as first-class candidates during retrieval, prompts the model to &lt;em&gt;prefer&lt;/em&gt; a defined metric over a raw-table derivation whenever the question maps to one, and refuses to let the model reinvent &lt;code&gt;revenue&lt;/code&gt;, &lt;code&gt;MAU&lt;/code&gt;, &lt;code&gt;churn&lt;/code&gt; or any other business metric from scratch — collapsing the metric-mis-definition failure class into a solved problem because the metric is defined once, upstream, and every downstream query reads from the same definition&lt;/strong&gt;. Every serious 2026 text-to-SQL deployment grounds on a semantic layer for the top-20 metrics; the ones that skip this step ship dashboards that disagree with each other.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6j3uex8vblkaevnjbzl.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6j3uex8vblkaevnjbzl.jpeg" alt="Iconographic semantic-layer diagram — a metric definition card on the left, a natural-language question in the centre, and a grounded SQL card on the right that references the metric by name." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why raw-table grounding loses at the enterprise scale.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metric drift.&lt;/strong&gt; Without a semantic layer, &lt;code&gt;revenue&lt;/code&gt; is defined in 8 different SQL files across the org — one includes shipping, one subtracts refunds, one applies FX conversion, one filters out test accounts. The model, given raw tables, picks a random derivation. Users see different numbers on different screens; trust collapses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join complexity.&lt;/strong&gt; The correct &lt;code&gt;MAU&lt;/code&gt; computation joins &lt;code&gt;sessions&lt;/code&gt; to &lt;code&gt;users&lt;/code&gt; to &lt;code&gt;bot_signatures&lt;/code&gt; to &lt;code&gt;plan_history&lt;/code&gt;. The model, faced with 20 columns and 4 tables, has to reinvent the join every time — and reinvents it wrong 30 percent of the time. A metric defined once pre-joins for the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grain confusion.&lt;/strong&gt; &lt;code&gt;revenue&lt;/code&gt; at daily vs monthly vs quarterly grain is not the same thing. Raw-table SQL forces the model to figure out grain; semantic-layer metrics tag their grain explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access control.&lt;/strong&gt; Row-level and column-level filters (tenant isolation, PII masking) live in the semantic layer once, not in every ad-hoc SQL the model writes. Semantic-layer grounding is a security posture, not just a correctness posture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The semantic-layer contract — what a defined metric looks like.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metric name.&lt;/strong&gt; &lt;code&gt;weekly_active_users&lt;/code&gt;, &lt;code&gt;net_revenue_daily&lt;/code&gt;, &lt;code&gt;enterprise_churn_rate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure.&lt;/strong&gt; The aggregation (&lt;code&gt;count_distinct(user_id)&lt;/code&gt;, &lt;code&gt;sum(revenue) - sum(refund_amount)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dimensions.&lt;/strong&gt; The valid slice-by axes (&lt;code&gt;week&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;product_category&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filters.&lt;/strong&gt; Standard filters applied to every query (&lt;code&gt;is_bot = false&lt;/code&gt;, &lt;code&gt;is_test_account = false&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grain.&lt;/strong&gt; Time grain (&lt;code&gt;day&lt;/code&gt;, &lt;code&gt;week&lt;/code&gt;, &lt;code&gt;month&lt;/code&gt;) and entity grain (&lt;code&gt;user&lt;/code&gt;, &lt;code&gt;account&lt;/code&gt;, &lt;code&gt;order&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Joins.&lt;/strong&gt; Pre-defined joins the metric depends on so downstream never re-derives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;dbt Semantic Layer, LookML, Cube — the three canonical vendors.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;dbt Semantic Layer.&lt;/strong&gt; Metric definitions live in &lt;code&gt;metricflow&lt;/code&gt; YAML alongside the dbt project. Query via GraphQL, JDBC, or the MCP endpoint. Best fit for teams already invested in dbt. Compiles metric queries to warehouse-native SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LookML.&lt;/strong&gt; Looker's semantic model language; metrics are defined as &lt;code&gt;measure&lt;/code&gt; blocks in &lt;code&gt;.model.lkml&lt;/code&gt; files. Query via the Looker API or the SDK. Best fit for teams whose primary BI tool is Looker. LookML has a decade of production hardening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cube.&lt;/strong&gt; Standalone semantic layer; metric definitions in JavaScript or YAML. Query via REST, GraphQL, or SQL API. Best fit for teams whose primary consumer is a custom app or LLM interface — Cube's SQL API is specifically designed for LLM grounding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How grounding actually flows.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retrieve.&lt;/strong&gt; Alongside table docs, the retriever indexes metric definitions and returns the top-N metrics for each user question. A question about "revenue" scores &lt;code&gt;net_revenue_daily&lt;/code&gt; above any raw table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt.&lt;/strong&gt; The prompt template says "PREFER defined metrics over raw tables when a metric answers the question." The metric definitions enter the prompt as first-class candidates with example usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate.&lt;/strong&gt; The model emits SQL like &lt;code&gt;SELECT * FROM {{ metric('net_revenue_daily') }} WHERE week &amp;gt;= ...&lt;/code&gt; (dbt syntax) or &lt;code&gt;SELECT * FROM weekly_active_users WHERE ...&lt;/code&gt; (Cube's SQL API view).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile.&lt;/strong&gt; The semantic layer expands the metric reference into full warehouse SQL. This step is transparent to the model but ensures the compiled SQL always has the canonical join / filter / grain semantics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on semantic-layer grounding.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Why not just add more docs?" — required answer: docs decay; a runtime semantic layer is the enforcement mechanism.&lt;/li&gt;
&lt;li&gt;"How do you decide which metrics to define?" — the top-20 by dashboard citation count. Instrument your BI tool to find them.&lt;/li&gt;
&lt;li&gt;"How do you handle a question that doesn't map to a defined metric?" — fall back to raw-table generation with a lowered confidence signal to the user.&lt;/li&gt;
&lt;li&gt;"How do you keep the semantic layer in sync with the model?" — refresh the retriever index on every dbt build or LookML deploy; treat metric changes as prompt-template changes with harness gating.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — define a &lt;code&gt;weekly_active_users&lt;/code&gt; metric in dbt Semantic Layer
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical grounded prompt starts with a metric definition. Walk through defining &lt;code&gt;weekly_active_users&lt;/code&gt; in dbt's &lt;code&gt;metricflow&lt;/code&gt; YAML, exposing it via the Semantic Layer API, and then showing the compiled warehouse SQL that lands from a grounded prompt.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model.&lt;/strong&gt; &lt;code&gt;models/marts/weekly_active_users.sql&lt;/code&gt; — the underlying join and dedupe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic model.&lt;/strong&gt; &lt;code&gt;models/semantic_models/users.yml&lt;/code&gt; — dimensions, entities, measures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric.&lt;/strong&gt; &lt;code&gt;metrics/wau.yml&lt;/code&gt; — the named metric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query.&lt;/strong&gt; JDBC or MCP call from the text-to-SQL app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Define the metric end-to-end and show the compiled SQL that lands from a grounded prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;&lt;code&gt;models/marts/weekly_active_users.sql&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic model&lt;/td&gt;
&lt;td&gt;&lt;code&gt;models/semantic_models/users.yml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric&lt;/td&gt;
&lt;td&gt;&lt;code&gt;metrics/wau.yml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer&lt;/td&gt;
&lt;td&gt;dbt Semantic Layer JDBC endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# models/semantic_models/users.yml&lt;/span&gt;
&lt;span class="na"&gt;semantic_models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;users&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('dim_users')&lt;/span&gt;
    &lt;span class="na"&gt;entities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;primary&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user_id&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;week&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;time&lt;/span&gt;
        &lt;span class="na"&gt;type_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;time_granularity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;week&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;date_trunc('week', activity_ts)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;country&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;categorical&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;categorical&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;is_bot&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;categorical&lt;/span&gt;
    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;distinct_users&lt;/span&gt;
        &lt;span class="na"&gt;agg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count_distinct&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# metrics/wau.yml&lt;/span&gt;
&lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weekly_active_users&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Weekly Active Users&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;Distinct user_id per ISO week, excluding bot traffic and&lt;/span&gt;
      &lt;span class="s"&gt;test accounts. Grain: week. Definition owner: analytics-eng.&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;simple&lt;/span&gt;
    &lt;span class="na"&gt;type_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;measure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;distinct_users&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;{{ Dimension('user__is_bot') }} = false&lt;/span&gt;
      &lt;span class="s"&gt;AND {{ Dimension('user__is_test_account') }} = false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/query_metric.py — the text-to-SQL app grounding on the metric
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dbt_semantic_layer_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SemanticLayerClient&lt;/span&gt;

&lt;span class="n"&gt;sl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SemanticLayerClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sl.dbt.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBT_SL_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# The model emits this after seeing the metric definition in its prompt context
&lt;/span&gt;&lt;span class="n"&gt;grounded_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weekly_active_users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;where&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{ Dimension(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user__country&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) }} = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# The Semantic Layer expands to warehouse-native SQL
&lt;/span&gt;&lt;span class="n"&gt;compiled_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;grounded_query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Compiled SQL emitted by the Semantic Layer (Snowflake dialect, abbreviated)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'week'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activity_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;week&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;distinct&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;weekly_active_users&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_bot&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_test_account&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;users&lt;/code&gt; semantic model in &lt;code&gt;users.yml&lt;/code&gt; binds the underlying &lt;code&gt;dim_users&lt;/code&gt; table to a canonical entity (&lt;code&gt;user_id&lt;/code&gt;), the valid dimensions (&lt;code&gt;week&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;is_bot&lt;/code&gt;), and the available measures (&lt;code&gt;distinct_users&lt;/code&gt;). This YAML is the durable contract.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;weekly_active_users&lt;/code&gt; metric in &lt;code&gt;wau.yml&lt;/code&gt; references the &lt;code&gt;distinct_users&lt;/code&gt; measure and pins the standard filter (&lt;code&gt;is_bot = false&lt;/code&gt;, &lt;code&gt;is_test_account = false&lt;/code&gt;). Every downstream query for this metric inherits the filter — no way to accidentally include bots.&lt;/li&gt;
&lt;li&gt;The text-to-SQL app calls the dbt Semantic Layer JDBC/MCP endpoint with a &lt;em&gt;metric query&lt;/em&gt; — a structured request naming the metric, group-by dimensions, filters, and order. The model does not emit raw SQL; it emits this structured payload.&lt;/li&gt;
&lt;li&gt;The Semantic Layer compiles the metric query into warehouse-native SQL. All the pre-defined joins, filters, and grain semantics are baked in. The compiled SQL that hits Snowflake is deterministic and correct by construction.&lt;/li&gt;
&lt;li&gt;If the user later asks "how many WAU last quarter by plan?", the model emits a different metric query (same metric, different group-by), and the Semantic Layer compiles a different SQL. The metric definition never changes; the grouping does.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Raw-table grounding (before)&lt;/th&gt;
&lt;th&gt;Semantic-layer grounding (after)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model writes SQL joining 4 tables&lt;/td&gt;
&lt;td&gt;Model calls metric with dimensions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter &lt;code&gt;is_bot = false&lt;/code&gt; sometimes forgotten&lt;/td&gt;
&lt;td&gt;Filter always applied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grain (week vs day) ambiguous&lt;/td&gt;
&lt;td&gt;Grain locked to week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 different &lt;code&gt;MAU&lt;/code&gt; numbers on 3 dashboards&lt;/td&gt;
&lt;td&gt;1 number, always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric change = touch 8 SQL files&lt;/td&gt;
&lt;td&gt;Metric change = 1 YAML PR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any metric worth naming, define it in the semantic layer once. Every downstream text-to-SQL query grounds on the metric name, not on raw tables. The metric definition is the source of truth; the compiled SQL is derived.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — grounded vs raw output for the same user question
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The clearest way to see semantic-layer grounding pay off is a side-by-side comparison: same question, one prompt with raw-table context, one prompt with metric-first context. Walk through both flows for the question "how many WAU last week by plan?"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Question.&lt;/strong&gt; "How many weekly active users last week, split by plan?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw-table context.&lt;/strong&gt; DDL for &lt;code&gt;dim.users&lt;/code&gt;, &lt;code&gt;fct.sessions&lt;/code&gt;, &lt;code&gt;dim.bot_signatures&lt;/code&gt;, &lt;code&gt;dim.test_accounts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric-first context.&lt;/strong&gt; The &lt;code&gt;weekly_active_users&lt;/code&gt; metric definition + &lt;code&gt;plan&lt;/code&gt; as a valid dimension.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Show the model output for both prompt strategies and compare correctness on 100 stakeholders' behalf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt strategy&lt;/th&gt;
&lt;th&gt;Retriever output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw-table&lt;/td&gt;
&lt;td&gt;4 DDL tables, ~40 columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric-first&lt;/td&gt;
&lt;td&gt;1 metric definition, 4 dimensions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Prompt A — raw-table grounded
User: How many weekly active users last week, split by plan?
Context: [DDL for dim.users, fct.sessions, dim.bot_signatures, dim.test_accounts]

# Model output (representative):
SELECT
    u.plan,
    COUNT(DISTINCT s.user_id) AS wau
FROM   fct.sessions   s
JOIN   dim.users      u ON u.user_id = s.user_id
WHERE  s.started_at &amp;gt;= date_trunc('week', current_date) - INTERVAL '1 week'
  AND  s.started_at &amp;lt;  date_trunc('week', current_date)
GROUP  BY u.plan
ORDER  BY wau DESC;
-- ❌ Missing is_bot filter
-- ❌ Missing is_test_account filter
-- ✓  Grain correct
-- ✓  Group-by correct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Prompt B — metric-first grounded
User: How many weekly active users last week, split by plan?
Context: metric weekly_active_users(measure=distinct_users, filter=is_bot=false AND is_test_account=false)
         dimensions: week, country, plan, is_bot

# Model output (representative):
{
  "metrics":  ["weekly_active_users"],
  "group_by": ["week", "plan"],
  "where":    "{{ Dimension('user__week') }} = date_trunc('week', current_date - 7)",
  "order_by": ["-weekly_active_users"],
  "limit":    100
}

# Compiled by Semantic Layer:
SELECT
    date_trunc('week', u.activity_ts) AS week,
    u.plan                             AS plan,
    COUNT(DISTINCT u.user_id)          AS weekly_active_users
FROM   analytics.dim.users u
WHERE  u.is_bot           = false
  AND  u.is_test_account  = false
  AND  date_trunc('week', u.activity_ts) = date_trunc('week', current_date - 7)
GROUP  BY 1, 2
ORDER  BY 3 DESC
LIMIT 100;
-- ✓ is_bot filter applied
-- ✓ is_test_account filter applied
-- ✓ Grain correct
-- ✓ Group-by correct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prompt A gives the model raw DDL. The model reconstructs the WAU logic every time; without the metric definition, it does not know about &lt;code&gt;is_bot&lt;/code&gt; or &lt;code&gt;is_test_account&lt;/code&gt; filters that live in tribal knowledge. The generated SQL is &lt;em&gt;plausibly right&lt;/em&gt; but silently over-counts.&lt;/li&gt;
&lt;li&gt;Prompt B gives the model a metric definition first. The model's job is dramatically simpler: pick the metric, pick the dimensions, pick the filter. The Semantic Layer handles the join, the filter, the grain.&lt;/li&gt;
&lt;li&gt;The compiled SQL is deterministic and correct by construction. Every question that uses &lt;code&gt;weekly_active_users&lt;/code&gt; gets the same filters. Every question that groups by &lt;code&gt;plan&lt;/code&gt; gets the same plan column.&lt;/li&gt;
&lt;li&gt;On a 100-prompt sample of WAU-adjacent questions, raw-table grounding got 62 percent right, metric-first grounding got 96 percent right. The 34-point delta is the value semantic-layer grounding creates on this metric alone.&lt;/li&gt;
&lt;li&gt;The failure modes of metric-first grounding are different: if the user asks a question the metric cannot answer (e.g. "how many WAU by hour?", but the metric grain is week), the compiler returns a validation error rather than a wrong answer. That's a much better failure mode.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Raw-table grounding&lt;/th&gt;
&lt;th&gt;Metric-first grounding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Correctness on WAU prompts (n=100)&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bot inclusion bug&lt;/td&gt;
&lt;td&gt;frequent&lt;/td&gt;
&lt;td&gt;never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test-account inclusion bug&lt;/td&gt;
&lt;td&gt;frequent&lt;/td&gt;
&lt;td&gt;never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grain confusion&lt;/td&gt;
&lt;td&gt;occasional&lt;/td&gt;
&lt;td&gt;never (fails loudly)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric owner surface area&lt;/td&gt;
&lt;td&gt;8 SQL files&lt;/td&gt;
&lt;td&gt;1 YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For every metric that appears on more than one dashboard, define it in the semantic layer and ground the text-to-SQL prompt on the metric name. The correctness delta is 20-40 points on that metric; the operational simplification is a single source of truth for the definition.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — pick a vendor: dbt vs LookML vs Cube
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The three canonical semantic layers have different sweet spots. Picking the wrong one for your stack costs 3-6 months of migration pain. Walk through the decision for three canonical scenarios.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scenario 1.&lt;/strong&gt; dbt-first analytics team with Snowflake warehouse and no BI standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenario 2.&lt;/strong&gt; Looker shop with 10 years of LookML models, moving into text-to-SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenario 3.&lt;/strong&gt; Product engineering team embedding text-to-SQL in a customer-facing app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Match each scenario to the right semantic layer and justify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Existing stack&lt;/th&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dbt shop, no BI standard&lt;/td&gt;
&lt;td&gt;dbt + Snowflake&lt;/td&gt;
&lt;td&gt;text-to-SQL app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Looker shop&lt;/td&gt;
&lt;td&gt;LookML + Snowflake&lt;/td&gt;
&lt;td&gt;text-to-SQL app + Looker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product embed&lt;/td&gt;
&lt;td&gt;Custom app + Postgres&lt;/td&gt;
&lt;td&gt;LLM-driven app&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Illustrative — pick the semantic layer
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pick_semantic_layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbt_native&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;looker_native&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;embedded_in_app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;looker_native&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LookML — reuse the models you already have; Looker API for grounding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dbt_native&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt Semantic Layer — MetricFlow YAML alongside your dbt project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;embedded_in_app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cube — REST/GraphQL/SQL API tuned for LLM grounding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt Semantic Layer (safe default)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scenario 1 — dbt shop, no BI standard. Pick dbt Semantic Layer. MetricFlow YAML lives beside the dbt models; the JDBC/MCP endpoint is the query surface for the text-to-SQL app. No parallel semantic layer to maintain.&lt;/li&gt;
&lt;li&gt;Scenario 2 — Looker shop. Pick LookML. Ten years of LookML models is a strategic asset; reusing them via the Looker API means the text-to-SQL feature ships without re-defining metrics. Everyone from the analytics team already reads LookML.&lt;/li&gt;
&lt;li&gt;Scenario 3 — product embed. Pick Cube. Its REST/GraphQL/SQL API is the cleanest fit for an LLM app that also drives dashboards inside a customer-facing product. Cube's caching layer is a bonus for high-QPS embedded scenarios.&lt;/li&gt;
&lt;li&gt;Do not mix. Running two semantic layers (dbt Semantic Layer for one team, LookML for another) is a metric-drift factory. Pick one; migrate the others over 6 months if you started with two.&lt;/li&gt;
&lt;li&gt;If none of the three fits (e.g. real-time streaming metrics), consider a light-weight custom metric registry rather than adopting a full vendor. But recognise this is a temporary state; the vendors will absorb the use case within 12 months.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dbt shop, no BI standard&lt;/td&gt;
&lt;td&gt;dbt Semantic Layer&lt;/td&gt;
&lt;td&gt;Zero parallel infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Looker shop&lt;/td&gt;
&lt;td&gt;LookML&lt;/td&gt;
&lt;td&gt;Reuse 10 years of models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product embed&lt;/td&gt;
&lt;td&gt;Cube&lt;/td&gt;
&lt;td&gt;LLM-tuned SQL/REST/GraphQL API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed (bad)&lt;/td&gt;
&lt;td&gt;Pick one, migrate the rest&lt;/td&gt;
&lt;td&gt;Drift is worse than migration cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Match the semantic-layer vendor to the team that owns metric definitions. dbt for dbt-first, LookML for Looker-first, Cube for embed-first. Never run two; drift is the enemy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on semantic-layer selection
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your team runs dbt on Snowflake, uses Looker for enterprise BI, and is about to ship a text-to-SQL Slack bot for the analytics team. Executives want to embed 'ask a question' into the customer app next quarter. Design the semantic-layer strategy — which vendors, what gets defined first, how the text-to-SQL grounding works, and how you protect against metric drift across the two consumers."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using dbt Semantic Layer as the source of truth + LookML as a downstream consumer + Cube for the product embed + a metric-drift monitor
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. dbt Semantic Layer as the canonical source&lt;/span&gt;
&lt;span class="c1"&gt;# metrics/canonical.yml&lt;/span&gt;
&lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;net_revenue_daily&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;simple&lt;/span&gt;
    &lt;span class="na"&gt;type_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;measure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;net_revenue&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Dimension('is_refund')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weekly_active_users&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;simple&lt;/span&gt;
    &lt;span class="na"&gt;type_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;measure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;distinct_users&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Dimension('is_bot')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AND&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Dimension('is_test_account')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enterprise_churn_rate&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ratio&lt;/span&gt;
    &lt;span class="na"&gt;type_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;numerator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enterprise_churned_users&lt;/span&gt;
      &lt;span class="na"&gt;denominator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enterprise_users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 2. LookML consumes dbt metrics via the LookML metric-import feature (or wrapper views)
# models/enterprise.model.lkml
explore: enterprise_dashboard {
  from: net_revenue_daily
  join: weekly_active_users {
    relationship: many_to_one
    sql_on: ${enterprise_dashboard.week} = ${weekly_active_users.week} ;;
  }
  # metrics reference the dbt-compiled tables; no re-definition
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 3. Cube for the product embed — references the same underlying tables&lt;/span&gt;
&lt;span class="c1"&gt;// cube/schema/NetRevenueDaily.js&lt;/span&gt;
&lt;span class="nf"&gt;cube&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NetRevenueDaily&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM analytics.semantic.net_revenue_daily&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;net_revenue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;day&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;   &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;country&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 4. Metric-drift monitor — runs nightly
# tools/metric_drift_check.py
&lt;/span&gt;&lt;span class="n"&gt;CANONICAL_METRICS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;net_revenue_daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weekly_active_users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enterprise_churn_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sample_metric_across_consumers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Query the same metric via each consumer path; expect identical numbers.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;dbt_val&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dbt_sl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;group_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;looker_val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;looker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_look&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saved_look_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;metric_dashboard_id&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;cube_val&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cube&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;measures&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.revenue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;time_dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;day&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dbt_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;looker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;looker_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cube&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cube_val&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CANONICAL_METRICS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sample_metric_across_consumers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metric drift on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source of truth&lt;/td&gt;
&lt;td&gt;dbt Semantic Layer YAML&lt;/td&gt;
&lt;td&gt;one metric definition per business concept&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BI consumer&lt;/td&gt;
&lt;td&gt;LookML &lt;code&gt;from:&lt;/code&gt; dbt metrics&lt;/td&gt;
&lt;td&gt;Looker dashboards read the same numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App consumer&lt;/td&gt;
&lt;td&gt;Cube reading &lt;code&gt;analytics.semantic.*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;product embed reads the same numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-SQL&lt;/td&gt;
&lt;td&gt;Slack bot grounds on dbt SL directly&lt;/td&gt;
&lt;td&gt;LLM prompt gets metric definitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-SQL (product)&lt;/td&gt;
&lt;td&gt;Product bot grounds on Cube's SQL API&lt;/td&gt;
&lt;td&gt;LLM prompt gets Cube view schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric-drift monitor&lt;/td&gt;
&lt;td&gt;Nightly cross-consumer check&lt;/td&gt;
&lt;td&gt;alerts on divergence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After rollout, &lt;code&gt;net_revenue_daily&lt;/code&gt; is defined once in dbt SL and read by Looker, Cube, and the Slack bot. If any downstream consumer somehow computes a different number, the nightly drift monitor pages on-call before an executive spots the mismatch. Every new metric goes through the dbt YAML first; consumers cannot define their own without triggering the drift alert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Grounded metric&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Analytics Slack bot&lt;/td&gt;
&lt;td&gt;dbt Semantic Layer MCP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;net_revenue_daily&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executive dashboard&lt;/td&gt;
&lt;td&gt;Looker → dbt SL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;net_revenue_daily&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer product embed&lt;/td&gt;
&lt;td&gt;Cube → dbt-compiled tables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;net_revenue_daily&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nightly drift monitor&lt;/td&gt;
&lt;td&gt;queries all three&lt;/td&gt;
&lt;td&gt;should always agree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric definition owner&lt;/td&gt;
&lt;td&gt;analytics-eng team&lt;/td&gt;
&lt;td&gt;1 YAML per metric&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;dbt Semantic Layer as source of truth&lt;/strong&gt;&lt;/strong&gt; — MetricFlow YAML lives beside the dbt models, is versioned, is PR-reviewed, and has a queryable endpoint. Every downstream consumer reads &lt;em&gt;through&lt;/em&gt; this layer, not around it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;LookML as downstream, not competitor&lt;/strong&gt;&lt;/strong&gt; — LookML imports the dbt-compiled metric tables via &lt;code&gt;from:&lt;/code&gt;. The BI dashboards are enriched by 10 years of LookML knowledge without duplicating metric definitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cube for the product embed&lt;/strong&gt;&lt;/strong&gt; — Cube's SQL/REST/GraphQL API is the cleanest fit for LLM-driven apps. It reads from the same physical tables &lt;code&gt;analytics.semantic.*&lt;/code&gt; that dbt produces, so the metric numbers are identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Text-to-SQL grounds on the closest semantic surface&lt;/strong&gt;&lt;/strong&gt; — Slack bot grounds on dbt SL directly; product bot grounds on Cube. Neither reads raw tables when a metric exists. The retriever prefers metric definitions in ranking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Nightly drift monitor&lt;/strong&gt;&lt;/strong&gt; — the safety net. Runs the same metric query through all three consumers; alerts on any divergence. In steady state it is silent; when it fires, it catches a real drift before users do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one metric-owner team (analytics-eng), 20-40 metrics defined in year one, a nightly cross-consumer drift check, and a semantic-layer-aware retriever in each text-to-SQL surface. The eliminated cost is the "revenue is $12.3M or $12.7M depending on the dashboard" war room, plus the model's freedom to redefine metrics on every prompt. Net O(1) per metric definition; O(consumers) for the drift monitor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — aggregation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL aggregation problems on metric definitions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/aggregation/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Design&lt;/span&gt;
&lt;span&gt;Topic — design&lt;/span&gt;
&lt;strong&gt;Design problems on semantic-layer architectures&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. The production loop — logs → eval → retrain → ship
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Every failure becomes tomorrow's gold prompt — the loop compounds; a static prompt-and-model decays
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the text-to-SQL production loop is the four-stage feedback cycle where every user request is logged with full context, human labellers promote failures to gold, the eval harness scores prompt-template and model changes offline, and a canary release gates any shipped change on live traffic — so the system compounds week over week rather than decaying as schema evolves, users get more ambitious, and new metrics land&lt;/strong&gt;. The teams that ship without the loop get worse; the teams that ship with the loop get better.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnpe8mek1x320420ocbf7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnpe8mek1x320420ocbf7.jpeg" alt="Iconographic production-loop diagram — a four-stage feedback loop with arrows connecting logs, labels, eval and ship, ringed around a central prompt-and-model card." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four loop stages.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capture.&lt;/strong&gt; Every request logs a full trace: &lt;code&gt;(request_id, user_id, timestamp, nl_question, retrieved_schema, retrieved_metrics, prompt_final, generated_sql, guardrail_verdicts[], executed_result_hash, latency_ms, user_feedback)&lt;/code&gt;. The trace is the raw material for every downstream stage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label.&lt;/strong&gt; A weekly triage rotation reviews the escalated traces (thumbs-down, guardrail-rejected, high-latency, high-cost) and either (a) fixes the SQL by hand and promotes to gold, (b) files a schema-linking or semantic-layer bug, or (c) closes as user error with no gold row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluate.&lt;/strong&gt; Prompt-template changes, model version bumps, and semantic-layer additions run through the offline harness (H2 §2). CI enforces the regression gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship.&lt;/strong&gt; Any change that passes the harness gate goes to canary: 10 percent of traffic for one week, watched by live-quality metrics. If canary is clean, ramp to 100 percent; if canary regresses on any live metric, roll back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The feedback-labelling contract.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicit feedback.&lt;/strong&gt; Thumbs up / thumbs down on every rendered result. Low friction (single click) so users actually give it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implicit feedback.&lt;/strong&gt; Did the user copy the SQL? Did they re-ask a rephrased question within 30 seconds (a failure signal)? Did they eventually give up and go to the dashboard?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corrected SQL.&lt;/strong&gt; For thumbs-down traces, the triage rotation writes the &lt;em&gt;correct&lt;/em&gt; SQL. This is the gold-set factory: escalated failure in, gold prompt out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gold promotion.&lt;/strong&gt; Once corrected SQL is validated by a second analyst, the trace becomes a new gold row. The gold set grows by 20-40 prompts per week in a busy deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prompt tuning vs fine-tuning vs schema-embedding refresh — three different loops at three different cadences.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt tuning.&lt;/strong&gt; Editing the prompt template (instructions, examples, formatting). Cadence: 1-3 times per week. Cheap, fast, low-risk. Every change runs through the harness before ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tuning.&lt;/strong&gt; Training a model on your (question, sql) corpus. Cadence: 1-2 times per quarter. Expensive, slow, higher-risk (regression on out-of-domain prompts). Justify only when prompt tuning has plateaued.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema-embedding refresh.&lt;/strong&gt; Re-embedding the table + metric corpus after schema evolution. Cadence: nightly (incremental) or weekly (full). Automatic; watched for retriever regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Canary release — the shipping mechanism.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Traffic split.&lt;/strong&gt; 10 percent of user requests go to the candidate (new prompt template or model). 90 percent stay on control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live metrics.&lt;/strong&gt; Per-cohort thumbs-up rate, guardrail-reject rate, p95 latency, mean cost per query. Alerts fire on any cohort divergence beyond 2 sigma.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration.&lt;/strong&gt; 5-7 days minimum, so weekday / weekend / peak-hour variance is captured. Shorter canaries let regressions ride.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ramp.&lt;/strong&gt; Clean canary → 25 percent → 50 percent → 100 percent over 3 days. Never jump from 10 to 100.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on the production loop.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What do you log per request?" — required answer: full trace including retrieved context, generated SQL, guardrail verdicts, execution result hash, user feedback.&lt;/li&gt;
&lt;li&gt;"How do failures become gold prompts?" — weekly triage rotation writes corrected SQL; second analyst validates; promote to gold.&lt;/li&gt;
&lt;li&gt;"How often do you refresh the model?" — prompt tuning weekly, fine-tuning quarterly, embedding refresh nightly.&lt;/li&gt;
&lt;li&gt;"How do you ship changes safely?" — offline harness gate + 10-percent canary for a week + gradual ramp.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the request-trace log schema
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Every request emits one row into a &lt;code&gt;text_to_sql_traces&lt;/code&gt; table. The schema is denormalised on purpose — grepping traces for "when did the model start hallucinating this column?" needs every field in one place.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PK.&lt;/strong&gt; &lt;code&gt;request_id UUID&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User.&lt;/strong&gt; &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;session_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs.&lt;/strong&gt; &lt;code&gt;nl_question&lt;/code&gt;, &lt;code&gt;retrieved_tables[]&lt;/code&gt;, &lt;code&gt;retrieved_metrics[]&lt;/code&gt;, &lt;code&gt;prompt_final TEXT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outputs.&lt;/strong&gt; &lt;code&gt;generated_sql&lt;/code&gt;, &lt;code&gt;guardrail_verdicts JSONB&lt;/code&gt;, &lt;code&gt;executed_result_hash&lt;/code&gt;, &lt;code&gt;row_count&lt;/code&gt;, &lt;code&gt;bytes_scanned&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback.&lt;/strong&gt; &lt;code&gt;user_feedback&lt;/code&gt; (&lt;code&gt;thumbs_up&lt;/code&gt;, &lt;code&gt;thumbs_down&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;), &lt;code&gt;feedback_at&lt;/code&gt;, &lt;code&gt;corrected_sql TEXT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perf.&lt;/strong&gt; &lt;code&gt;latency_ms&lt;/code&gt;, &lt;code&gt;cost_credits&lt;/code&gt;, &lt;code&gt;model_version&lt;/code&gt;, &lt;code&gt;prompt_template_version&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the log schema and the log-emit function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The trace table (in the same warehouse as the app, or a dedicated logs DB)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_to_sql_traces&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;request_id&lt;/span&gt;                &lt;span class="n"&gt;UUID&lt;/span&gt;          &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;                   &lt;span class="nb"&gt;BIGINT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tenant_id&lt;/span&gt;                 &lt;span class="nb"&gt;BIGINT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;                &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;                &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

    &lt;span class="n"&gt;nl_question&lt;/span&gt;               &lt;span class="nb"&gt;TEXT&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;retrieved_tables&lt;/span&gt;          &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;retrieved_metrics&lt;/span&gt;         &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prompt_template_version&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prompt_final&lt;/span&gt;              &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="n"&gt;generated_sql&lt;/span&gt;             &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;guardrail_verdicts&lt;/span&gt;        &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;executed_result_hash&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;row_count&lt;/span&gt;                 &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bytes_scanned&lt;/span&gt;             &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;latency_ms&lt;/span&gt;                &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cost_credits&lt;/span&gt;              &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;model_version&lt;/span&gt;             &lt;span class="nb"&gt;TEXT&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="n"&gt;user_feedback&lt;/span&gt;             &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'up', 'down', null&lt;/span&gt;
    &lt;span class="n"&gt;feedback_at&lt;/span&gt;               &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;corrected_sql&lt;/span&gt;             &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;triage_status&lt;/span&gt;             &lt;span class="nb"&gt;TEXT&lt;/span&gt;           &lt;span class="c1"&gt;-- 'pending', 'promoted', 'closed'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_traces_feedback&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_to_sql_traces&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_feedback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_traces_status&lt;/span&gt;   &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_to_sql_traces&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;triage_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/log_trace.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nl_question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;retrieved_tables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retrieved_metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;generated_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guardrail_trace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;result_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_credits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;prompt_template_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            INSERT INTO analytics.text_to_sql_traces
            (request_id, user_id, tenant_id, session_id, nl_question,
             retrieved_tables, retrieved_metrics, prompt_template_version,
             prompt_final, generated_sql, guardrail_verdicts,
             executed_result_hash, latency_ms, cost_credits, model_version,
             triage_status)
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nl_question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;retrieved_tables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retrieved_metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt_template_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;prompt_final&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generated_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guardrail_trace&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="n"&gt;result_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_credits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_version&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;request_id UUID&lt;/code&gt; is the durable handle. Every downstream artifact (feedback, triage decision, gold promotion) references this UUID so the full trace stays reconstructable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retrieved_tables[]&lt;/code&gt; and &lt;code&gt;retrieved_metrics[]&lt;/code&gt; capture what the schema-linking retriever picked. If the model hallucinates a column, the trace shows whether the retriever missed the relevant table (retriever bug) or the model went off-script (prompt bug).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prompt_template_version&lt;/code&gt; and &lt;code&gt;model_version&lt;/code&gt; are the two dimensions along which quality can regress. Every trace is tagged, so post-hoc analysis can slice quality by version.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;guardrail_verdicts JSONB&lt;/code&gt; stores the per-lane verdict from H2 §3. Post-hoc: "show me traces where lane 4 rejected but the user still gave a thumbs-down" surfaces situations where the cost-gate cap is too aggressive.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;triage_status&lt;/code&gt; starts &lt;code&gt;pending&lt;/code&gt;; the weekly triage rotation moves it to &lt;code&gt;promoted&lt;/code&gt; (added to gold) or &lt;code&gt;closed&lt;/code&gt; (user error or duplicate of existing gold). This turns triage into a queryable pipeline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query use case&lt;/th&gt;
&lt;th&gt;Query pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Find all thumbs-down last week&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WHERE user_feedback='down' AND feedback_at &amp;gt;= now() - INTERVAL '7 days'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find pending triage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WHERE triage_status='pending' AND user_feedback='down'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slice quality by model version&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT model_version, avg(CASE user_feedback WHEN 'up' THEN 1 ELSE 0 END) FROM traces GROUP BY 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detect prompt-template regression&lt;/td&gt;
&lt;td&gt;&lt;code&gt;... GROUP BY prompt_template_version&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Log full traces on every request into a queryable warehouse table. Include retrieved context (not just the final SQL) so you can distinguish retriever bugs from prompt bugs. Index on &lt;code&gt;user_feedback&lt;/code&gt; and &lt;code&gt;triage_status&lt;/code&gt; for the on-call and triage queries.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the CI-gated canary release
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Every prompt-template change (or model bump, or semantic-layer addition) goes through the same rollout: offline harness gate → 10 percent canary for 5-7 days → gradual ramp → 100 percent. The rollout mechanism is a feature-flag on &lt;code&gt;prompt_template_version&lt;/code&gt; and &lt;code&gt;model_version&lt;/code&gt; per request.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feature flag.&lt;/strong&gt; LaunchDarkly, Statsig, or a home-grown flag service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic split.&lt;/strong&gt; 10 percent → 25 percent → 50 percent → 100 percent over 3 days after canary passes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live-quality metrics.&lt;/strong&gt; thumbs-up rate, guardrail-reject rate, p95 latency, cost per query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback trigger.&lt;/strong&gt; Any metric divergence beyond 2 sigma vs control cohort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the canary rollout logic and the divergence alert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# rollout/canary.py — per-request cohort assignment
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statsig&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;statsig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StatsigUser&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve_prompt_and_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StatsigUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Two independent experiments — prompt template + model
&lt;/span&gt;    &lt;span class="n"&gt;prompt_variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statsig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text_to_sql_prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v2.3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;model_variant&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statsig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text_to_sql_model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4.1-2026-05&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prompt_variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_variant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- rollout/divergence_check.sql — nightly canary vs control comparison&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;cohorts&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;prompt_template_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;model_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;user_feedback&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'up'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'down'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;thumb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cost_credits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guardrail_verdicts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'lane4_verdict'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'reject_over_budget'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cost_reject&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_to_sql_traces&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;user_feedback&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="k"&gt;rollup&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;prompt_template_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thumb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;thumbs_up_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;percentile_cont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WITHIN&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p95_latency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost_credits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost_reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cost_reject_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cohorts&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;rollup&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;prompt_template_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;resolve_prompt_and_model&lt;/code&gt; reads the per-user cohort assignment from Statsig. A stable-hash bucketing on &lt;code&gt;user_id&lt;/code&gt; means a given user sees the same variant on repeat requests, which is essential for measurable per-user quality.&lt;/li&gt;
&lt;li&gt;The Statsig experiment starts at 10 percent for the new variant; the rest fall through to the current default. Ramp-up is a config change in Statsig, not a code deploy.&lt;/li&gt;
&lt;li&gt;The divergence check runs nightly. For each &lt;code&gt;(prompt_template_version, model_version)&lt;/code&gt; cohort with at least ~500 requests, compute thumbs-up rate, p95 latency, average cost, and cost-reject rate.&lt;/li&gt;
&lt;li&gt;On-call reviews the divergence report weekly during canary. Any 2-sigma-worse move on the candidate cohort blocks ramp-up. Any 2-sigma-better move accelerates ramp-up (with the same reviewer approval).&lt;/li&gt;
&lt;li&gt;Rollback is a Statsig config change: set the candidate's traffic weight to 0 percent. In flight requests complete; new requests all route to control. Time-to-rollback: seconds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;prompt_template_version&lt;/th&gt;
&lt;th&gt;model_version&lt;/th&gt;
&lt;th&gt;thumbs_up_rate&lt;/th&gt;
&lt;th&gt;p95_latency&lt;/th&gt;
&lt;th&gt;avg_cost&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v2.3 (control)&lt;/td&gt;
&lt;td&gt;gpt-4.1-2026-05&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;td&gt;4600 ms&lt;/td&gt;
&lt;td&gt;$0.048&lt;/td&gt;
&lt;td&gt;41,203&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2.4 (canary)&lt;/td&gt;
&lt;td&gt;gpt-4.1-2026-05&lt;/td&gt;
&lt;td&gt;0.87&lt;/td&gt;
&lt;td&gt;4400 ms&lt;/td&gt;
&lt;td&gt;$0.046&lt;/td&gt;
&lt;td&gt;4,512&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every prompt-template change, model bump, and semantic-layer addition ships through 10 percent canary for 5-7 days, gated on live-quality metrics. Never bypass the canary — a change that passes the offline harness can still regress in ways the harness didn't cover.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on the production loop
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design the production loop for a text-to-SQL feature that has been live for 3 months and stalled at 82 percent user satisfaction. Cover the log-trace schema, the weekly triage rotation, the promotion-to-gold contract, the harness-gated release, and the canary rollout. How would you decide when to invest in fine-tuning vs when to keep iterating on prompts?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using traced requests + weekly triage + gold promotion + harness gate + 10-percent canary + prompt-tuning-before-fine-tuning heuristic
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# loop/weekly_triage.py — the triage rotation
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_pending_traces&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT request_id, nl_question, generated_sql,
                   retrieved_tables, retrieved_metrics,
                   guardrail_verdicts, user_feedback
            FROM   analytics.text_to_sql_traces
            WHERE  triage_status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
              AND  user_feedback = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;down&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
              AND  created_at &amp;gt;= %s
            ORDER  BY created_at DESC
            LIMIT  100
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;triage_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Human writes corrected SQL, second reviewer validates.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nl_question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model wrote:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generated_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;corrected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;corrected SQL (or &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;skip&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;corrected&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Validate against harness — must execute and produce a plausible result
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;corrected_validates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;corrected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="nf"&gt;promote_to_gold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nl_question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;corrected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promoted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;promote_to_gold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write a new row into gold_set.jsonl.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;result_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_and_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gold_set.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gold_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source:triage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# loop/decision.py — when to prompt-tune vs fine-tune
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;next_investment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recent_gold_promotions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;prompt_change_uplift_pp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;fine_tune_est_uplift_pp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;fine_tune_cost_weeks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prompt_change_uplift_pp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keep prompt-tuning — you have room&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;recent_gold_promotions&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grow the gold set first — you can&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t measure fine-tune uplift&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fine_tune_est_uplift_pp&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;fine_tune_cost_weeks&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt-tune; fine-tune ROI too low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invest in fine-tuning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Loop stage&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Capture&lt;/td&gt;
&lt;td&gt;request trace → warehouse table&lt;/td&gt;
&lt;td&gt;every request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Label&lt;/td&gt;
&lt;td&gt;weekly triage rotation&lt;/td&gt;
&lt;td&gt;1x/week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promote&lt;/td&gt;
&lt;td&gt;corrected SQL → gold_set.jsonl&lt;/td&gt;
&lt;td&gt;continuous within triage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluate&lt;/td&gt;
&lt;td&gt;harness on prompt PR / model bump&lt;/td&gt;
&lt;td&gt;CI on every relevant PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canary ship&lt;/td&gt;
&lt;td&gt;10% cohort for 5-7 days&lt;/td&gt;
&lt;td&gt;per change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ramp&lt;/td&gt;
&lt;td&gt;10 → 25 → 50 → 100%&lt;/td&gt;
&lt;td&gt;3 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback&lt;/td&gt;
&lt;td&gt;Statsig weight → 0%&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After 4 weeks of running the loop on a stalled 82 percent system, execution accuracy on the gold set climbs from 82 to 87 percent driven purely by prompt-template iteration and gold-set expansion. Only after prompt tuning plateaus (weekly uplift under 0.5 pp) does the team consider fine-tuning, and only if the fine-tune ROI (estimated uplift divided by weeks of engineering) beats 0.5 pp/week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Week&lt;/th&gt;
&lt;th&gt;Gold set size&lt;/th&gt;
&lt;th&gt;Prompt template&lt;/th&gt;
&lt;th&gt;Exec acc&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;220&lt;/td&gt;
&lt;td&gt;v1.0&lt;/td&gt;
&lt;td&gt;82%&lt;/td&gt;
&lt;td&gt;stalled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Week 1&lt;/td&gt;
&lt;td&gt;260&lt;/td&gt;
&lt;td&gt;v1.1&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;td&gt;few-shot examples added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Week 2&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;v1.2&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;metric-first instruction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Week 3&lt;/td&gt;
&lt;td&gt;340&lt;/td&gt;
&lt;td&gt;v1.3&lt;/td&gt;
&lt;td&gt;86%&lt;/td&gt;
&lt;td&gt;retriever K=12 → 15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Week 4&lt;/td&gt;
&lt;td&gt;380&lt;/td&gt;
&lt;td&gt;v1.4&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;td&gt;canary passed, ramped 100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Full-trace logging&lt;/strong&gt;&lt;/strong&gt; — capture prompt, retrieved context, SQL, guardrail verdicts, execution hash, and feedback in one row. Every subsequent stage grep-queries this table; skimping on fields is skimping on debuggability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Weekly triage rotation&lt;/strong&gt;&lt;/strong&gt; — humans in the loop. Every thumbs-down that reveals a real gap becomes a gold prompt within the same week. The gold set grows organically toward what users actually ask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Prompt-tuning before fine-tuning&lt;/strong&gt;&lt;/strong&gt; — prompt changes are cheap and reversible; fine-tuning is expensive and hard to reverse. The heuristic "keep prompt-tuning while it yields at least 1 pp per change" prevents premature model investment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Harness-gated ship + canary&lt;/strong&gt;&lt;/strong&gt; — offline harness catches obvious regressions; canary catches live-behavior regressions the harness didn't cover. Both are non-negotiable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one triage-rotation engineer at ~30 percent time; one CI harness run per relevant PR at ~$5; one canary experiment per change at zero incremental cost. The eliminated cost is quality decay in production and the executive-visible regressions that decay causes. Net O(1) per change; O(traffic) per canary day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — window-functions&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL window-function problems for trace analysis&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;API Integration&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — api-integration&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;API-integration problems for LLM feedback loops&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/api-integration" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — text-to-SQL production recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The four failure classes.&lt;/strong&gt; Every &lt;code&gt;text-to-sql in production&lt;/code&gt; failure sorts into schema hallucination (invented tables/columns), join drift (wrong keys or wrong join type), metric mis-definition (wrong aggregation or missing filter), or unsafe execution (destructive DML, runaway scan). Every mitigation is on-axis: schema-link retriever kills hallucination; join graph or semantic-layer joins kill drift; semantic-layer metric grounding kills mis-definition; four-lane guardrails kill unsafe execution. Miss any axis and the failure class stays alive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The eval-harness ship gate.&lt;/strong&gt; Execution accuracy on a 500-prompt private gold set is the release gate; exact-match is a lower bound; semantic-diff via &lt;code&gt;EXPLAIN&lt;/code&gt; plan hashing is the cheap pre-filter. Cascade: exact-match (microseconds) → plan-hash (milliseconds) → execution (seconds + $) so the average prompt resolves in a cheap tier. Zero-copy Snowflake &lt;code&gt;CLONE&lt;/code&gt; snapshots make results deterministic; a dedicated X-SMALL warehouse with a resource-monitor cap keeps a full harness run under $5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The four guardrail lanes.&lt;/strong&gt; (1) Schema-linking retriever prunes prompt to 15 tables + FK neighbours (drops hallucination 70-90%). (2) &lt;code&gt;SQLGlot&lt;/code&gt; parse + identifier validation against a cached &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt; snapshot rejects any unknown table or column. (3) Warehouse dry-run cost estimator (&lt;code&gt;EXPLAIN USING JSON&lt;/code&gt; in Snowflake, &lt;code&gt;dryRun&lt;/code&gt; in BigQuery) rejects any query above the per-tenant scanned-bytes budget. (4) Safety allow-list — only &lt;code&gt;SELECT&lt;/code&gt;/&lt;code&gt;WITH&lt;/code&gt; on a read-only role.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The read-only + row-limit + column-mask trio.&lt;/strong&gt; Service account has &lt;code&gt;SELECT&lt;/code&gt; only (no &lt;code&gt;INSERT&lt;/code&gt;/&lt;code&gt;UPDATE&lt;/code&gt;/&lt;code&gt;DELETE&lt;/code&gt;/&lt;code&gt;CREATE&lt;/code&gt;/&lt;code&gt;DROP&lt;/code&gt;). Every generated SQL is rewritten with a mandatory &lt;code&gt;LIMIT N&lt;/code&gt;. Every PII column has a warehouse-level &lt;code&gt;MASKING POLICY&lt;/code&gt;. Belt-and-braces: even if a guardrail lane misfires, the warehouse's own permission check and mask enforcement provides a second defense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema-linking retriever recipe.&lt;/strong&gt; One embedded document per table (&lt;code&gt;table_name&lt;/code&gt; + &lt;code&gt;description&lt;/code&gt; + &lt;code&gt;columns[]&lt;/code&gt;); pgvector or Qdrant index; top-K by cosine; expand one hop along foreign keys; cap at 15 tables and 300 columns; format as annotated &lt;code&gt;CREATE TABLE&lt;/code&gt; DDL for the prompt. Refresh embeddings on schema-evolution events; cache popular questions for zero-latency repeats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLGlot parse validator template.&lt;/strong&gt; &lt;code&gt;tree = sqlglot.parse_one(sql, read="snowflake")&lt;/code&gt; → &lt;code&gt;tree.find_all(exp.Table)&lt;/code&gt; to enumerate tables → &lt;code&gt;tree.find_all(exp.Column)&lt;/code&gt; to enumerate columns → look up in a cached &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt; snapshot. Distinguish &lt;code&gt;unknown_table&lt;/code&gt;, &lt;code&gt;unknown_column&lt;/code&gt;, &lt;code&gt;ambiguous_column&lt;/code&gt;. Feed each failure kind back to the model as a targeted retry hint. Latency budget: 5 ms end-to-end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic-layer contract.&lt;/strong&gt; Metrics defined once in dbt Semantic Layer (or LookML, or Cube). Each metric ships &lt;code&gt;measure&lt;/code&gt; + &lt;code&gt;dimensions&lt;/code&gt; + &lt;code&gt;filter&lt;/code&gt; + &lt;code&gt;grain&lt;/code&gt;. Text-to-SQL retriever indexes metric definitions alongside table docs. Prompt template says "PREFER defined metrics over raw tables." Compiled SQL from the semantic layer inherits joins/filters/grain — the model never re-derives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor decision matrix.&lt;/strong&gt; dbt-first team → dbt Semantic Layer (MetricFlow YAML). Looker-first team → LookML (reuse the models you have). Product-embed team → Cube (LLM-tuned SQL/REST/GraphQL API). Never run two — metric drift is worse than migration cost. Add a nightly cross-consumer drift monitor when the same metric surfaces through multiple layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request-trace log schema.&lt;/strong&gt; &lt;code&gt;text_to_sql_traces(request_id, user_id, tenant_id, nl_question, retrieved_tables[], retrieved_metrics[], prompt_template_version, generated_sql, guardrail_verdicts JSONB, executed_result_hash, row_count, bytes_scanned, latency_ms, cost_credits, model_version, user_feedback, corrected_sql, triage_status)&lt;/code&gt;. Index on &lt;code&gt;(user_feedback, created_at)&lt;/code&gt; and &lt;code&gt;(triage_status, created_at)&lt;/code&gt; for the two hot query paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly triage rotation.&lt;/strong&gt; Load pending thumbs-down traces → human writes corrected SQL → second analyst validates → promote to gold with new &lt;code&gt;expected_hash&lt;/code&gt;. Target 20-40 gold promotions per week in busy deployments. Every failed prompt becomes tomorrow's regression test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt-tune vs fine-tune heuristic.&lt;/strong&gt; Keep prompt-tuning while per-change uplift stays above 1 pp. Fine-tune only when (a) prompt-tuning has plateaued for 2+ weeks, (b) gold set exceeds 500 prompts, and (c) fine-tune estimated uplift beats 0.5 pp per engineer-week. Fine-tuning is expensive to reverse; prompt tuning is a config change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI-gated canary release.&lt;/strong&gt; Every change (prompt template, model bump, semantic-layer addition) runs through the harness with a &lt;code&gt;-2 pp&lt;/code&gt; regression gate. On pass, ship to 10 percent traffic via Statsig / LaunchDarkly for 5-7 days, watched by thumbs-up rate + guardrail-reject rate + p95 latency + cost per query. Clean canary → ramp 10 → 25 → 50 → 100 over 3 days. Rollback is a config change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost bounding recipe.&lt;/strong&gt; Per-query cap: 5 GB scanned (Snowflake). Per-tenant daily cap: 500 GB scanned. Warehouse resource monitor: hard-stop at $500/day on the bot warehouse. &lt;code&gt;STATEMENT_TIMEOUT_IN_SECONDS = 60&lt;/code&gt; on the bot session. Result-set client cap: 10,000 rows. Combined, these bound any single query at $0.50 and any tenant-day at $50 in a well-tuned setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure semantics reminder.&lt;/strong&gt; Model unavailable → request fails fast with "model unavailable, try again." Guardrail rejection → return actionable message ("your query would scan 47 GB; try adding a date filter"). Persistent hallucination (3 retries) → escalate with "I couldn't answer that reliably" — the honest failure mode. Never let a low-confidence answer render as if it were high-confidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is text-to-SQL in production and why is it different from a demo?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;text-to-sql in production&lt;/code&gt; is a full analytics feature that lets end users ask questions in natural language against a real warehouse, with correctness, safety, cost, and latency guarantees appropriate for a customer-facing or internal-employee product. It differs from a demo in every dimension that matters: the demo uses a toy schema (5 tables); production hits a 400-table warehouse. The demo trusts the model output; production runs every candidate SQL through a four-lane guardrail stack (schema linking, parse validation, dry-run cost, safety allow-list). The demo has no eval; production gates every change through a private gold set of 200-2000 &lt;code&gt;(question, gold_sql, expected_result)&lt;/code&gt; triples plus a canary release. The demo uses raw DDL; production grounds on a semantic layer (dbt Semantic Layer, LookML, Cube) so metrics like &lt;code&gt;revenue&lt;/code&gt; and &lt;code&gt;MAU&lt;/code&gt; are defined once and never re-derived by the model. The demo is a screenshot; production is a system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Execution accuracy vs exact-match — which is the right metric for my team?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Execution accuracy&lt;/strong&gt; is the ship gate; &lt;strong&gt;exact-match&lt;/strong&gt; is a diagnostic. Execution accuracy runs the candidate SQL and the gold SQL against a frozen snapshot of the warehouse, canonicalises both result sets (sort rows, sort columns, cast types), and compares them — this measures what the user actually experiences. Exact-match canonicalises the SQL strings (whitespace, aliases, keyword casing) and compares them — fast, cheap, but blind to semantically equivalent rewrites (&lt;code&gt;SELECT a, b FROM t&lt;/code&gt; vs &lt;code&gt;SELECT b, a FROM t&lt;/code&gt; are exact-match different but result-set identical). Ship on execution accuracy on a private gold set (target 85 percent for internal analytics, 90 percent for customer-facing self-serve). Track exact-match as a lower bound and semantic-diff via &lt;code&gt;EXPLAIN&lt;/code&gt; plan hashing as a cheap pre-filter, but never let exact-match alone decide releases. Every text-to-SQL paper and every serious production deployment converges on this hierarchy: exact-match ≤ semantic-diff ≤ execution accuracy, with execution accuracy the release contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  What guardrails must every text-to-SQL system ship on day one?
&lt;/h3&gt;

&lt;p&gt;Four independent lanes, non-negotiable. &lt;strong&gt;Schema linking&lt;/strong&gt; — an embedding-based retriever that prunes the prompt to 5-15 relevant tables plus their foreign-key neighbours, so the model does not hallucinate columns that aren't in the prompt. &lt;strong&gt;Parse validation&lt;/strong&gt; — &lt;code&gt;SQLGlot&lt;/code&gt; parses the generated SQL, enumerates every table and column reference, and checks each against a cached &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt; snapshot; unknown identifiers are rejected before the warehouse sees the query. &lt;strong&gt;Warehouse dry-run cost&lt;/strong&gt; — use the native estimator (&lt;code&gt;EXPLAIN USING JSON&lt;/code&gt; in Snowflake, &lt;code&gt;dryRun=True&lt;/code&gt; in BigQuery, &lt;code&gt;EXPLAIN COST&lt;/code&gt; in Databricks) to bound scan bytes per query at 5 GB and per-tenant daily at 500 GB. &lt;strong&gt;Safety allow-list&lt;/strong&gt; — only &lt;code&gt;SELECT&lt;/code&gt; / &lt;code&gt;WITH&lt;/code&gt; statements are executed, on a service account with &lt;code&gt;SELECT&lt;/code&gt;-only permissions. Layer these with a mandatory &lt;code&gt;LIMIT N&lt;/code&gt;, warehouse-level &lt;code&gt;MASKING POLICY&lt;/code&gt; on PII columns, and a resource-monitor hard-stop at a daily dollar cap. The lanes are independent so a fault on one does not cascade; the read-only role plus statement-type check is belt-and-braces safety against destructive DML.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semantic layer or raw-schema grounding — which do I need?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Semantic layer&lt;/strong&gt; for every metric worth naming; raw-schema for everything else. Metric mis-definition is one of the four failure classes and it is the failure that most damages user trust — when &lt;code&gt;revenue&lt;/code&gt; disagrees between two dashboards, everyone doubts everything. Defining metrics once in &lt;code&gt;semantic layer grounding&lt;/code&gt; (dbt Semantic Layer, LookML, or Cube) with &lt;code&gt;measure&lt;/code&gt; + &lt;code&gt;dimensions&lt;/code&gt; + &lt;code&gt;filter&lt;/code&gt; + &lt;code&gt;grain&lt;/code&gt;, and prompting the text-to-SQL model to prefer defined metrics over raw tables, collapses this failure class. On a WAU-adjacent 100-prompt sample, raw-table grounding hit 62 percent correctness while metric-first grounding hit 96 percent — a 34-point delta on that one metric alone. For questions that don't map to a defined metric, fall back to raw-schema grounding with a lowered confidence signal to the user. Vendor pick: dbt Semantic Layer for dbt-first teams, LookML for Looker-first teams, Cube for embed-first teams. Never run two — metric drift across parallel semantic layers is worse than the migration cost to consolidate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I evaluate on Spider, BIRD, or a private gold set?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Private gold set&lt;/strong&gt; is the release gate; Spider and BIRD are development-time sanity checks. &lt;code&gt;spider benchmark&lt;/code&gt; (200 databases, cross-domain, tiny schemas mostly under 20 tables) tells you the model can do SQL at all — useful for model selection and prompt-template scaffolding but useless as a production gate against your 400-table Snowflake warehouse. &lt;code&gt;bird-sql&lt;/code&gt; (larger schemas, dirty values, external knowledge) is closer to reality but still not your schema; the gap between "trained on BIRD" and "actually works on your warehouse" is typically 15 percentage points of execution accuracy. Build a private gold set of 200 prompts on day one, 500 within the quarter, and 2000 within the year, targeting your top-10 query patterns across your top-10 tables. Every production incident and every escalated user feedback becomes a new gold row. Version the gold set in git as JSONL; PR-review changes; rotate 20 percent quarterly; keep an adversarial hold-out partition (never used in prompts or fine-tunes) as the overfit detector. Ship-gate release on private-gold execution accuracy; report Spider/BIRD scores for benchmark visibility only.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should I refresh the model, the prompt, and the schema embeddings?
&lt;/h3&gt;

&lt;p&gt;Three loops at three different cadences. &lt;strong&gt;Prompt tuning&lt;/strong&gt; every 1-3 weeks: cheap, fast, low-risk, gated by the eval harness. Most quality gains in a mature deployment come from prompt tuning, not model changes. &lt;strong&gt;Schema-embedding refresh&lt;/strong&gt; nightly (incremental) or weekly (full): triggered by schema-evolution events from your warehouse catalog; watched by a retriever-regression metric on the eval set. &lt;strong&gt;Fine-tuning&lt;/strong&gt; once or twice per quarter, and only after prompt tuning has plateaued (weekly uplift below 0.5 pp for 2+ weeks) AND the gold set exceeds 500 prompts AND the estimated fine-tune uplift beats 0.5 pp per engineer-week. Fine-tuning is expensive to reverse; prompt tuning is a config change. Every change of any kind runs through the offline harness with a &lt;code&gt;-2 pp&lt;/code&gt; regression gate, then a 10-percent canary for 5-7 days, then ramp 10 → 25 → 50 → 100 percent over 3 days. Rollback is a Statsig / LaunchDarkly weight change measured in seconds. Skip the loop for any of these and the system silently decays as your warehouse schema evolves and your users get more ambitious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; for the query patterns text-to-SQL systems must generate reliably — joins, aggregations, window functions, subqueries.&lt;/li&gt;
&lt;li&gt;Rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;SQL-generation practice library →&lt;/a&gt; for text-to-SQL, NL2SQL, and prompt-driven query construction scenarios.&lt;/li&gt;
&lt;li&gt;Sharpen the safety axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;data-validation practice library →&lt;/a&gt; for result-set diff, schema-check, and guardrail patterns.&lt;/li&gt;
&lt;li&gt;Practice the design axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt; for LLM harness, semantic-layer, and eval-loop architecture prompts.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the four-workstream production plan against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lock in text-to-SQL production muscle memory&lt;/h3&gt;

&lt;p&gt;Docs explain the API. PipeCode drills explain the harness — when execution accuracy is the truth vs. when exact-match is a lower bound, when the schema-linking retriever earns its keep, when semantic-layer grounding kills metric drift, when the production loop turns yesterday's incident into tomorrow's regression test. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — pattern-first practice tuned for the production trade-offs senior data platform engineers actually face when shipping text-to-SQL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;Practice SQL-generation problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice design problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Cursor &amp; GitHub Copilot for SQL Engineers: LLM-Native SQL, dbt &amp; Airflow Authoring Patterns</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 31 Jul 2026 04:23:45 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/cursor-github-copilot-for-sql-engineers-llm-native-sql-dbt-airflow-authoring-patterns-3an8</link>
      <guid>https://dev.to/gowthampotureddi/cursor-github-copilot-for-sql-engineers-llm-native-sql-dbt-airflow-authoring-patterns-3an8</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;cursor for sql&lt;/code&gt;&lt;/strong&gt; is the phrase that quietly rewrote the senior data engineer's job description in 2026 — the SQL you author, the dbt model you scaffold, the Airflow DAG you wire up, and the pytest fixture you generate now travel through an LLM-native editor before they ever hit the compile step, and the engineers who have learned to &lt;em&gt;drive&lt;/em&gt; that editor ship two or three times the volume of production-grade SQL that engineers who still hand-type every window function do. The productivity delta is not marginal; it is a step change, and it is measurable in PR throughput, in incident-response time, and in the size of the analytics-engineering roadmap a single senior can realistically own. The editor is no longer a text buffer with syntax highlighting — it is a small autonomous collaborator that reads your repo, remembers your dbt conventions, and drafts your next SELECT, task_group, or dbt macro before your fingers have finished typing the docstring.&lt;/p&gt;

&lt;p&gt;This guide is the senior-DE walkthrough you wished existed the first time an interviewer asked "walk me through how you configure Cursor for a dbt monorepo," or "how do you keep &lt;code&gt;github copilot sql&lt;/code&gt; from hallucinating column names against a schema it hasn't seen," or "how would you use an LLM to co-author an Airflow DAG that ingests from a Snowflake external stage." It walks through the four canonical LLM-authoring patterns — the &lt;code&gt;.cursorrules&lt;/code&gt; file as the single source of truth for repo prompts, the comment-first Copilot inline pattern that pins ghost-text to a natural-language contract, the docstring-first Airflow DAG scaffold that makes the LLM propose sensors and task groups, and the three-lane guardrail (dbt compile → sqlfluff lint → human review) that protects the merge — plus the failure modes that make senior interviewers probe for review discipline more than for prompt cleverness. Each section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ca1h6rszq9qn8oyiv85.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ca1h6rszq9qn8oyiv85.jpeg" alt="PipeCode blog header for Cursor &amp;amp; GitHub Copilot for SQL Engineers — bold white headline 'LLM-Native SQL Authoring' over a split SQL + sparkle hero with four small glyph medallions (rules, autocomplete, dbt-brick, DAG-graph) around a central purple AUTHORING seal on a dark gradient." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt;, rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;SQL-generation practice library →&lt;/a&gt;, and sharpen the pipeline axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why LLM-native editors changed SQL authoring in 2026&lt;/li&gt;
&lt;li&gt;Cursor rules, .cursorrules, and repo-scoped context for SQL / dbt&lt;/li&gt;
&lt;li&gt;GitHub Copilot patterns — inline SQL, dbt macros, Jinja completions&lt;/li&gt;
&lt;li&gt;Airflow DAG authoring with LLM assistants — task groups, sensors, tests&lt;/li&gt;
&lt;li&gt;Guardrails, review discipline &amp;amp; the SQL-engineer's Copilot workflow&lt;/li&gt;
&lt;li&gt;Cheat sheet — LLM-native SQL authoring recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why LLM-native editors changed SQL authoring in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The editor is now the LLM — four axes that separate a productive workflow from a hallucinating one
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;an LLM-native editor is not "autocomplete with more parameters" but a small autonomous collaborator that reads your repo, keeps a rolling context window of your dbt model graph and Airflow DAG shape, drafts code against natural-language prompts you either write explicitly or leak accidentally through comments and docstrings, and either accelerates your senior SQL work by 2–3× or ships subtly-wrong SQL every day depending entirely on whether you have configured repo context, comment discipline, and review guardrails&lt;/strong&gt;. The tools — Cursor, GitHub Copilot, Copilot Chat, Copilot Workspace, Codex CLI, Windsurf — differ in ergonomics, latency, and agentic ability but converge on the same four axes senior interviewers now probe: how the LLM sees your repo, how you frame the prompt, how autonomously it acts, and how the compile / lint / review gates catch what it gets wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four LLM-editor axes interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo context.&lt;/strong&gt; Cursor's &lt;code&gt;.cursorrules&lt;/code&gt; file plus the &lt;code&gt;@Codebase&lt;/code&gt; / &lt;code&gt;@Docs&lt;/code&gt; symbol injection gives the LLM a persistent, versioned repo prompt. GitHub Copilot inline uses the current file plus a handful of recently-opened files as its "context"; Copilot Chat can pull in &lt;code&gt;@workspace&lt;/code&gt; and &lt;code&gt;@github&lt;/code&gt;. The width and freshness of that context window is the single strongest predictor of hallucination rate — narrow context means the LLM invents column names, wide-but-stale context means it drafts against dbt models that were renamed last month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline latency.&lt;/strong&gt; Ghost-text completions must arrive in 200–400 ms or the flow breaks and you fall back to typing. Cursor's small-model tab completion is optimised for this; Copilot's inline model is similar. Copilot Chat and Cursor Composer intentionally trade latency for depth — you accept 3–10 s in exchange for multi-file edits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agentic ability.&lt;/strong&gt; How much does the LLM do without asking? Cursor Composer, Copilot Workspace, and Codex CLI open, read, edit, and run files across the repo; inline Copilot only completes at the caret. Agentic mode is where senior data engineers gain the biggest velocity multiplier — and where the review discipline must be strongest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails.&lt;/strong&gt; Precommit hooks (&lt;code&gt;sqlfluff&lt;/code&gt;, &lt;code&gt;dbt parse&lt;/code&gt;, &lt;code&gt;dbt compile&lt;/code&gt;), CI checks (&lt;code&gt;dbt test&lt;/code&gt;, &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;sqlmesh diff&lt;/code&gt;), and PR review conventions are the only defenses against the LLM's failure modes. An engineer who cannot recite their three-lane guardrail out loud is not senior — they are lucky.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cursor vs GitHub Copilot vs Copilot Chat vs Codex CLI — what each is best at.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cursor.&lt;/strong&gt; VS Code fork with first-class repo context, &lt;code&gt;.cursorrules&lt;/code&gt;, &lt;code&gt;@Codebase&lt;/code&gt;/&lt;code&gt;@Docs&lt;/code&gt; symbol injection, and the Composer agentic mode. Best fit: dbt monorepos, analytics-engineering teams, iterative model refactors that touch a dozen files. The &lt;code&gt;.cursorrules&lt;/code&gt; file is the load-bearing config — set it well and hallucination rates drop measurably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Copilot (inline).&lt;/strong&gt; Fastest ghost-text completion; deep VS Code / JetBrains / Vim integration; excellent for line-by-line SQL and dbt macro authoring. Weak on repo-wide refactors — it doesn't know models it hasn't seen in the current session unless you pull them into scope explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copilot Chat.&lt;/strong&gt; Conversational assistant with &lt;code&gt;@workspace&lt;/code&gt;, &lt;code&gt;@github&lt;/code&gt;, &lt;code&gt;@vscode&lt;/code&gt; symbols. Best fit: "explain this DAG," "generate a test for this dbt model," "refactor this window function." Slower than inline; more thoughtful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex CLI / Claude Code / Aider.&lt;/strong&gt; Terminal-native agents that read, edit, and run shell / SQL / pytest across the repo. Best fit: overnight refactors, one-shot data-quality audits, automated dbt exposure generation. Requires the strongest guardrails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2026 reality — the SQL engineer's editor is now the LLM.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every senior data-engineering interview loop in 2026 that touches "how do you work day-to-day?" includes an LLM-workflow probe. The bare minimum answer is: "I use Cursor + Copilot with a &lt;code&gt;.cursorrules&lt;/code&gt; file, comment-first prompting, and a three-lane guardrail before merge."&lt;/li&gt;
&lt;li&gt;The failure state is unchanged from 2024: the LLM drafts against a schema it hasn't seen, invents a column name, the SQL compiles because dbt didn't parse it, the query silently returns the wrong number, and a dashboard ships with 3% row drift for a week. The fix is not "don't use the LLM" — it's "configure repo context + comment-first prompts + &lt;code&gt;dbt parse&lt;/code&gt; precommit."&lt;/li&gt;
&lt;li&gt;The productivity delta is measurable. Senior teams that have adopted the pattern report 2–3× PR throughput on analytics-engineering work, ~50% reduction in scaffolding time for new Airflow DAGs, and near-instant "explain this legacy CTE" turnaround during incident response.&lt;/li&gt;
&lt;li&gt;The interview signal that separates senior from mid is the ability to name &lt;em&gt;failure modes&lt;/em&gt; by name — hallucinated columns, wrong SQL dialect, outdated Airflow operator API, drift between &lt;code&gt;.cursorrules&lt;/code&gt; and the current dbt convention — not the ability to demo a slick completion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers actually listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you name &lt;strong&gt;&lt;code&gt;.cursorrules&lt;/code&gt; and comment-first prompting&lt;/strong&gt; in the first sentence when asked how you use an LLM? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you say &lt;strong&gt;"I never accept a completion I haven't run through &lt;code&gt;dbt parse&lt;/code&gt; locally"&lt;/strong&gt;? — required answer.&lt;/li&gt;
&lt;li&gt;Do you push back on &lt;strong&gt;"just use Cursor Composer for everything"&lt;/strong&gt; with the review-discipline argument? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name the &lt;strong&gt;three-lane guardrail&lt;/strong&gt; (compile, lint, human review) as your merge invariant? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you describe your LLM workflow as &lt;strong&gt;"pair programming with a very fast but occasionally lying junior"&lt;/strong&gt; rather than as "autocomplete"? — required answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the four-axis LLM-editor comparison for a dbt + Airflow shop
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful artifact for an LLM-workflow interview is a memorised 4×4 comparison table. Every senior conversation about "which editor / which model / which agentic depth" converges on this table within the first ten minutes; having it in your head is what separates a fluent answer from a stumbling one. Walk through building the table for a hypothetical analytics-engineering team maintaining a dbt monorepo (~300 models) plus ~40 Airflow DAGs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo shape.&lt;/strong&gt; &lt;code&gt;analytics-eng/&lt;/code&gt; monorepo with &lt;code&gt;dbt_project.yml&lt;/code&gt;, &lt;code&gt;models/{staging,intermediate,marts}/&lt;/code&gt;, &lt;code&gt;macros/&lt;/code&gt;, &lt;code&gt;analyses/&lt;/code&gt;, &lt;code&gt;snapshots/&lt;/code&gt;; sibling &lt;code&gt;dags/&lt;/code&gt; folder with 40 Airflow DAGs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team.&lt;/strong&gt; 6 analytics engineers + 2 senior data engineers; ~15 PRs/day; ~4 new dbt models/week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraints.&lt;/strong&gt; No prod DB credentials in dev; dbt runs against &lt;code&gt;dev_&amp;lt;user&amp;gt;&lt;/code&gt; schemas; Snowflake target; Airflow 2.9.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goal.&lt;/strong&gt; Ship correct, idiomatic SQL and DAGs 2× faster than the pre-LLM baseline without shipping subtly-wrong SQL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the four-axis LLM-editor comparison for this team and pick the tool + workflow each engineer type should default to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Editor / Mode&lt;/th&gt;
&lt;th&gt;Repo context&lt;/th&gt;
&lt;th&gt;Inline latency&lt;/th&gt;
&lt;th&gt;Agentic depth&lt;/th&gt;
&lt;th&gt;Guardrail cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cursor inline&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.cursorrules&lt;/code&gt; + open files&lt;/td&gt;
&lt;td&gt;200–400 ms&lt;/td&gt;
&lt;td&gt;none (caret only)&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor Composer&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.cursorrules&lt;/code&gt; + &lt;code&gt;@Codebase&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;3–10 s&lt;/td&gt;
&lt;td&gt;multi-file edit&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot inline&lt;/td&gt;
&lt;td&gt;current file + recents&lt;/td&gt;
&lt;td&gt;150–300 ms&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot Chat&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@workspace&lt;/code&gt; + &lt;code&gt;@github&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2–5 s&lt;/td&gt;
&lt;td&gt;single-file suggest&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex CLI / Aider&lt;/td&gt;
&lt;td&gt;full repo (agent-selected)&lt;/td&gt;
&lt;td&gt;5–30 s&lt;/td&gt;
&lt;td&gt;shell + edit + run&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .cursorrules (top of repo)&lt;/span&gt;
&lt;span class="c1"&gt;# This file is the single source of truth for repo prompts.&lt;/span&gt;
&lt;span class="c1"&gt;# Every Cursor completion sees this preamble.&lt;/span&gt;

&lt;span class="s"&gt;You are pair-programming with a senior analytics engineer on the&lt;/span&gt;
&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;analytics-eng/` dbt + Airflow monorepo.&lt;/span&gt;

&lt;span class="na"&gt;Repo conventions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;dbt profile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snowflake_prod / snowflake_dev&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Model layers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging → intermediate → marts&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Naming&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stg_&amp;lt;source&amp;gt;__&amp;lt;table&amp;gt;, int_&amp;lt;domain&amp;gt;__&amp;lt;verb&amp;gt;, dim_/fct_&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Every model MUST have a schema.yml entry with description + tests&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Prefer CTEs over subqueries; one CTE per logical step&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;SQL dialect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Snowflake (use QUALIFY, ILIKE, DATE_TRUNC, etc.)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Never invent column names; ask the user to paste the schema&lt;/span&gt;
    &lt;span class="s"&gt;if the ref() target is not visible in context&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;When authoring DAGs, use TaskFlow API (@dag / @task decorators)&lt;/span&gt;
    &lt;span class="s"&gt;and Airflow &amp;gt;= 2.9 operator names&lt;/span&gt;

&lt;span class="na"&gt;Guardrails you MUST respect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Every SQL suggestion must compile with `dbt parse` (no invalid&lt;/span&gt;
    &lt;span class="s"&gt;Jinja, no unresolved refs)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Every DAG suggestion must import from `airflow.decorators`, not&lt;/span&gt;
    &lt;span class="s"&gt;`airflow.operators.python_operator` (deprecated)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Never write to `prod` targets in generated code&lt;/span&gt;

&lt;span class="s"&gt;When you don't know, say so and ask for the schema.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;.cursorrules&lt;/code&gt; file at the repo root is the single most important config for a Cursor deployment. Every completion — inline and Composer — sees this preamble injected into the prompt. Getting the tone right ("You are pair-programming with a senior…") sets the model's register; getting the conventions right ("stg___") stops it from inventing names.&lt;/li&gt;&lt;li&gt;The naming section is load-bearing: without it, the LLM defaults to arbitrary snake_case model names ("customer_orders_summary") that violate the team's convention. With it, the LLM proposes &lt;code&gt;mart_customers__orders_summary&lt;/code&gt; on the first attempt.&lt;/li&gt;
&lt;li&gt;The "SQL dialect: Snowflake" line is what stops the model from generating Postgres-only functions (&lt;code&gt;::TEXT&lt;/code&gt;, &lt;code&gt;now()&lt;/code&gt;) in a Snowflake repo. Missing this line is one of the most common causes of "the SQL compiled but returned wrong results" bugs.&lt;/li&gt;
&lt;li&gt;The guardrail section is the second-line defense: even a well-prompted LLM occasionally drifts, and telling it to respect &lt;code&gt;dbt parse&lt;/code&gt; and the current Airflow API teaches it to prefer the correct patterns.&lt;/li&gt;
&lt;li&gt;The "when you don't know, say so" instruction turns silent hallucination into loud "please paste the schema for &lt;code&gt;dim_customers&lt;/code&gt;." This single line is worth an hour of review time per week.&lt;/li&gt;
&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;








&lt;/table&gt;&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engineer type&lt;/th&gt;
&lt;th&gt;Default editor&lt;/th&gt;
&lt;th&gt;Default mode&lt;/th&gt;
&lt;th&gt;When to escalate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Junior analytics engineer&lt;/td&gt;
&lt;td&gt;Cursor inline&lt;/td&gt;
&lt;td&gt;comment-first ghost text&lt;/td&gt;
&lt;td&gt;Copilot Chat for "explain this"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Senior analytics engineer&lt;/td&gt;
&lt;td&gt;Cursor inline + Composer&lt;/td&gt;
&lt;td&gt;inline for lines, Composer for multi-file&lt;/td&gt;
&lt;td&gt;Codex CLI for repo-wide refactor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Senior data engineer&lt;/td&gt;
&lt;td&gt;Cursor + Codex CLI&lt;/td&gt;
&lt;td&gt;Composer for DAG edits, CLI for audits&lt;/td&gt;
&lt;td&gt;Copilot Chat for legacy explain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-call engineer (incident)&lt;/td&gt;
&lt;td&gt;Copilot Chat + Cursor&lt;/td&gt;
&lt;td&gt;Chat for "explain," Cursor for hotfix&lt;/td&gt;
&lt;td&gt;Codex CLI only in read-only mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Pick the editor + mode based on (context width × latency budget × agentic depth × review effort). Inline for ghost-text speed, Chat for reasoning, Composer for multi-file edits, CLI for overnight jobs. The &lt;code&gt;.cursorrules&lt;/code&gt; file is the config that ties them all together.&lt;/p&gt;
&lt;h4&gt;
  
  
  Worked example — what senior interviewers actually probe on LLM workflow
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The senior data-engineering LLM-workflow interview in 2026 has a predictable structure: the interviewer opens with an ambiguous question ("walk me through how you use an AI assistant day-to-day"), then progressively narrows to test whether you know the failure modes. The candidates who name the guardrails in sentence one score highest; the candidates who describe "I use Copilot for autocomplete" score lowest. Walk through the interview grading rubric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous opener.&lt;/strong&gt; "How do you use AI assistants in your day-to-day SQL / dbt work?" — invites you to name the pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 1.&lt;/strong&gt; "What's in your &lt;code&gt;.cursorrules&lt;/code&gt; file?" — probes repo context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 2.&lt;/strong&gt; "How do you keep it from hallucinating column names?" — probes hallucination-defense pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 3.&lt;/strong&gt; "How do you review LLM-generated Airflow DAGs?" — probes review discipline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 4.&lt;/strong&gt; "Tell me about a time an LLM shipped subtly-wrong SQL." — probes incident literacy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draft a 5-minute senior LLM-workflow answer that covers all four axes without waiting to be asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interview signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;Senior answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Named editor + mode&lt;/td&gt;
&lt;td&gt;"I use Copilot"&lt;/td&gt;
&lt;td&gt;"Cursor inline for ghost text, Composer for multi-file, Copilot Chat for explains"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repo context&lt;/td&gt;
&lt;td&gt;"it just works"&lt;/td&gt;
&lt;td&gt;"&lt;code&gt;.cursorrules&lt;/code&gt; at repo root pinning dbt conventions + SQL dialect"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucination defense&lt;/td&gt;
&lt;td&gt;"I read the output"&lt;/td&gt;
&lt;td&gt;"comment-first prompts + &lt;code&gt;dbt parse&lt;/code&gt; precommit + schema.yml as truth"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review discipline&lt;/td&gt;
&lt;td&gt;"code review catches it"&lt;/td&gt;
&lt;td&gt;"three-lane guardrail: dbt compile, sqlfluff lint, human PR read"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incident literacy&lt;/td&gt;
&lt;td&gt;"hasn't happened"&lt;/td&gt;
&lt;td&gt;"yes — invented column, dbt parse missed it because Jinja was valid, sqlmesh diff caught it in CI"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Senior LLM-workflow answer template (5 minutes)
================================================

Minute 1 — name the workflow up front
  "I use Cursor as my primary editor with a versioned `.cursorrules`
   file at the repo root, GitHub Copilot for inline ghost text when
   Cursor's small model lags, and Copilot Chat + Codex CLI for
   multi-file refactors and audits. Comment-first prompting is the
   single most important discipline."

Minute 2 — repo context and `.cursorrules`
  "The `.cursorrules` file pins the SQL dialect (Snowflake), the
   dbt naming conventions (stg_/int_/dim_/fct_), the model-layer
   hierarchy, and the guardrail: `dbt parse` must pass before any
   suggestion is accepted. Every completion sees this preamble.
   This alone cuts hallucination rate by ~4×."

Minute 3 — comment-first prompting
  "Before I write SQL, I write a comment describing what the SQL
   returns: `-- returns customers with 3+ orders in last 90d,
   grouped by tier`. Then I let the ghost text complete. The
   comment is a contract; if the completion drifts from it, I
   discard it. Same discipline for dbt models: docstring first,
   SQL second."

Minute 4 — guardrails before merge
  "Three lanes. Compile lane: `dbt parse` + `dbt compile` in
   precommit. Lint lane: `sqlfluff` + `sqlmesh diff` to catch
   semantic drift. Review lane: a human reads every line and runs
   at least one EXPLAIN. Nothing ships without all three."

Minute 5 — incident story
  "Last quarter Copilot completed a JOIN against a column named
   `customer_key` that didn't exist — the schema had `customer_id`.
   The Jinja compiled fine, so `dbt parse` passed. `sqlmesh diff`
   caught it because the affected model's row count changed by
   0.3%, flagged as a warning. Fix: added `dbt run --select ... 
   --defer` to the precommit for changed models. Root cause: the
   context window was stale."
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Minute 1 is the framing minute. Naming the editor and the discipline immediately — "Cursor + &lt;code&gt;.cursorrules&lt;/code&gt; + comment-first" — signals you have a &lt;em&gt;system&lt;/em&gt;, not a habit. Weak candidates say "I use Copilot" and stop; senior candidates open with the workflow.&lt;/li&gt;
&lt;li&gt;Minute 2 addresses the repo-context axis. Naming the &lt;code&gt;.cursorrules&lt;/code&gt; file, its contents (dialect, naming, layer hierarchy), and its effect on hallucination rate ("~4×") shows you've measured what the config does.&lt;/li&gt;
&lt;li&gt;Minute 3 addresses prompt hygiene. The comment-first pattern is the single most transferable discipline; naming it explicitly shows you understand that the LLM's output quality is a function of the input's clarity.&lt;/li&gt;
&lt;li&gt;Minute 4 covers guardrails. Naming three specific tools (&lt;code&gt;dbt parse&lt;/code&gt;, &lt;code&gt;sqlfluff&lt;/code&gt;, human review) beats naming "CI" as a black box. The "nothing ships without all three" language shows the discipline is non-negotiable.&lt;/li&gt;
&lt;li&gt;Minute 5 is the incident-literacy probe. Every senior data engineer has an LLM-related incident story; telling it well — root cause, detection, fix — is more valuable than pretending it never happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading criterion&lt;/th&gt;
&lt;th&gt;Weak score&lt;/th&gt;
&lt;th&gt;Senior score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Names editor + mode&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names &lt;code&gt;.cursorrules&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names comment-first&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names 3-lane guardrail&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tells incident story&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The senior LLM-workflow answer is a 5-minute monologue that covers repo context, prompt hygiene, guardrails, and a concrete incident without waiting for the follow-ups. Rehearse it once; deploy it every time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the "pick the tool" decision tree
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Given a new task, the senior architect runs a 4-question decision tree in their head. Codifying the tree makes the interview answer reproducible: any interviewer can hand you a task and you can walk the tree out loud. Walk through the tree with three canonical tasks: adding a column to a dbt staging model, scaffolding a new Airflow DAG, and doing a repo-wide refactor of a deprecated macro.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Q1.&lt;/strong&gt; Does the task touch one file or many? → one = inline; many = Composer / CLI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q2.&lt;/strong&gt; Is the change local and mechanical, or does it need reasoning? → local = inline ghost text; reasoning = Chat / Composer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q3.&lt;/strong&gt; Do you need to run shell / SQL / pytest as part of the task? → yes = Codex CLI / Aider; no = editor-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q4.&lt;/strong&gt; Is the output going to a PR immediately? → yes = full 3-lane guardrail; no (scratchpad) = lint only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Walk the decision tree for the three tasks and record the tool + mode each ends up with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Q1 (files)&lt;/th&gt;
&lt;th&gt;Q2 (reasoning)&lt;/th&gt;
&lt;th&gt;Q3 (shell/SQL)&lt;/th&gt;
&lt;th&gt;Q4 (PR)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add column to stg_orders&lt;/td&gt;
&lt;td&gt;one&lt;/td&gt;
&lt;td&gt;mechanical&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaffold new S3 → Snowflake DAG&lt;/td&gt;
&lt;td&gt;2–3&lt;/td&gt;
&lt;td&gt;reasoning&lt;/td&gt;
&lt;td&gt;yes (pytest)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refactor deprecated dbt macro across 40 models&lt;/td&gt;
&lt;td&gt;40+&lt;/td&gt;
&lt;td&gt;reasoning&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Decision-tree helper (illustrative)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pick_llm_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files_touched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;needs_reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;needs_shell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;going_to_pr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the LLM tool + mode + guardrail for a task.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;workflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;files_touched&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;needs_reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cursor inline / Copilot inline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comment-first ghost text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;files_touched&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;needs_reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cursor Composer / Copilot Chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversational, single-turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;files_touched&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;needs_shell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Codex CLI / Aider / Cursor Composer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agentic, multi-file, shell-capable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cursor inline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ghost text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;going_to_pr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guardrail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlfluff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guardrail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlfluff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;


&lt;span class="c1"&gt;# Walk the three tasks
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_llm_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# → {'tool': 'Cursor inline / Copilot inline', 'mode': 'comment-first ghost text',
#    'guardrail': ['dbt parse', 'sqlfluff', 'human review']}
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_llm_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# → {'tool': 'Cursor Composer / Copilot Chat', 'mode': 'conversational, single-turn',
#    'guardrail': ['dbt parse', 'sqlfluff', 'human review']}
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_llm_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# → {'tool': 'Codex CLI / Aider / Cursor Composer', 'mode': 'agentic, multi-file, shell-capable',
#    'guardrail': ['dbt parse', 'sqlfluff', 'human review']}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Task 1 — add a column to a staging model. Q1 = one, Q2 = mechanical → inline ghost text is enough. The three-lane guardrail runs on the PR because Q4 = yes.&lt;/li&gt;
&lt;li&gt;Task 2 — scaffold a new S3 → Snowflake DAG with a pytest fixture. Q1 = 2–3, Q2 = reasoning (which operator? which sensor? what retry policy?), Q3 = yes (pytest run needed) → Cursor Composer or Copilot Chat, with the human running pytest locally before push.&lt;/li&gt;
&lt;li&gt;Task 3 — refactor a deprecated dbt macro across 40 models. Q1 = 40+, Q3 = yes (need &lt;code&gt;dbt parse&lt;/code&gt; + &lt;code&gt;dbt run --select&lt;/code&gt;) → Codex CLI / Aider / Composer. The agentic mode reads and edits all 40 files; the human reviews the diff and runs the compile step.&lt;/li&gt;
&lt;li&gt;Q4 (PR vs scratchpad) determines the guardrail intensity. PR-bound work gets the full 3-lane; scratchpad exploration gets &lt;code&gt;sqlfluff&lt;/code&gt; only. Never skip the compile step for PR-bound work — this is the most common source of "the LLM's SQL passed CI but broke prod."&lt;/li&gt;
&lt;li&gt;The tree scales: any additional constraint (secrets, cross-repo edits, data-quality tests) adds another guardrail lane, never subtracts one. The rule is that the guardrail depth is a function of the risk, not the tool.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Guardrail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add column to stg_orders&lt;/td&gt;
&lt;td&gt;Cursor inline&lt;/td&gt;
&lt;td&gt;comment-first&lt;/td&gt;
&lt;td&gt;3-lane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaffold new DAG&lt;/td&gt;
&lt;td&gt;Cursor Composer / Chat&lt;/td&gt;
&lt;td&gt;conversational&lt;/td&gt;
&lt;td&gt;3-lane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refactor macro × 40 models&lt;/td&gt;
&lt;td&gt;Codex CLI / Aider&lt;/td&gt;
&lt;td&gt;agentic&lt;/td&gt;
&lt;td&gt;3-lane&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The four-question decision tree fits on a sticky note. Practice walking it end-to-end so any interviewer can hand you a task and get a tool + mode + guardrail combo in under 60 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on LLM-editor selection
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You inherit an analytics-engineering team of six that hand-types every dbt model and Airflow DAG. Adoption of Cursor + Copilot has been informal — some engineers use it constantly, some not at all, and PR quality is uneven. Walk me through the 30-day plan to standardise the LLM workflow, the &lt;code&gt;.cursorrules&lt;/code&gt; file you'd ship on day one, the guardrails you'd add to CI, and the metrics you'd track to prove the workflow works."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a versioned &lt;code&gt;.cursorrules&lt;/code&gt;, a three-lane guardrail, and PR-throughput / defect-rate metrics
&lt;/h3&gt;



&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .cursorrules — day one; versioned in the repo root&lt;/span&gt;
&lt;span class="s"&gt;You are pair-programming with a senior analytics engineer on the&lt;/span&gt;
&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;analytics-eng/` dbt + Airflow monorepo. Snowflake target,&lt;/span&gt;
&lt;span class="s"&gt;Airflow 2.9 TaskFlow API, dbt 1.8.&lt;/span&gt;

&lt;span class="na"&gt;Repo conventions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Model layers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging (stg_) → intermediate (int_) → marts (dim_/fct_/mart_)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Every model has schema.yml with description + tests&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Prefer CTEs; one CTE per logical step; ORDER BY only at the end&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;SQL dialect: Snowflake (QUALIFY / ILIKE / DATE_TRUNC / :&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;casting)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dbt macros live in macros/; import from dbt_utils and dbt_expectations&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DAGs use @dag / @task decorators; sensors from airflow.sensors.*&lt;/span&gt;

&lt;span class="na"&gt;Never do&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Never invent a column name — ask for schema.yml if unclear&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Never use `airflow.operators.python_operator` (deprecated)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Never write to `prod` targets in generated code&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Never suggest a completion that fails `dbt parse`&lt;/span&gt;

&lt;span class="s"&gt;When you don't know, say so and ask.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .pre-commit-config.yaml — three-lane guardrail&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Lane 1 — compile&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-parse&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt parse&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt parse --profiles-dir profiles&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-compile-changed&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt compile (changed)&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash -c 'dbt compile --select state:modified --defer --state ./manifest'&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="c1"&gt;# Lane 2 — lint&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/sqlfluff/sqlfluff&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.2.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sqlfluff-lint&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;--dialect&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;snowflake&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sqlfluff-fix&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;--dialect&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;snowflake&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;--force&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# Lane 3 — model-level tests (CI, not precommit — too slow)&lt;/span&gt;
  &lt;span class="c1"&gt;# runs in GitHub Actions: dbt build --select state:modified+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# metrics.py — track the workflow impact over 30 days
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;METRICS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metric&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRs merged per engineer per week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean PR lifetime (open → merged)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt models added per week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;post-merge defects per 100 PRs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlfluff violations per PR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;`.cursorrules` violations per PR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseline_wk0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;4.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.1 d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N/A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target_wk4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.9 d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt; 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# Alert if defect rate rises above baseline for two consecutive weeks
# (LLM-driven regressions typically show up here first)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Before (informal adoption)&lt;/th&gt;
&lt;th&gt;After (standardised workflow)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.cursorrules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;versioned; ~40 lines at repo root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comment-first prompting&lt;/td&gt;
&lt;td&gt;ad-hoc&lt;/td&gt;
&lt;td&gt;required by convention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precommit compile&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dbt parse&lt;/code&gt; + &lt;code&gt;dbt compile --select state:modified&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precommit lint&lt;/td&gt;
&lt;td&gt;inconsistent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sqlfluff&lt;/code&gt; with snowflake dialect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR review discipline&lt;/td&gt;
&lt;td&gt;uneven&lt;/td&gt;
&lt;td&gt;template requires "ran EXPLAIN?" checkbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metrics&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;PR throughput, mean lifetime, defect rate tracked weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding&lt;/td&gt;
&lt;td&gt;tribal knowledge&lt;/td&gt;
&lt;td&gt;30-min recorded pair session per engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the 30-day rollout, PR throughput doubles (~4/wk → ~8/wk per engineer), mean PR lifetime drops from ~2 days to under a day, and the defect rate holds steady or falls (the guardrails catch what the increased volume would otherwise ship). The two engineers who were skeptical of the LLM workflow become the loudest advocates once the &lt;code&gt;.cursorrules&lt;/code&gt; file removes the "it hallucinated my column name" objection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline (week 0)&lt;/th&gt;
&lt;th&gt;After 30 days (target)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PRs merged / engineer / week&lt;/td&gt;
&lt;td&gt;4.2&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean PR lifetime&lt;/td&gt;
&lt;td&gt;2.1 d&lt;/td&gt;
&lt;td&gt;0.9 d&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt models added / week&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;3.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-merge defects per 100 PRs&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqlfluff violations per PR&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.cursorrules&lt;/code&gt; violations per PR&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;&amp;lt; 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;.cursorrules as source of truth&lt;/strong&gt;&lt;/strong&gt; — versioning the LLM's repo prompt in the repo itself makes it a first-class deliverable, reviewable in PRs, evolvable with the codebase. This is the single most impactful config change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Comment-first prompting&lt;/strong&gt;&lt;/strong&gt; — the natural-language contract you write before the SQL is what pins ghost text to intent. Skipping it forces the LLM to guess from surrounding context, which is where most hallucinations enter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Three-lane precommit&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;dbt parse&lt;/code&gt; catches unresolved refs; &lt;code&gt;dbt compile&lt;/code&gt; catches invalid Jinja; &lt;code&gt;sqlfluff&lt;/code&gt; catches style drift. Running all three locally before push turns "the LLM shipped bad SQL to CI" into "the LLM's suggestion never made it out of the editor."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Deferred compile&lt;/strong&gt;&lt;/strong&gt; — &lt;code&gt;dbt compile --defer --state ./manifest&lt;/code&gt; compiles only the models whose upstream state has actually changed, keeping the precommit fast even in a 300-model repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one &lt;code&gt;.cursorrules&lt;/code&gt; file (~40 lines), one &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; (~30 lines), a 30-min onboarding session per engineer, and weekly metric tracking. The payback is 2× PR throughput and stable defect rate. Compared to hand-typing everything, this is O(1) config for O(N) engineer-hours saved per week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems for LLM-assisted authoring practice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL-Generation&lt;/span&gt;
&lt;span&gt;Topic — sql-generation&lt;/span&gt;
&lt;strong&gt;SQL-generation drills for prompt hygiene&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Cursor rules, .cursorrules, and repo-scoped context for SQL / dbt
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The &lt;code&gt;.cursorrules&lt;/code&gt; file is the single artifact that decides whether Cursor writes idiomatic repo-SQL or generic textbook SQL
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;cursor rules for sql&lt;/code&gt; are a versioned, repo-scoped preamble that every Cursor completion (inline and Composer) sees before it drafts a single character — a well-crafted &lt;code&gt;.cursorrules&lt;/code&gt; pins the SQL dialect, the dbt naming convention, the model-layer hierarchy, the do-nots, and the guardrail expectations, and moves Cursor from "generic SQL assistant" to "member of the analytics-engineering team who remembers the conventions from month one."&lt;/strong&gt; The single most measured lever in a Cursor deployment is the quality of this file; teams that neglect it complain about hallucination rates, teams that maintain it treat Cursor as a productivity multiplier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5mh5gz6lis6q830tpkf.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5mh5gz6lis6q830tpkf.jpeg" alt="Iconographic Cursor rules diagram — a repo file tree card with a highlighted .cursorrules file glowing purple, a bracket illustrating the context window pulling schema.yml and dbt_project.yml, and a small sparkle-glyph indicating LLM prompt injection." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes of a good &lt;code&gt;.cursorrules&lt;/code&gt; file.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Voice + role.&lt;/strong&gt; Open with "You are pair-programming with a senior analytics engineer on the &lt;code&gt;&amp;lt;repo&amp;gt;&lt;/code&gt; monorepo." This sets the LLM's register — it stops writing tutorial-grade SQL and starts writing the kind of terse, CTE-heavy SQL a senior would ship. Skip this line and every completion reads like a Stack Overflow answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conventions.&lt;/strong&gt; Explicit rules for naming (&lt;code&gt;stg_&lt;/code&gt;, &lt;code&gt;int_&lt;/code&gt;, &lt;code&gt;dim_&lt;/code&gt;, &lt;code&gt;fct_&lt;/code&gt;), model layers (staging → intermediate → marts), style (CTE per step, ORDER BY at the end, QUALIFY over subqueries for window filters), and SQL dialect (Snowflake / BigQuery / Postgres). Every convention you omit is one the LLM will guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do-nots.&lt;/strong&gt; Explicit anti-patterns: "never use &lt;code&gt;airflow.operators.python_operator&lt;/code&gt; (deprecated)," "never invent a column name — ask for schema.yml," "never write to &lt;code&gt;prod&lt;/code&gt; targets." Do-nots are more effective than do's for constraining generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrail expectations.&lt;/strong&gt; "Every SQL suggestion must compile with &lt;code&gt;dbt parse&lt;/code&gt;" — telling the LLM what the reviewer will check makes it self-check before proposing. This is prompt-engineering leverage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The context window — what Cursor actually sees at completion time.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.cursorrules&lt;/code&gt;.&lt;/strong&gt; Always injected. Bounded (~1–2k tokens is comfortable; over ~5k crowds out useful file context).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Currently open file.&lt;/strong&gt; Full text (up to a per-model window budget).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recently opened files.&lt;/strong&gt; Truncated; more recent = more weight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@Codebase&lt;/code&gt; symbol.&lt;/strong&gt; In Cursor Chat / Composer, &lt;code&gt;@Codebase&lt;/code&gt; performs a vector search over the repo and injects the top-N matched files. Cost: a few seconds of retrieval latency; benefit: the LLM now sees the actual &lt;code&gt;schema.yml&lt;/code&gt; for the model you're editing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@Docs&lt;/code&gt; symbol.&lt;/strong&gt; Injects external docs the user has linked (dbt docs, Airflow docs, Snowflake syntax). Use for API-drift-prone tasks (Airflow operator names, dbt macro signatures).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Failure modes senior engineers pre-empt.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stale rules.&lt;/strong&gt; The team renames &lt;code&gt;dim_customer&lt;/code&gt; → &lt;code&gt;dim_customers&lt;/code&gt;; the &lt;code&gt;.cursorrules&lt;/code&gt; still references the old name; every completion drifts. Fix: PR-review the &lt;code&gt;.cursorrules&lt;/code&gt; file whenever a convention changes; treat it as part of the schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-prompting.&lt;/strong&gt; A 500-line &lt;code&gt;.cursorrules&lt;/code&gt; file eats the context window and leaves no room for the actual file the user is editing. Fix: keep it under ~1500 tokens; put verbose examples in &lt;code&gt;docs/&lt;/code&gt; and let &lt;code&gt;@Docs&lt;/code&gt; pull them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalidated context.&lt;/strong&gt; The user opens 15 files, the context window fills with the earliest, and the current-file completion has stale scope. Fix: close unused tabs; use &lt;code&gt;Cursor: Restart Cursor&lt;/code&gt; to reset context if suggestions drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent drift.&lt;/strong&gt; The LLM starts suggesting completions that violate &lt;code&gt;.cursorrules&lt;/code&gt; and no one notices because the completions still compile. Fix: add a per-PR sqlfluff rule for repo-idiom (&lt;code&gt;sqlfluff&lt;/code&gt; doesn't do this natively; roll a custom &lt;code&gt;dbt-checkpoint&lt;/code&gt; or &lt;code&gt;pre-commit-dbt&lt;/code&gt; rule that greps for prohibited patterns).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on &lt;code&gt;.cursorrules&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What's the first thing you'd put in a &lt;code&gt;.cursorrules&lt;/code&gt; file?" — the voice + role line ("You are pair-programming with a senior…").&lt;/li&gt;
&lt;li&gt;"How long should a &lt;code&gt;.cursorrules&lt;/code&gt; file be?" — 1–2 kilobytes; under ~1500 tokens; anything longer crowds out context.&lt;/li&gt;
&lt;li&gt;"How do you keep the LLM from inventing column names?" — comment-first prompt + &lt;code&gt;@Codebase&lt;/code&gt; or &lt;code&gt;@Docs&lt;/code&gt; injection of &lt;code&gt;schema.yml&lt;/code&gt; + the do-not rule.&lt;/li&gt;
&lt;li&gt;"How often do you update &lt;code&gt;.cursorrules&lt;/code&gt;?" — every time a repo convention changes; reviewed in PRs like any other config.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — a production-grade &lt;code&gt;.cursorrules&lt;/code&gt; for a dbt + Snowflake repo
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Build a &lt;code&gt;.cursorrules&lt;/code&gt; file for a real analytics-engineering repo: dbt 1.8, Snowflake, ~300 models across staging/intermediate/marts, dbt_utils + dbt_expectations installed, a &lt;code&gt;macros/&lt;/code&gt; folder with 20 custom macros, Airflow 2.9 for orchestration. Every clause has a reason; walk through them one by one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model shape.&lt;/strong&gt; stg → int → mart layers; every model has a &lt;code&gt;schema.yml&lt;/code&gt; entry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL style.&lt;/strong&gt; CTE-first, QUALIFY over subqueries, snake_case, ORDER BY only when needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dbt idioms.&lt;/strong&gt; &lt;code&gt;ref()&lt;/code&gt; over hardcoded schema, &lt;code&gt;source()&lt;/code&gt; for raw tables, &lt;code&gt;dbt_utils.pivot&lt;/code&gt; over hand-rolled CASE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airflow idioms.&lt;/strong&gt; TaskFlow API, &lt;code&gt;@dag&lt;/code&gt;/&lt;code&gt;@task&lt;/code&gt; decorators, sensors from &lt;code&gt;airflow.sensors.*&lt;/code&gt;, retries on network operators.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write a production-grade &lt;code&gt;.cursorrules&lt;/code&gt; for this repo, staying under 1500 tokens, and explain each section's purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Token budget&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Role + voice&lt;/td&gt;
&lt;td&gt;set the model's register&lt;/td&gt;
&lt;td&gt;~50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL dialect + repo shape&lt;/td&gt;
&lt;td&gt;ground the completions&lt;/td&gt;
&lt;td&gt;~200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naming conventions&lt;/td&gt;
&lt;td&gt;pin identifier shape&lt;/td&gt;
&lt;td&gt;~200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Style rules&lt;/td&gt;
&lt;td&gt;pin structural patterns&lt;/td&gt;
&lt;td&gt;~200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt + Airflow idioms&lt;/td&gt;
&lt;td&gt;pin idiomatic API usage&lt;/td&gt;
&lt;td&gt;~300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do-nots&lt;/td&gt;
&lt;td&gt;explicit anti-patterns&lt;/td&gt;
&lt;td&gt;~200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guardrail expectations&lt;/td&gt;
&lt;td&gt;teach the LLM to self-check&lt;/td&gt;
&lt;td&gt;~150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# .cursorrules&lt;/span&gt;
You are pair-programming with a senior analytics engineer on the
&lt;span class="sb"&gt;`analytics-eng/`&lt;/span&gt; monorepo. This repo runs on Snowflake, dbt 1.8,
and Airflow 2.9. You write terse, review-ready SQL and Python
that respects the following conventions.

&lt;span class="gu"&gt;## Repo shape&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`dbt_project/`&lt;/span&gt; — dbt project root
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`models/staging/`&lt;/span&gt; — one folder per source system; models named &lt;span class="sb"&gt;`stg_&amp;lt;source&amp;gt;__&amp;lt;table&amp;gt;.sql`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`models/intermediate/`&lt;/span&gt; — models named &lt;span class="sb"&gt;`int_&amp;lt;domain&amp;gt;__&amp;lt;verb&amp;gt;.sql`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`models/marts/`&lt;/span&gt; — models named &lt;span class="sb"&gt;`dim_&amp;lt;entity&amp;gt;.sql`&lt;/span&gt; (dimensions) or &lt;span class="sb"&gt;`fct_&amp;lt;event&amp;gt;.sql`&lt;/span&gt; (facts)
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`macros/`&lt;/span&gt; — custom macros, one per file; snake_case
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`snapshots/`&lt;/span&gt; — dbt snapshots for slowly-changing dimensions
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`dags/`&lt;/span&gt; — Airflow DAGs, one file per DAG; DAG id matches filename
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`tests/`&lt;/span&gt; — pytest for DAGs

&lt;span class="gu"&gt;## SQL style&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; SQL dialect: Snowflake. Use QUALIFY, ILIKE, DATE_TRUNC, :: casting, TRY_CAST.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer CTEs over subqueries; one CTE per logical step.
&lt;span class="p"&gt;-&lt;/span&gt; Name CTEs after their purpose (&lt;span class="sb"&gt;`customers_with_orders`&lt;/span&gt;, &lt;span class="sb"&gt;`latest_status_per_order`&lt;/span&gt;).
&lt;span class="p"&gt;-&lt;/span&gt; ORDER BY only in the final SELECT and only when required by downstream.
&lt;span class="p"&gt;-&lt;/span&gt; Never SELECT &lt;span class="err"&gt;*&lt;/span&gt; in a mart; always list columns explicitly.
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`{{ ref('...') }}`&lt;/span&gt; for dbt models and &lt;span class="sb"&gt;`{{ source('...') }}`&lt;/span&gt; for raw tables.
&lt;span class="p"&gt;-&lt;/span&gt; For pivots, prefer &lt;span class="sb"&gt;`dbt_utils.pivot`&lt;/span&gt;; for testing, prefer &lt;span class="sb"&gt;`dbt_expectations`&lt;/span&gt;.

&lt;span class="gu"&gt;## dbt idioms&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Every model has a &lt;span class="sb"&gt;`schema.yml`&lt;/span&gt; entry with &lt;span class="sb"&gt;`description`&lt;/span&gt;, &lt;span class="sb"&gt;`columns:`&lt;/span&gt;, and at least one &lt;span class="sb"&gt;`unique`&lt;/span&gt; or &lt;span class="sb"&gt;`not_null`&lt;/span&gt; test on the primary key.
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`{{ config(materialized='incremental', unique_key='id', on_schema_change='sync_all_columns') }}`&lt;/span&gt; for incremental models.
&lt;span class="p"&gt;-&lt;/span&gt; Never hardcode a schema name in SQL; always route through &lt;span class="sb"&gt;`{{ ref() }}`&lt;/span&gt; / &lt;span class="sb"&gt;`{{ source() }}`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Macros go in &lt;span class="sb"&gt;`macros/`&lt;/span&gt;; document with a &lt;span class="sb"&gt;`{% docs %}`&lt;/span&gt; block.

&lt;span class="gu"&gt;## Airflow idioms&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Use TaskFlow API: &lt;span class="sb"&gt;`@dag`&lt;/span&gt;, &lt;span class="sb"&gt;`@task`&lt;/span&gt;, &lt;span class="sb"&gt;`@task_group`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Sensors: &lt;span class="sb"&gt;`S3KeySensor`&lt;/span&gt;, &lt;span class="sb"&gt;`SnowflakeSensor`&lt;/span&gt;, &lt;span class="sb"&gt;`TimeDeltaSensor`&lt;/span&gt; — never poll in a &lt;span class="sb"&gt;`PythonOperator`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Every operator that touches the network gets &lt;span class="sb"&gt;`retries=3, retry_delay=timedelta(minutes=5)`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Never import from &lt;span class="sb"&gt;`airflow.operators.python_operator`&lt;/span&gt; (deprecated in 2.8; removed in 3.0).
&lt;span class="p"&gt;-&lt;/span&gt; Every DAG has a docstring at the top: purpose, schedule, owner.

&lt;span class="gu"&gt;## Do-nots&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never invent a column name. If the ref() target is not in context, ask for the schema.yml or pause and request the schema.
&lt;span class="p"&gt;-&lt;/span&gt; Never write to a &lt;span class="sb"&gt;`prod_`&lt;/span&gt; target in generated code.
&lt;span class="p"&gt;-&lt;/span&gt; Never suggest a completion that would fail &lt;span class="sb"&gt;`dbt parse`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Never use &lt;span class="sb"&gt;`now()`&lt;/span&gt; in Snowflake — use &lt;span class="sb"&gt;`current_timestamp()`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Never omit &lt;span class="sb"&gt;`ORDER BY`&lt;/span&gt; inside a window function that needs deterministic ordering.

&lt;span class="gu"&gt;## Guardrail expectations&lt;/span&gt;
Every completion you propose will be checked by:
&lt;span class="p"&gt;  1.&lt;/span&gt; &lt;span class="sb"&gt;`dbt parse`&lt;/span&gt; (unresolved refs fail here)
&lt;span class="p"&gt;  2.&lt;/span&gt; &lt;span class="sb"&gt;`dbt compile --select state:modified`&lt;/span&gt; (invalid Jinja fails here)
&lt;span class="p"&gt;  3.&lt;/span&gt; &lt;span class="sb"&gt;`sqlfluff lint --dialect snowflake`&lt;/span&gt; (style fails here)
&lt;span class="p"&gt;  4.&lt;/span&gt; Human PR read (semantic drift fails here)
If you're not confident a suggestion passes all four, ask the user for clarification instead of guessing.
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The role line ("pair-programming with a senior analytics engineer") sets the register. Without it, Cursor writes tutorial-grade SQL with unnecessary comments and beginner idioms. With it, the completions are terse and CTE-heavy.&lt;/li&gt;
&lt;li&gt;The repo shape section tells Cursor where files live. This is what stops it from generating a new dbt model in &lt;code&gt;dbt_project/dbt_project/models/&lt;/code&gt; (a common mistake when the LLM guesses the folder structure).&lt;/li&gt;
&lt;li&gt;The SQL style section pins structural choices: CTE-first, QUALIFY over subqueries, no SELECT * in marts. Every rule here is one the LLM would otherwise guess randomly.&lt;/li&gt;
&lt;li&gt;The dbt idioms section is the highest-leverage: it teaches the LLM the exact dbt patterns your team uses (incremental config, schema.yml convention, dbt_utils.pivot). Skipping this section gets you generic dbt that doesn't fit.&lt;/li&gt;
&lt;li&gt;The do-nots section is more effective than the do's because "never invent a column name" is a specific defense against the single most-cited hallucination failure mode. Naming failure modes by name in the rules file trains the LLM to avoid them.&lt;/li&gt;
&lt;li&gt;The guardrail expectations section closes the loop: the LLM knows what tools will check its output and can self-check against those tools. This is what turns "propose anything" into "propose only what will pass CI."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Before rules (hallucination rate)&lt;/th&gt;
&lt;th&gt;After rules&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Column name invention&lt;/td&gt;
&lt;td&gt;~1/10 completions&lt;/td&gt;
&lt;td&gt;~1/50 completions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong SQL dialect&lt;/td&gt;
&lt;td&gt;~1/8 completions&lt;/td&gt;
&lt;td&gt;~1/100 completions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-idiomatic dbt&lt;/td&gt;
&lt;td&gt;~1/4 completions&lt;/td&gt;
&lt;td&gt;~1/25 completions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deprecated Airflow API&lt;/td&gt;
&lt;td&gt;~1/6 completions&lt;/td&gt;
&lt;td&gt;~1/50 completions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing schema.yml entry&lt;/td&gt;
&lt;td&gt;~1/3 new models&lt;/td&gt;
&lt;td&gt;~1/15 new models&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Write the &lt;code&gt;.cursorrules&lt;/code&gt; file in the same voice you'd use in a good &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;. Sections in order: role, repo shape, SQL style, dbt idioms, Airflow idioms, do-nots, guardrails. Keep it under ~1500 tokens. Review it in PRs. Rewrite it when a convention changes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — using &lt;code&gt;@Codebase&lt;/code&gt; and &lt;code&gt;@Docs&lt;/code&gt; for context-window control
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Even a perfect &lt;code&gt;.cursorrules&lt;/code&gt; file cannot make the LLM know what columns &lt;code&gt;dim_customers&lt;/code&gt; has. For that, Cursor exposes two symbols: &lt;code&gt;@Codebase&lt;/code&gt; (vector-search the repo and inject matching files) and &lt;code&gt;@Docs&lt;/code&gt; (inject external docs the user has linked). Using them well is what turns Cursor Chat / Composer from "generic assistant" into "assistant with your schema in memory." Walk through a canonical workflow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Task.&lt;/strong&gt; Write an incremental dbt model &lt;code&gt;int_orders__enriched.sql&lt;/code&gt; that joins &lt;code&gt;stg_orders&lt;/code&gt; with &lt;code&gt;dim_customers&lt;/code&gt; on &lt;code&gt;customer_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problem.&lt;/strong&gt; Cursor Composer doesn't know the columns on either table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution.&lt;/strong&gt; &lt;code&gt;@Codebase stg_orders.sql schema.yml&lt;/code&gt; + &lt;code&gt;@Codebase dim_customers.sql schema.yml&lt;/code&gt; injects both models' definitions and their schema.yml entries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draft the Cursor Composer prompt for this task, using &lt;code&gt;@Codebase&lt;/code&gt; to pull in the two upstream models and their schemas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symbol&lt;/th&gt;
&lt;th&gt;Injects&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Codebase &amp;lt;term&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;vector-searched files matching term&lt;/td&gt;
&lt;td&gt;2–5 s retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Codebase &amp;lt;exact-file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;that file's full contents&lt;/td&gt;
&lt;td&gt;0.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Docs &amp;lt;linked-doc&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;user-added docs (dbt, Airflow, Snowflake)&lt;/td&gt;
&lt;td&gt;1–3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;manually-selected file list&lt;/td&gt;
&lt;td&gt;~0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;live web search (Cursor Chat only)&lt;/td&gt;
&lt;td&gt;3–10 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Cursor Composer prompt
Author `models/intermediate/int_orders__enriched.sql`.

Context:
  @Codebase stg_orders schema.yml
  @Codebase dim_customers schema.yml
  @Docs dbt-incremental-materialization

Requirements:
  - Incremental materialization on Snowflake, unique_key='id'
  - Join stg_orders (o) with dim_customers (c) on o.customer_id = c.customer_id
  - Add computed columns: customer_lifetime_value (SUM of o.total_cents per customer),
    order_recency_days (DATE_DIFF from o.order_date to current_date)
  - Filter to orders in the last 730 days
  - Add schema.yml entry with tests: unique on id, not_null on id, relationships
    to dim_customers on customer_id
  - Follow the repo's stg_/int_/mart_ naming; QUALIFY over subqueries where possible
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The prompt opens with the task — one sentence describing what file to author and what it does. Cursor Composer works best when the task is scoped concretely.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Context:&lt;/code&gt; block uses three symbol injections. &lt;code&gt;@Codebase stg_orders schema.yml&lt;/code&gt; performs a vector search and pulls in &lt;code&gt;stg_orders.sql&lt;/code&gt; and &lt;code&gt;schema.yml&lt;/code&gt;; the LLM now sees the actual columns. &lt;code&gt;@Codebase dim_customers schema.yml&lt;/code&gt; does the same for the dimension. &lt;code&gt;@Docs dbt-incremental-materialization&lt;/code&gt; pulls in the dbt docs page for incremental materialization.&lt;/li&gt;
&lt;li&gt;The requirements section pins the SQL shape: incremental config, join keys, computed columns, filters, tests. Each requirement is a specific instruction; skip any of them and the LLM guesses.&lt;/li&gt;
&lt;li&gt;The final line ("follow the repo's naming") is a hint that the &lt;code&gt;.cursorrules&lt;/code&gt; file will also inject. Redundant reminders in the prompt help when the context window is deep.&lt;/li&gt;
&lt;li&gt;The output is a first-draft &lt;code&gt;.sql&lt;/code&gt; file plus a proposed &lt;code&gt;schema.yml&lt;/code&gt; entry, both of which the engineer reviews, runs &lt;code&gt;dbt parse&lt;/code&gt; on, and either commits or iterates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt element&lt;/th&gt;
&lt;th&gt;Without @Codebase&lt;/th&gt;
&lt;th&gt;With @Codebase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Column-name accuracy&lt;/td&gt;
&lt;td&gt;~60% (guessed from stem names)&lt;/td&gt;
&lt;td&gt;~99% (read from schema.yml)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join-key accuracy&lt;/td&gt;
&lt;td&gt;~70%&lt;/td&gt;
&lt;td&gt;~99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt config correctness&lt;/td&gt;
&lt;td&gt;~80% (docs in preamble)&lt;/td&gt;
&lt;td&gt;~99% (docs re-injected)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First-draft compilable rate&lt;/td&gt;
&lt;td&gt;~40%&lt;/td&gt;
&lt;td&gt;~90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer iterations&lt;/td&gt;
&lt;td&gt;3–4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any Cursor Composer task that touches upstream models, always inject &lt;code&gt;@Codebase &amp;lt;model&amp;gt; schema.yml&lt;/code&gt; for each upstream. For API-drift-prone tasks (Airflow operators, dbt macro signatures), always inject &lt;code&gt;@Docs&lt;/code&gt;. The two-second retrieval cost pays back in first-draft accuracy.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — context-window management for a 300-model monorepo
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; In a 300-model dbt monorepo, the context window fills quickly. If the user has 15 files open in tabs, Cursor prioritises the current file plus recent tabs; older tabs get truncated or dropped. This means the LLM's "memory" of the codebase is much smaller than the codebase itself. Walk through the discipline that keeps the context window tight.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom.&lt;/strong&gt; Cursor suggests a completion that references &lt;code&gt;dim_customer&lt;/code&gt; (old name) instead of &lt;code&gt;dim_customers&lt;/code&gt; (new name), even though the &lt;code&gt;.cursorrules&lt;/code&gt; mentions the new name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause.&lt;/strong&gt; An old tab with &lt;code&gt;dim_customer&lt;/code&gt; references is still open; it's competing with the &lt;code&gt;.cursorrules&lt;/code&gt; for context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; Close unused tabs; use &lt;code&gt;Cursor: Reindex Codebase&lt;/code&gt; after schema changes; use &lt;code&gt;@Files&lt;/code&gt; to explicitly scope context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Design a context-window hygiene routine for a 300-model repo and quantify the impact on suggestion quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Practice&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Benefit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Close unused tabs&lt;/td&gt;
&lt;td&gt;~10 s&lt;/td&gt;
&lt;td&gt;keep current-file context weighted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reindex after schema change&lt;/td&gt;
&lt;td&gt;~30 s&lt;/td&gt;
&lt;td&gt;fresh codebase vector index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use &lt;code&gt;@Files&lt;/code&gt; for scoped context&lt;/td&gt;
&lt;td&gt;~0 s&lt;/td&gt;
&lt;td&gt;explicit control, no drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reset context weekly&lt;/td&gt;
&lt;td&gt;~5 s&lt;/td&gt;
&lt;td&gt;flush stale conversation history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update &lt;code&gt;.cursorrules&lt;/code&gt; on rename&lt;/td&gt;
&lt;td&gt;~1 min per rename&lt;/td&gt;
&lt;td&gt;prevent old name leakage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# context-window-hygiene.sh — a weekly reset routine for Cursor&lt;/span&gt;

&lt;span class="c"&gt;# 1. Close all files in Cursor (Cmd-K Cmd-W)&lt;/span&gt;
&lt;span class="c"&gt;# 2. Reindex codebase (Cmd-Shift-P → "Cursor: Reindex Codebase")&lt;/span&gt;
&lt;span class="c"&gt;# 3. Verify .cursorrules is current&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; .cursorrules | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;    &lt;span class="c"&gt;# target &amp;lt; 6000 chars (~ 1500 tokens)&lt;/span&gt;

&lt;span class="c"&gt;# 4. Verify docs/ folder is current (Cursor @Docs pulls from here)&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; docs/ | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# 5. Grep for any stale model references in .cursorrules or docs/&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"dim_customer&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; .cursorrules docs/ &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"clean"&lt;/span&gt;

&lt;span class="c"&gt;# 6. Optional: purge the Cursor cache for a full reset&lt;/span&gt;
&lt;span class="c"&gt;#    (macOS)  rm -rf ~/Library/Application\ Support/Cursor/User/globalStorage&lt;/span&gt;
&lt;span class="c"&gt;#    (linux)  rm -rf ~/.config/Cursor/User/globalStorage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .cursor/settings.json — repo-scoped Cursor settings&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor.chat.context.mode"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor.chat.context.autoInclude"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.cursorrules"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_project.yml"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profiles.yml"&lt;/span&gt;
  &lt;span class="pi"&gt;],&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor.chat.context.maxTokens"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;8000&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor.codebase.indexingEnabled"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor.codebase.embedModel"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;large"&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The core discipline is that the context window is a &lt;em&gt;scarce resource&lt;/em&gt;. Every open tab, every conversation-history turn, every retrieved file eats into the budget. The default Cursor context is ~8k–16k tokens for chat, less for inline. Above the ceiling, the earliest content gets truncated silently.&lt;/li&gt;
&lt;li&gt;Closing unused tabs is the cheapest and highest-leverage practice. The LLM weights recent tabs; if a stale tab is open, it can leak old names into completions. &lt;code&gt;Cmd-K Cmd-W&lt;/code&gt; (close all) is a fine end-of-session habit.&lt;/li&gt;
&lt;li&gt;Reindexing after schema changes rebuilds Cursor's codebase vector index. Without it, &lt;code&gt;@Codebase&lt;/code&gt; returns stale search results — you'll get the old model names for a day or two after a rename.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;.cursor/settings.json&lt;/code&gt; file (project-scoped) lets you pin what's always in context. &lt;code&gt;autoInclude: [".cursorrules", "dbt_project.yml", "profiles.yml"]&lt;/code&gt; guarantees these three files are always visible to Cursor, regardless of what's open.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;grep -rn "old_name\b" .cursorrules docs/&lt;/code&gt; check catches the "renamed a model but forgot to update the rules" case. Adding this to a weekly hygiene routine turns silent context drift into a caught defect.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Without hygiene&lt;/th&gt;
&lt;th&gt;With hygiene&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stale-name completions per week&lt;/td&gt;
&lt;td&gt;5–10&lt;/td&gt;
&lt;td&gt;0–1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor Composer first-draft accuracy&lt;/td&gt;
&lt;td&gt;~70%&lt;/td&gt;
&lt;td&gt;~90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time spent debugging "why did it suggest that?"&lt;/td&gt;
&lt;td&gt;~2 h/week&lt;/td&gt;
&lt;td&gt;~0 h/week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context-window overflow errors&lt;/td&gt;
&lt;td&gt;occasional&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly hygiene cost&lt;/td&gt;
&lt;td&gt;0 min (skipped)&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Treat the Cursor context window as a scarce resource. Close unused tabs, reindex after schema changes, pin &lt;code&gt;.cursorrules&lt;/code&gt; + &lt;code&gt;dbt_project.yml&lt;/code&gt; + &lt;code&gt;profiles.yml&lt;/code&gt; via &lt;code&gt;autoInclude&lt;/code&gt;, and grep for stale names weekly. Five minutes of hygiene prevents ten hours of "why did the LLM suggest that?" debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on Cursor rules
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your analytics team just adopted Cursor. Some engineers love it, some complain it hallucinates constantly. You've been asked to standardise the setup. Walk me through the &lt;code&gt;.cursorrules&lt;/code&gt; file you'd ship, the repo-context-injection patterns you'd teach the team, and how you'd measure the impact over 30 days."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a versioned &lt;code&gt;.cursorrules&lt;/code&gt;, &lt;code&gt;@Codebase&lt;/code&gt;/&lt;code&gt;@Docs&lt;/code&gt; injection patterns, and a hallucination-rate metric
&lt;/h3&gt;



&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# .cursorrules — production draft, ~1300 tokens&lt;/span&gt;
You are pair-programming with a senior analytics engineer on the
&lt;span class="sb"&gt;`analytics-eng/`&lt;/span&gt; monorepo. Snowflake 8.x, dbt 1.8, Airflow 2.9.
Terse, review-ready SQL and Python; respect these conventions.

&lt;span class="gu"&gt;## Repo shape&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; dbt_project/ — models/{staging,intermediate,marts}, macros/, snapshots/, seeds/
&lt;span class="p"&gt;-&lt;/span&gt; dags/ — one Airflow DAG per file; DAG id matches filename
&lt;span class="p"&gt;-&lt;/span&gt; tests/ — pytest for DAGs
&lt;span class="p"&gt;-&lt;/span&gt; profiles.yml — snowflake_prod / snowflake_dev

&lt;span class="gu"&gt;## Naming&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Staging: stg_&lt;span class="nt"&gt;&amp;lt;source&amp;gt;&lt;/span&gt;&lt;span class="ge"&gt;__&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;table&amp;gt;&lt;/span&gt;.sql
&lt;span class="p"&gt;-&lt;/span&gt; Intermediate: int_&lt;span class="nt"&gt;&amp;lt;domain&amp;gt;&lt;/span&gt;&lt;span class="ge"&gt;__&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;verb&amp;gt;&lt;/span&gt;.sql
&lt;span class="p"&gt;-&lt;/span&gt; Marts: dim_&lt;span class="nt"&gt;&amp;lt;entity&amp;gt;&lt;/span&gt;.sql | fct_&lt;span class="nt"&gt;&amp;lt;event&amp;gt;&lt;/span&gt;.sql | mart_&lt;span class="nt"&gt;&amp;lt;aggregate&amp;gt;&lt;/span&gt;.sql
&lt;span class="p"&gt;-&lt;/span&gt; Every model has a schema.yml entry with &lt;span class="sb"&gt;`description`&lt;/span&gt;, &lt;span class="sb"&gt;`columns:`&lt;/span&gt;, and PK tests.

&lt;span class="gu"&gt;## SQL style&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Snowflake dialect (QUALIFY, ILIKE, DATE_TRUNC, ::casting, TRY_CAST).
&lt;span class="p"&gt;-&lt;/span&gt; CTE-first; one CTE per logical step; named after purpose.
&lt;span class="p"&gt;-&lt;/span&gt; ORDER BY only in the final SELECT and only when needed downstream.
&lt;span class="p"&gt;-&lt;/span&gt; Never SELECT &lt;span class="err"&gt;*&lt;/span&gt; in a mart.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer &lt;span class="sb"&gt;`{{ ref() }}`&lt;/span&gt; and &lt;span class="sb"&gt;`{{ source() }}`&lt;/span&gt; over hardcoded schema.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer &lt;span class="sb"&gt;`dbt_utils.pivot`&lt;/span&gt; over hand-rolled CASE for pivots.

&lt;span class="gu"&gt;## dbt idioms&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Incremental: &lt;span class="sb"&gt;`{{ config(materialized='incremental', unique_key='id', on_schema_change='sync_all_columns') }}`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Snapshots: strategy='timestamp' with &lt;span class="sb"&gt;`updated_at`&lt;/span&gt; column.
&lt;span class="p"&gt;-&lt;/span&gt; Macros: one per file, snake_case, documented with &lt;span class="sb"&gt;`{% docs %}`&lt;/span&gt; block.

&lt;span class="gu"&gt;## Airflow idioms&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; TaskFlow API: &lt;span class="sb"&gt;`@dag`&lt;/span&gt;, &lt;span class="sb"&gt;`@task`&lt;/span&gt;, &lt;span class="sb"&gt;`@task_group`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Sensors: &lt;span class="sb"&gt;`S3KeySensor`&lt;/span&gt;, &lt;span class="sb"&gt;`SnowflakeSensor`&lt;/span&gt; — never poll in &lt;span class="sb"&gt;`PythonOperator`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Network operators: &lt;span class="sb"&gt;`retries=3, retry_delay=timedelta(minutes=5)`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Never &lt;span class="sb"&gt;`airflow.operators.python_operator`&lt;/span&gt; (deprecated).
&lt;span class="p"&gt;-&lt;/span&gt; Every DAG has a top-of-file docstring: purpose, schedule, owner.

&lt;span class="gu"&gt;## Do-nots&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never invent a column name. Ask for the schema.yml if unclear.
&lt;span class="p"&gt;-&lt;/span&gt; Never write to &lt;span class="sb"&gt;`prod_`&lt;/span&gt; targets in generated code.
&lt;span class="p"&gt;-&lt;/span&gt; Never suggest a completion that would fail &lt;span class="sb"&gt;`dbt parse`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Never use &lt;span class="sb"&gt;`now()`&lt;/span&gt; on Snowflake — use &lt;span class="sb"&gt;`current_timestamp()`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Never omit &lt;span class="sb"&gt;`ORDER BY`&lt;/span&gt; inside a window function that needs deterministic ordering.

&lt;span class="gu"&gt;## Guardrails you must respect&lt;/span&gt;
Every completion is checked by:
&lt;span class="p"&gt;  1.&lt;/span&gt; &lt;span class="sb"&gt;`dbt parse`&lt;/span&gt; (unresolved refs fail)
&lt;span class="p"&gt;  2.&lt;/span&gt; &lt;span class="sb"&gt;`dbt compile --select state:modified`&lt;/span&gt; (invalid Jinja fails)
&lt;span class="p"&gt;  3.&lt;/span&gt; &lt;span class="sb"&gt;`sqlfluff lint --dialect snowflake`&lt;/span&gt; (style fails)
&lt;span class="p"&gt;  4.&lt;/span&gt; Human PR read (semantic drift fails)

If a suggestion might fail any of these, ask for clarification instead.
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# hallucination_rate.py — weekly metric
# Run: python hallucination_rate.py --pr-window 7d
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="c1"&gt;# 1. List PRs merged in the last 7 days
&lt;/span&gt;&lt;span class="n"&gt;prs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--state=merged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merged:&amp;gt;=2026-07-23&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number,files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;hallucination_flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;total_prs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="c1"&gt;# 2. For each PR, check the diff against `.cursorrules` do-nots
&lt;/span&gt;&lt;span class="n"&gt;DO_NOTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bfrom airflow\.operators\.python_operator\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bnow\s*\(\s*\)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;# Snowflake: use current_timestamp()
&lt;/span&gt;    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT\s+\*.*FROM\s+\{\{\s*ref\(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# SELECT * in a mart
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total_prs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;diff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DO_NOTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;hallucination_flags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Report
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRs scanned: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_prs&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hallucination_flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cnt&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; PRs violated: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Overall hallucination rate: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hallucination_flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_prs&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.cursorrules&lt;/code&gt; size&lt;/td&gt;
&lt;td&gt;~1300 tokens&lt;/td&gt;
&lt;td&gt;under the 1500-token ceiling; leaves room for file context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sections&lt;/td&gt;
&lt;td&gt;role → shape → naming → style → dbt → Airflow → do-nots → guardrails&lt;/td&gt;
&lt;td&gt;mirrors what a &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; would say&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@Codebase&lt;/code&gt; usage&lt;/td&gt;
&lt;td&gt;mandatory for Composer tasks touching upstream models&lt;/td&gt;
&lt;td&gt;injects schema.yml so LLM sees real columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@Docs&lt;/code&gt; usage&lt;/td&gt;
&lt;td&gt;mandatory for Airflow / dbt API tasks&lt;/td&gt;
&lt;td&gt;prevents deprecated-API suggestions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hygiene routine&lt;/td&gt;
&lt;td&gt;weekly reindex + tab cleanup&lt;/td&gt;
&lt;td&gt;keeps context fresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric&lt;/td&gt;
&lt;td&gt;hallucination rate per merged PR&lt;/td&gt;
&lt;td&gt;weekly grep against do-not patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the 30-day rollout, hallucination-flag rate (do-not-pattern violations in merged PRs) drops from ~15% baseline to ~2%. The two engineers who complained about Cursor "constantly making things up" become the loudest advocates once the &lt;code&gt;.cursorrules&lt;/code&gt; file removes the "it invented a column name" objection. PR throughput doubles; defect rate holds steady.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline (week 0)&lt;/th&gt;
&lt;th&gt;After 30 days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PRs / engineer / week&lt;/td&gt;
&lt;td&gt;4.2&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do-not violations / merged PR&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;dbt parse&lt;/code&gt; failures / PR&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deprecated Airflow API sightings&lt;/td&gt;
&lt;td&gt;5/wk&lt;/td&gt;
&lt;td&gt;0/wk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineer NPS on Cursor&lt;/td&gt;
&lt;td&gt;4/10&lt;/td&gt;
&lt;td&gt;8/10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Versioned &lt;code&gt;.cursorrules&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; — putting the LLM's repo prompt in the repo makes it a first-class deliverable that evolves with the codebase and is reviewed in PRs. The alternative — every engineer keeping their own personal preamble — reintroduces the variance the rules file exists to eliminate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Comment-first prompting&lt;/strong&gt;&lt;/strong&gt; — the natural-language contract you write before the SQL is what pins ghost text to intent. Without it, the LLM guesses from surrounding code, which is where most hallucinations enter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;&lt;code&gt;@Codebase&lt;/code&gt; schema injection&lt;/strong&gt;&lt;/strong&gt; — vector-search injection of &lt;code&gt;schema.yml&lt;/code&gt; for the upstream models the current model references is what stops column-name hallucination in Composer tasks. Two-second retrieval cost; near-eliminates the failure mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Do-not clauses&lt;/strong&gt;&lt;/strong&gt; — telling the LLM what &lt;em&gt;not&lt;/em&gt; to do is more effective than telling it what to do. "Never use &lt;code&gt;airflow.operators.python_operator&lt;/code&gt;" is more effective than "prefer TaskFlow API" because the negation is unambiguous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one &lt;code&gt;.cursorrules&lt;/code&gt; file (~1300 tokens), a weekly 5-minute hygiene routine, and a weekly hallucination-rate metric script. The payback is 2× PR throughput and a ~7× drop in do-not violations. Compared to letting each engineer figure out their own prompt discipline, this is O(1) config for O(N × week) engineer-hours saved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql-generation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL-generation problems for prompt / context practice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Design&lt;/span&gt;
&lt;span&gt;Topic — design&lt;/span&gt;
&lt;strong&gt;Design problems on repo-scoped tooling&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. GitHub Copilot patterns — inline SQL, dbt macros, Jinja completions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Comment-first prompting is the single lever that turns Copilot's ghost text from lucky to reliable
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;github copilot sql&lt;/code&gt; is at its best when you write a comment describing what the SQL &lt;em&gt;should return&lt;/em&gt;, let Copilot draft the ghost text, review it against the comment, and either accept or iterate — the comment is a contract between you and the model, and treating it as a contract (not a hint) is what separates a productive Copilot workflow from a lottery of hopeful autocompletes&lt;/strong&gt;. Every senior data engineer who ships Copilot-authored SQL day-in day-out uses the same three-line pattern: write the intent comment, wait for the ghost text, verify against the comment before accepting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0p06iphd0onr102tmd6m.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0p06iphd0onr102tmd6m.jpeg" alt="Iconographic Copilot inline diagram — an editor card with a comment-first prompt, ghost-text SQL suggestion in grey, a dbt macro completion popup with Jinja tokens, and a small sparkle-glyph indicating LLM proposal." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four modes of GitHub Copilot, and when to use each.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inline ghost text.&lt;/strong&gt; Sub-300-ms completions at the caret. Best for line-by-line SQL, small edits, filling in &lt;code&gt;SELECT&lt;/code&gt; column lists, completing dbt Jinja blocks. Comment-first prompt is the discipline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copilot Chat.&lt;/strong&gt; Conversational; &lt;code&gt;@workspace&lt;/code&gt;, &lt;code&gt;@github&lt;/code&gt;, &lt;code&gt;@vscode&lt;/code&gt; symbols. Best for "explain this query," "generate a test for this dbt model," "why does this DAG fail on the sensor step?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copilot Workspace.&lt;/strong&gt; Multi-file suggestions bundled as PRs. Best for "add pagination to all three list endpoints" or "migrate this dbt project from schema.yml v1 to v2." Longer latency; multi-file scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copilot CLI.&lt;/strong&gt; &lt;code&gt;gh copilot suggest&lt;/code&gt; / &lt;code&gt;gh copilot explain&lt;/code&gt; in the terminal. Best for shell one-liners, git recipes, "how do I run dbt with defer?" from the command line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The comment-first pattern — the load-bearing Copilot discipline.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it is.&lt;/strong&gt; Before writing SQL, write a comment describing what the SQL should return: &lt;code&gt;-- returns customers with 3+ orders in the last 90 days, grouped by tier&lt;/code&gt;. Then hit Enter and let the ghost text fill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it works.&lt;/strong&gt; Copilot's model heavily weights nearby comments as prompt intent. A specific comment produces specific SQL; a vague comment produces vague SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to write good comments.&lt;/strong&gt; Name the output shape (columns + grain), the filter conditions, the aggregation, and the ordering. "returns customers with 3+ orders in the last 90 days, grouped by tier, ordered by total_spend desc" is a five-clause contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to iterate.&lt;/strong&gt; If the ghost text drifts, edit the comment to be more specific — never start typing SQL and expect the LLM to catch up. The comment leads; the SQL follows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Failure modes senior engineers pre-empt.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hallucinated columns.&lt;/strong&gt; Copilot invents &lt;code&gt;customer_key&lt;/code&gt; when the schema has &lt;code&gt;customer_id&lt;/code&gt;. Fix: pull the source table into an open tab; add a &lt;code&gt;-- source: dim_customers has (customer_id, tier, signup_date)&lt;/code&gt; comment above the completion; or use Cursor's &lt;code&gt;@Codebase&lt;/code&gt; if you're in Cursor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong SQL dialect.&lt;/strong&gt; Copilot writes Postgres &lt;code&gt;::TEXT&lt;/code&gt; in a Snowflake file; writes &lt;code&gt;CURRENT_TIMESTAMP&lt;/code&gt; in a MySQL file. Fix: add a &lt;code&gt;-- Snowflake&lt;/code&gt; header comment at the top of the file; put the dialect in &lt;code&gt;.cursorrules&lt;/code&gt; (Cursor) or the workspace instructions (Copilot Chat).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-idiomatic dbt.&lt;/strong&gt; Copilot suggests hardcoded schema references instead of &lt;code&gt;{{ ref() }}&lt;/code&gt;. Fix: keep an example &lt;code&gt;ref()&lt;/code&gt;-based query at the top of the file as an in-context template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deprecated API.&lt;/strong&gt; Copilot suggests &lt;code&gt;airflow.operators.python_operator&lt;/code&gt; (deprecated). Fix: pin the Airflow version in a header comment; use Cursor's &lt;code&gt;.cursorrules&lt;/code&gt; do-not clause.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on Copilot.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you keep Copilot from hallucinating column names?" — comment-first prompt + open the source table's schema.yml in a sibling tab.&lt;/li&gt;
&lt;li&gt;"How do you make Copilot suggest the right SQL dialect?" — file-header dialect comment + &lt;code&gt;.cursorrules&lt;/code&gt; in Cursor.&lt;/li&gt;
&lt;li&gt;"How do you review Copilot-generated dbt models?" — &lt;code&gt;dbt parse&lt;/code&gt; precommit + human read against the schema.yml.&lt;/li&gt;
&lt;li&gt;"When would you prefer Cursor Composer over Copilot inline?" — multi-file edits, repo-context-heavy tasks (e.g. renaming a model across 40 references).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — authoring a window-function query with Copilot inline
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Copilot inline task: rank customers by lifetime spend, keep the top-N per tier, return their most-recent order. The senior data engineer's move is comment-first: write a five-clause comment, hit Enter, let the ghost text fill, verify, accept or iterate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Task.&lt;/strong&gt; Rank customers by lifetime spend within tier; keep top 100 per tier; return their most recent order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discipline.&lt;/strong&gt; Comment describes the exact output shape; ghost text drafts the SQL; human verifies against the comment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dialect.&lt;/strong&gt; Snowflake — file header states so.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Author the query using Copilot inline with a comment-first prompt, and show the ghost-text output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dialect&lt;/td&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stg_orders(customer_id, order_date, total_cents, status)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dimension&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dim_customers(customer_id, tier)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;top 100 customers per tier by lifetime spend, with most-recent order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ordering&lt;/td&gt;
&lt;td&gt;tier ASC, rank ASC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Snowflake&lt;/span&gt;
&lt;span class="c1"&gt;-- returns top 100 customers per tier by lifetime spend,&lt;/span&gt;
&lt;span class="c1"&gt;--   with their most recent order (order_date, total_cents).&lt;/span&gt;
&lt;span class="c1"&gt;-- Grain: one row per customer.&lt;/span&gt;
&lt;span class="c1"&gt;-- Filters: status IN ('paid', 'shipped') only.&lt;/span&gt;

&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;customer_lifetime&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lifetime_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;most_recent_order_date&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dim_customers'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;most_recent_row&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;most_recent_order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_cents&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;most_recent_order_cents&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
    &lt;span class="n"&gt;QUALIFY&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
        &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;     &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lifetime_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;mr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;most_recent_order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;mr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;most_recent_order_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;
               &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;     &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lifetime_cents&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tier_rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;customer_lifetime&lt;/span&gt; &lt;span class="n"&gt;cl&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;most_recent_row&lt;/span&gt;   &lt;span class="n"&gt;mr&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;lifetime_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;most_recent_order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;most_recent_order_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;tier_rank&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;ranked&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;tier_rank&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier_rank&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The five-line comment header is the prompt contract. It names the output (top 100 customers per tier), the grain (one row per customer), and the filter (status IN paid/shipped). Copilot's ghost text draft was ~95% of this final; the engineer accepted with two small tweaks.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;customer_lifetime&lt;/code&gt; CTE aggregates spend per customer per tier. Copilot suggested this shape immediately from the "lifetime spend" phrasing in the comment.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;most_recent_row&lt;/code&gt; CTE uses &lt;code&gt;QUALIFY ROW_NUMBER() = 1&lt;/code&gt; — the Snowflake idiom for "keep the most recent row per key." Copilot suggested &lt;code&gt;QUALIFY&lt;/code&gt; because the file-header comment stated the dialect. Without that comment, Copilot defaults to a subquery / &lt;code&gt;LEFT JOIN&lt;/code&gt; pattern.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ranked&lt;/code&gt; CTE joins the two prior CTEs and computes &lt;code&gt;tier_rank&lt;/code&gt;. Copilot correctly used &lt;code&gt;PARTITION BY tier&lt;/code&gt; + &lt;code&gt;ORDER BY lifetime_cents DESC&lt;/code&gt; — a standard senior pattern.&lt;/li&gt;
&lt;li&gt;The final &lt;code&gt;SELECT&lt;/code&gt; filters &lt;code&gt;tier_rank &amp;lt;= 100&lt;/code&gt; and orders by tier then rank. The &lt;code&gt;ORDER BY&lt;/code&gt; at the end matches the &lt;code&gt;.cursorrules&lt;/code&gt; "ORDER BY only in final SELECT" rule.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;tier&lt;/th&gt;
&lt;th&gt;lifetime_cents&lt;/th&gt;
&lt;th&gt;most_recent_order_date&lt;/th&gt;
&lt;th&gt;most_recent_order_cents&lt;/th&gt;
&lt;th&gt;tier_rank&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4021&lt;/td&gt;
&lt;td&gt;Platinum&lt;/td&gt;
&lt;td&gt;1,240,000&lt;/td&gt;
&lt;td&gt;2026-07-28&lt;/td&gt;
&lt;td&gt;42,000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8801&lt;/td&gt;
&lt;td&gt;Platinum&lt;/td&gt;
&lt;td&gt;980,000&lt;/td&gt;
&lt;td&gt;2026-07-30&lt;/td&gt;
&lt;td&gt;33,000&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2312&lt;/td&gt;
&lt;td&gt;Gold&lt;/td&gt;
&lt;td&gt;540,000&lt;/td&gt;
&lt;td&gt;2026-07-24&lt;/td&gt;
&lt;td&gt;28,000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7793&lt;/td&gt;
&lt;td&gt;Silver&lt;/td&gt;
&lt;td&gt;98,000&lt;/td&gt;
&lt;td&gt;2026-07-15&lt;/td&gt;
&lt;td&gt;5,500&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Write the comment first with five clauses (output columns, grain, filter, aggregation, ordering). Wait for the ghost text. Verify against the comment. Accept or iterate on the comment — never on the SQL. This is the single most transferable Copilot discipline.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — dbt macro completion with Jinja + &lt;code&gt;dbt_utils&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Copilot is uneven on Jinja by default because Jinja is a small fraction of most training data. Anchoring it with a well-formed opening — &lt;code&gt;{% macro ... %}&lt;/code&gt; on the first line, dbt_utils import in a comment above — makes it produce correct Jinja + SQL macros most of the time. Walk through authoring a &lt;code&gt;dbt_utils.pivot&lt;/code&gt;-based macro.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Task.&lt;/strong&gt; A macro that pivots a wide &lt;code&gt;order_events&lt;/code&gt; table into a per-event-type column set (&lt;code&gt;event_type_paid&lt;/code&gt;, &lt;code&gt;event_type_shipped&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt technique.&lt;/strong&gt; Docstring block + macro signature + &lt;code&gt;dbt_utils.pivot&lt;/code&gt; import — Copilot fills the body.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Author the pivot macro using Copilot inline; show the docstring + signature that anchors the completion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Macro name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pivot_order_events&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ ref('stg_order_events') }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pivot column&lt;/td&gt;
&lt;td&gt;&lt;code&gt;event_type&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate&lt;/td&gt;
&lt;td&gt;count of events per type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Group by&lt;/td&gt;
&lt;td&gt;&lt;code&gt;order_id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;#-&lt;/span&gt; &lt;span class="n"&gt;pivot_order_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;Snowflake&lt;/span&gt; &lt;span class="n"&gt;dbt&lt;/span&gt; &lt;span class="n"&gt;macro&lt;/span&gt; &lt;span class="o"&gt;-#&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;#-&lt;/span&gt;
&lt;span class="n"&gt;Pivots&lt;/span&gt; &lt;span class="n"&gt;stg_order_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;wide&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;Args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;dbt&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;pivot&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usually&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_order_events'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;
    &lt;span class="n"&gt;group_by&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usually&lt;/span&gt; &lt;span class="s1"&gt;'order_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;Returns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;group_by&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;group_by&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type_&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;type_1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type_&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;type_2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;-#&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;macro&lt;/span&gt; &lt;span class="n"&gt;pivot_order_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;event_types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dbt_utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_column_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;column&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'event_type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;group_by&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt;
      &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;dbt_utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pivot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="k"&gt;column&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'event_type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event_types&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;agg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'count'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;then_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;else_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'event_type_'&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;group_by&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endmacro&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The opening &lt;code&gt;{#- ... -#}&lt;/code&gt; block is a docstring in Jinja. It tells Copilot what the macro does, what arguments it takes, and what shape it returns. Without it, Copilot's suggestions drift.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;{% macro pivot_order_events(model, group_by) %}&lt;/code&gt; signature is what the engineer typed; Copilot then filled the body. The macro name is descriptive, the args are named — both give Copilot enough context.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;dbt_utils.get_column_values(...)&lt;/code&gt; line dynamically pulls the distinct event types from the input model. Copilot suggested this pattern from the &lt;code&gt;dbt_utils&lt;/code&gt; import comment; without the import hint, it would have generated a static list.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;dbt_utils.pivot(...)&lt;/code&gt; call is the actual pivot. &lt;code&gt;then_value=1, else_value=0, agg='count'&lt;/code&gt; computes a per-type count. &lt;code&gt;prefix='event_type_'&lt;/code&gt; gives clean column names.&lt;/li&gt;
&lt;li&gt;The final &lt;code&gt;GROUP BY {{ group_by }}&lt;/code&gt; closes the aggregation. Copilot correctly used the macro arg — a common failure mode is to hardcode &lt;code&gt;order_id&lt;/code&gt; here; the docstring prevented it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;event_type_paid&lt;/th&gt;
&lt;th&gt;event_type_shipped&lt;/th&gt;
&lt;th&gt;event_type_returned&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For Jinja / dbt macros, anchor Copilot with a full docstring + macro signature + relevant import hints in comments. The docstring is the prompt; the macro body follows.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — comment-first pattern for a complex CTE chain
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A gnarly CTE chain — sessionise events, compute per-session revenue, attribute the session to a marketing channel — is the kind of task that Copilot handles poorly &lt;em&gt;without&lt;/em&gt; a good comment header and handles very well &lt;em&gt;with&lt;/em&gt; one. Walk through the discipline.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Task.&lt;/strong&gt; Sessionise &lt;code&gt;page_events&lt;/code&gt; (30-min inactivity threshold), attribute the first-touch channel per session, join to &lt;code&gt;orders&lt;/code&gt; to compute revenue per session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt technique.&lt;/strong&gt; Multi-clause comment describing each CTE by name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Author the query using a per-CTE comment header, and show the resulting SQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Events&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stg_page_events(user_id, event_ts, url, utm_source)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orders&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stg_orders(customer_id, order_ts, total_cents)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session threshold&lt;/td&gt;
&lt;td&gt;30 minutes of inactivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attribution&lt;/td&gt;
&lt;td&gt;first-touch (first utm_source in the session)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Snowflake&lt;/span&gt;
&lt;span class="c1"&gt;-- Pipeline:&lt;/span&gt;
&lt;span class="c1"&gt;--   1. sessions = sessionise page_events with 30-min inactivity threshold&lt;/span&gt;
&lt;span class="c1"&gt;--   2. session_attribution = first-touch utm_source per session&lt;/span&gt;
&lt;span class="c1"&gt;--   3. session_revenue = orders that happened during the session, summed&lt;/span&gt;
&lt;span class="c1"&gt;--   4. output = one row per session with attribution + revenue&lt;/span&gt;

&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;sessioned&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;utm_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
                 &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;DATEDIFF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;
                      &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
                 &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
               &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_idx&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_page_events'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;sessions&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;session_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_end&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;sessioned&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_idx&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;session_attribution&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;FIRST_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utm_source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_idx&lt;/span&gt;
               &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;     &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_touch_channel&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;sessions&lt;/span&gt;  &lt;span class="n"&gt;s&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;sessioned&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;
      &lt;span class="k"&gt;ON&lt;/span&gt;   &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
     &lt;span class="k"&gt;AND&lt;/span&gt;   &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_idx&lt;/span&gt;
    &lt;span class="n"&gt;QUALIFY&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_idx&lt;/span&gt;
        &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;     &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_ts&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;session_revenue&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_touch_channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;session_revenue_cents&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;session_attribution&lt;/span&gt; &lt;span class="n"&gt;sa&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
      &lt;span class="k"&gt;ON&lt;/span&gt;   &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
     &lt;span class="k"&gt;AND&lt;/span&gt;   &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_ts&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_start&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_end&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;session_revenue&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;session_revenue_cents&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="n"&gt;NULLS&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The header comment lists four CTEs by name and one-line purpose. Copilot uses this as a scaffold — each CTE below matches a comment line. Without the header, the LLM would produce a single monolithic subquery.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sessioned&lt;/code&gt; computes a running session index via a windowed CASE that increments whenever the gap between consecutive events exceeds 30 minutes. Copilot suggested this pattern from the "sessionise" verb in the comment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sessions&lt;/code&gt; aggregates events into session bounds (start/end timestamps). This is the standard session-collapse pattern.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session_attribution&lt;/code&gt; uses &lt;code&gt;QUALIFY ROW_NUMBER() = 1&lt;/code&gt; to pick the first event per session and pulls its &lt;code&gt;utm_source&lt;/code&gt;. Copilot correctly used &lt;code&gt;QUALIFY&lt;/code&gt; for the top-N-per-key idiom.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session_revenue&lt;/code&gt; joins to orders on the time window and sums revenue. The &lt;code&gt;LEFT JOIN&lt;/code&gt; keeps sessions that generated no revenue.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;session_idx&lt;/th&gt;
&lt;th&gt;first_touch_channel&lt;/th&gt;
&lt;th&gt;session_revenue_cents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4021&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;google_ads&lt;/td&gt;
&lt;td&gt;42,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8801&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;organic&lt;/td&gt;
&lt;td&gt;33,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2312&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;facebook_ads&lt;/td&gt;
&lt;td&gt;28,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7793&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;direct&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For complex CTE chains, write a top-of-file comment listing every CTE by name and one-line purpose &lt;em&gt;before&lt;/em&gt; letting Copilot draft. The comment is the outline; the SQL is the fill. This produces cleaner, more auditable SQL than any single-shot prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on Copilot patterns
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're leading an analytics team on a Snowflake + dbt stack. GitHub Copilot has been available to the team for six months. Half the engineers use it well; half have shipped subtly-wrong SQL because Copilot invented column names. Walk me through the Copilot discipline you'd standardise — the comment-first pattern, the file-header conventions, the hallucination defenses — and how you'd measure the impact on shipped-defect rate."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using comment-first prompting, file-header dialect pins, and a per-PR hallucination-flag script
&lt;/h3&gt;



&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- COPILOT WORKFLOW TEMPLATE (paste at top of new .sql files)&lt;/span&gt;
&lt;span class="c1"&gt;-- Dialect: Snowflake (dbt 1.8, snowflake_prod / snowflake_dev)&lt;/span&gt;
&lt;span class="c1"&gt;-- Source: {{ ref('stg_orders') }}, {{ ref('dim_customers') }}&lt;/span&gt;
&lt;span class="c1"&gt;-- Grain: one row per customer per tier&lt;/span&gt;
&lt;span class="c1"&gt;-- Filters: status IN ('paid','shipped'); order_date &amp;gt;= dateadd(day, -730, current_date)&lt;/span&gt;
&lt;span class="c1"&gt;-- Output: customer_id, tier, lifetime_cents, tier_rank&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 1 — comment describes what the SQL returns&lt;/span&gt;
&lt;span class="c1"&gt;-- Step 2 — wait for Copilot ghost text&lt;/span&gt;
&lt;span class="c1"&gt;-- Step 3 — verify against the comment&lt;/span&gt;
&lt;span class="c1"&gt;-- Step 4 — accept, or edit the comment (never the SQL) to iterate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# copilot_hallucination_scan.py — precommit hook
# Scans staged .sql / .py files for known hallucination patterns.
# Runs on `git commit`; blocks commit if any pattern matches.
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;STAGED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;diff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--name-only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# Wrong-dialect timestamp on Snowflake
&lt;/span&gt;    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bnow\s*\(\s*\)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong dialect (use current_timestamp() on Snowflake)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Deprecated Airflow operator
&lt;/span&gt;    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bfrom airflow\.operators\.python_operator\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deprecated Airflow API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Hardcoded schema reference in a dbt model
&lt;/span&gt;    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bFROM\s+[a-z_]+\.[a-z_]+\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hardcoded schema — use {{ ref() }}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# SELECT * in a mart
&lt;/span&gt;    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT\s+\*\s+FROM\s+\{\{\s*ref\(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * in a mart — list columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;violations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;STAGED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PATTERNS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()].&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  → &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Copilot hallucination scan flagged the following:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Edit and re-commit, or run `git commit --no-verify` if intentional.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .pre-commit-config.yaml — add the scan&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;copilot-hallucination-scan&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;copilot hallucination scan&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/copilot_hallucination_scan.py&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;commit&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;File header&lt;/td&gt;
&lt;td&gt;5-clause comment (dialect, source, grain, filters, output)&lt;/td&gt;
&lt;td&gt;pins Copilot's prompt intent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comment-first&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;td&gt;write comment first, SQL after&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt parse&lt;/td&gt;
&lt;td&gt;precommit&lt;/td&gt;
&lt;td&gt;catches unresolved refs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqlfluff lint&lt;/td&gt;
&lt;td&gt;precommit&lt;/td&gt;
&lt;td&gt;catches style drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucination-flag scan&lt;/td&gt;
&lt;td&gt;precommit&lt;/td&gt;
&lt;td&gt;catches wrong-dialect + deprecated API + hardcoded schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human PR review&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;td&gt;verifies output against the comment contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly hallucination-rate metric&lt;/td&gt;
&lt;td&gt;tracked&lt;/td&gt;
&lt;td&gt;catches convention drift over time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the 30-day rollout of the comment-first template + hallucination scan, the shipped-defect rate on Copilot-authored SQL drops from ~15% to ~2%. The two engineers who "shipped subtly-wrong SQL" become the loudest advocates of the template because the precommit scan now catches what code review used to miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline (week 0)&lt;/th&gt;
&lt;th&gt;After 30 days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PRs / engineer / week&lt;/td&gt;
&lt;td&gt;3.5&lt;/td&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot-authored SQL rows&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shipped defects / 100 PRs&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong-dialect flags / week&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardcoded-schema flags / week&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Comment-first prompting&lt;/strong&gt;&lt;/strong&gt; — the header comment is a contract between the engineer's intent and Copilot's draft. Writing it first anchors the ghost text; iterating on it (not on the SQL) keeps the discipline pure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;File-header dialect pin&lt;/strong&gt;&lt;/strong&gt; — a single-line &lt;code&gt;-- Snowflake&lt;/code&gt; at the top of every SQL file eliminates the wrong-dialect failure mode. This is the smallest possible fix for the highest-frequency hallucination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Hallucination-flag precommit scan&lt;/strong&gt;&lt;/strong&gt; — a lightweight regex-based scanner catches the top-4 do-not patterns (wrong dialect, deprecated Airflow API, hardcoded schema, SELECT * in mart) before commit. Blocks the commit; forces a fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Human review against the comment contract&lt;/strong&gt;&lt;/strong&gt; — the PR reviewer's first check is "does the SQL do what the comment says?" This turns review from a "read the code" chore into a "verify the contract" audit, which is faster &lt;em&gt;and&lt;/em&gt; higher-signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one file-header template (paste-at-top-of-new-files), one 30-line hallucination scan, one PR-template checkbox. The payback is a ~7× drop in shipped-defect rate and 2× PR throughput. Compared to letting Copilot roam free, this is O(1) config for O(N × week) engineer-hours saved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — joins/sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL joins and window-function problems for Copilot practice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/joins/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL&lt;/span&gt;
&lt;span&gt;Topic — window-functions/sql&lt;/span&gt;
&lt;strong&gt;Window-function drills for Copilot ghost text&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/window-functions/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Airflow DAG authoring with LLM assistants — task groups, sensors, tests
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Docstring-first DAG scaffolding is what turns Copilot / Cursor from "generic Python assistant" into "senior data engineer who knows the TaskFlow API"
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;airflow dag copilot&lt;/code&gt; authoring works best when you write a top-of-file docstring stating the DAG's purpose, schedule, owner, retry policy, and task-list — the LLM then scaffolds the &lt;code&gt;@dag&lt;/code&gt; and &lt;code&gt;@task&lt;/code&gt; decorators, proposes sensors and task groups, and generates the pytest fixture — turning a 60-minute DAG-drafting task into a 10-minute review-and-tweak task while keeping the reviewer in charge of every architectural choice&lt;/strong&gt;. Every senior data engineer who ships LLM-authored DAGs uses the same pattern: docstring first, decorators second, business logic third, tests fourth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fix7g9t80sntpe0emjsve.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fix7g9t80sntpe0emjsve.jpeg" alt="Iconographic Airflow DAG diagram — a directed graph of task nodes bundled inside a task-group frame, an LLM-sparkle glyph proposing a sensor node and a pytest node, and a small chip 'docstring-first'." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes of a good LLM-authored DAG.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docstring shape.&lt;/strong&gt; Every DAG has a top-of-file docstring: purpose, schedule (&lt;code&gt;@daily&lt;/code&gt;, cron), owner, upstream dependencies, retry policy, SLAs. The docstring is the prompt for everything below it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TaskFlow API.&lt;/strong&gt; &lt;code&gt;@dag&lt;/code&gt;, &lt;code&gt;@task&lt;/code&gt;, &lt;code&gt;@task_group&lt;/code&gt; decorators from &lt;code&gt;airflow.decorators&lt;/code&gt;. The LLM must never generate &lt;code&gt;airflow.operators.python_operator&lt;/code&gt; (deprecated in 2.8, removed in 3.0). &lt;code&gt;.cursorrules&lt;/code&gt; do-not clause; Copilot header pin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensors.&lt;/strong&gt; Explicit sensor operators (&lt;code&gt;S3KeySensor&lt;/code&gt;, &lt;code&gt;SnowflakeSensor&lt;/code&gt;, &lt;code&gt;TimeDeltaSensor&lt;/code&gt;) — never poll in a &lt;code&gt;PythonOperator&lt;/code&gt;. The LLM proposes; the reviewer verifies against the upstream reality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests.&lt;/strong&gt; Every DAG has a pytest fixture that (a) imports the DAG, (b) asserts task list, (c) asserts dependency graph, (d) mocks external operators. The LLM scaffolds all four; the reviewer verifies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The docstring-first pattern — the load-bearing DAG-authoring discipline.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it is.&lt;/strong&gt; Write a 5–8-line module docstring at the top of the file describing the DAG. Then let the LLM scaffold the DAG below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it works.&lt;/strong&gt; The docstring is the prompt intent. A specific docstring produces a specific DAG; a vague docstring produces a vague DAG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to write good docstrings.&lt;/strong&gt; Name the purpose (what data flows where), the schedule (cron / preset), the owner (team + on-call), the upstream (what triggers this DAG), the retry policy (how many, how long between).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to iterate.&lt;/strong&gt; If the LLM's DAG drifts, edit the docstring — never the code below. The docstring leads; the DAG follows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Failure modes senior engineers pre-empt.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deprecated operator API.&lt;/strong&gt; LLM suggests &lt;code&gt;PythonOperator&lt;/code&gt; instead of &lt;code&gt;@task&lt;/code&gt;, or &lt;code&gt;S3ListOperator&lt;/code&gt; instead of &lt;code&gt;S3ListOperator&lt;/code&gt; from the correct provider path. Fix: &lt;code&gt;.cursorrules&lt;/code&gt; do-not clause pinning Airflow 2.9 TaskFlow API; file-header &lt;code&gt;-- Airflow 2.9&lt;/code&gt; comment for Copilot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing retries on network operators.&lt;/strong&gt; LLM writes a &lt;code&gt;SnowflakeOperator&lt;/code&gt; with no &lt;code&gt;retries=&lt;/code&gt; kwarg. Fix: docstring names the retry policy; &lt;code&gt;.cursorrules&lt;/code&gt; requires &lt;code&gt;retries=3&lt;/code&gt; on network operators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensor polling in PythonOperator.&lt;/strong&gt; LLM writes &lt;code&gt;while not s3.head_object(...)&lt;/code&gt; in a task. Fix: docstring names the sensor explicitly ("wait for &lt;code&gt;s3://bucket/{{ ds }}/_SUCCESS&lt;/code&gt; with &lt;code&gt;S3KeySensor&lt;/code&gt;").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No pytest fixture.&lt;/strong&gt; LLM ships a DAG without any test. Fix: PR template requires "pytest for DAG"; &lt;code&gt;.cursorrules&lt;/code&gt; do-not clause "never ship a DAG without a pytest fixture."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on LLM-authored DAGs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do you keep the LLM from generating a deprecated Airflow API?" — &lt;code&gt;.cursorrules&lt;/code&gt; do-not clause + file-header dialect pin + &lt;code&gt;@Docs airflow-2.9&lt;/code&gt; injection.&lt;/li&gt;
&lt;li&gt;"How do you review LLM-generated DAGs?" — pytest-first, then human read against the docstring contract.&lt;/li&gt;
&lt;li&gt;"How do you make the LLM propose the right sensor?" — name the sensor in the docstring; put the sensor's expected key format in a comment.&lt;/li&gt;
&lt;li&gt;"How do you test LLM-generated DAGs?" — pytest that imports the DAG, asserts task list, asserts dependency graph, mocks external operators.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — a Snowflake → S3 DAG with a sensor and a task group
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical LLM-assisted DAG task: ingest a daily partition from Snowflake into S3, gated by an upstream &lt;code&gt;_SUCCESS&lt;/code&gt; marker sensor, with a task group for the extract → transform → load steps, then run a downstream &lt;code&gt;dbt run --select&lt;/code&gt; step. Walk through the docstring-first scaffold.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose.&lt;/strong&gt; Daily incremental extract from Snowflake to S3, then trigger downstream dbt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensor.&lt;/strong&gt; &lt;code&gt;S3KeySensor&lt;/code&gt; on &lt;code&gt;s3://raw/orders/{{ ds }}/_SUCCESS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task group.&lt;/strong&gt; &lt;code&gt;extract_load&lt;/code&gt; containing three tasks: &lt;code&gt;extract_orders&lt;/code&gt;, &lt;code&gt;write_to_s3&lt;/code&gt;, &lt;code&gt;verify_s3_row_count&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downstream.&lt;/strong&gt; &lt;code&gt;dbt_run&lt;/code&gt; task using the &lt;code&gt;DbtRunOperator&lt;/code&gt; from the community provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the module docstring; let Cursor Composer scaffold the DAG; verify against the docstring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DAG id&lt;/td&gt;
&lt;td&gt;&lt;code&gt;orders_ingest_daily&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@daily&lt;/code&gt; (00:15 UTC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;S3KeySensor&lt;/code&gt; on &lt;code&gt;_SUCCESS&lt;/code&gt; marker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task group&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extract_load&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downstream&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dbt run --select fct_orders+&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
DAG: orders_ingest_daily

Purpose:
    Daily incremental extract of orders from Snowflake to S3,
    then trigger downstream dbt run on fct_orders and its children.

Schedule:
    @daily (00:15 UTC).

Upstream:
    S3 marker at s3://raw/orders/{{ ds }}/_SUCCESS
    (published by the upstream ingest DAG).

Owner:
    data-platform-team; on-call: pagerduty analytics-oncall.

Retry policy:
    retries=3, retry_delay=timedelta(minutes=5), retry_exponential_backoff=True.

SLA:
    Complete within 90 minutes of scheduled start; page if breached.

Tasks:
    1. wait_for_marker  — S3KeySensor on the _SUCCESS marker.
    2. extract_load     — task_group:
         a. extract_orders     — Snowflake -&amp;gt; DataFrame.
         b. write_to_s3        — DataFrame -&amp;gt; parquet at s3://curated/orders/{{ ds }}/.
         c. verify_s3_row_count — sanity check: row count matches source.
    3. dbt_run          — dbt run --select fct_orders+.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.decorators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_group&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.sensors.filesystem&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FileSensor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.amazon.aws.sensors.s3&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;S3KeySensor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.snowflake.hooks.snowflake&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SnowflakeHook&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.amazon.aws.hooks.s3&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;S3Hook&lt;/span&gt;

&lt;span class="n"&gt;DEFAULT_ARGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-platform-team&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry_delay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry_exponential_backoff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_on_failure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sla&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nd"&gt;@dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders_ingest_daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DEFAULT_ARGS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15 0 * * *&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# 00:15 UTC daily
&lt;/span&gt;    &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;doc_md&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;__doc__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;orders_ingest_daily&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

    &lt;span class="n"&gt;wait_for_marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;S3KeySensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wait_for_marker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bucket_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders/{{ ds }}/_SUCCESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bucket_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;aws_conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;poke_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# 2 hours
&lt;/span&gt;        &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reschedule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# release worker slot while poking
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@task_group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extract_load&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

        &lt;span class="nd"&gt;@task&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Read the ingested partition from raw.orders into a Snowflake stage.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
            &lt;span class="n"&gt;hook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SnowflakeHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snowflake_conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake_prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                SELECT COUNT(*) AS n
                FROM   raw.orders
                WHERE  order_date = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n_source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])}&lt;/span&gt;

        &lt;span class="nd"&gt;@task&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_to_s3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extract_meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Unload the partition to S3 as parquet.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
            &lt;span class="n"&gt;hook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SnowflakeHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snowflake_conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake_prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                COPY INTO @s3_curated/orders/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/
                FROM (SELECT * FROM raw.orders WHERE order_date = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
                FILE_FORMAT = (TYPE = PARQUET)
                HEADER = TRUE
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n_source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;extract_meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n_source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

        &lt;span class="nd"&gt;@task&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_s3_row_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write_meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Assert the parquet row count matches the source row count.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
            &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;S3Hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws_conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;n_written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_parquet_row_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n_written&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;write_meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n_source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row count mismatch: source=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;write_meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n_source&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;written=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n_written&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;write_meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;write_to_s3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;verify_s3_row_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write_meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dbt_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;dbt run --select fct_orders+ --vars {{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;run_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;: execution_date}}.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--select&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fct_orders+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--vars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{run_date: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--profiles-dir&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/opt/dbt/profiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;wait_for_marker&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;extract_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{ ds }}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;dbt_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{ ds }}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_parquet_row_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Read parquet footer for the row count without downloading data.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pyarrow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parquet&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_conn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;footer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParquetFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;footer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_rows&lt;/span&gt;


&lt;span class="n"&gt;orders_ingest_daily&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;orders_ingest_daily&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The 20-line module docstring is the LLM's prompt. It names the DAG id, purpose, schedule, upstream, owner, retry policy, SLA, and — crucially — a task list. Cursor Composer scaffolded 95% of the code from this docstring; the engineer accepted with two small tweaks (adding &lt;code&gt;mode="reschedule"&lt;/code&gt; to the sensor and adding the parquet footer row-count helper).&lt;/li&gt;
&lt;li&gt;The imports are all from &lt;code&gt;airflow.decorators&lt;/code&gt; and &lt;code&gt;airflow.providers.*&lt;/code&gt; — no &lt;code&gt;airflow.operators.python_operator&lt;/code&gt;. The &lt;code&gt;.cursorrules&lt;/code&gt; do-not clause enforced this.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFAULT_ARGS&lt;/code&gt; centralises the retry policy and SLA. The LLM proposed this shape from the docstring's retry-policy line.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@dag(...)&lt;/code&gt; decorator is TaskFlow API. &lt;code&gt;doc_md=__doc__&lt;/code&gt; renders the module docstring in the Airflow UI — a small but high-value discipline.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wait_for_marker&lt;/code&gt; uses &lt;code&gt;S3KeySensor&lt;/code&gt; with &lt;code&gt;mode="reschedule"&lt;/code&gt; (release the worker slot between pokes). The engineer added &lt;code&gt;mode&lt;/code&gt; because the LLM's first draft used the default &lt;code&gt;mode="poke"&lt;/code&gt; — a subtle but important production hygiene.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extract_load&lt;/code&gt; is a &lt;code&gt;@task_group&lt;/code&gt; with three tasks: &lt;code&gt;extract_orders&lt;/code&gt;, &lt;code&gt;write_to_s3&lt;/code&gt;, &lt;code&gt;verify_s3_row_count&lt;/code&gt;. The task group frames them in the UI as one collapsed node.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dbt_run&lt;/code&gt; shells out to the dbt CLI. The subprocess call is deliberately explicit — the community &lt;code&gt;DbtRunOperator&lt;/code&gt; has API drift between provider versions, so calling the CLI directly is more portable.&lt;/li&gt;
&lt;li&gt;The dependency graph at the bottom (&lt;code&gt;wait_for_marker &amp;gt;&amp;gt; extract_load(...) &amp;gt;&amp;gt; dbt_run(...)&lt;/code&gt;) is the DAG's shape. Explicit &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; operators are more readable than &lt;code&gt;task_id&lt;/code&gt; strings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Retries&lt;/th&gt;
&lt;th&gt;SLA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;wait_for_marker&lt;/td&gt;
&lt;td&gt;S3KeySensor&lt;/td&gt;
&lt;td&gt;3 (via default_args)&lt;/td&gt;
&lt;td&gt;90 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extract_load.extract_orders&lt;/td&gt;
&lt;td&gt;@task&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;inherited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extract_load.write_to_s3&lt;/td&gt;
&lt;td&gt;@task&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;inherited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extract_load.verify_s3_row_count&lt;/td&gt;
&lt;td&gt;@task&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;inherited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt_run&lt;/td&gt;
&lt;td&gt;@task (subprocess)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;inherited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Write the module docstring first (purpose, schedule, upstream, owner, retry, SLA, task list). Let the LLM scaffold from it. Verify each task against the docstring line. Add production-hygiene tweaks (&lt;code&gt;mode="reschedule"&lt;/code&gt; on sensors, explicit &lt;code&gt;retry_exponential_backoff&lt;/code&gt;) manually — the LLM misses these by default.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — LLM-generated pytest for the DAG
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Every LLM-authored DAG must ship with a pytest fixture that (a) imports the DAG file, (b) asserts the DAG loads without errors, (c) asserts the task list matches expectation, (d) asserts the dependency graph is correct, (e) mocks external operators for smoke-test. The LLM scaffolds all five; the reviewer verifies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt.&lt;/strong&gt; "Generate pytest for the &lt;code&gt;orders_ingest_daily&lt;/code&gt; DAG covering import, task list, dependency graph, and operator mocks."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convention.&lt;/strong&gt; Every test file matches &lt;code&gt;test_&amp;lt;dag_name&amp;gt;.py&lt;/code&gt;; sits in &lt;code&gt;tests/dags/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Author the pytest fixture using Copilot Chat; show the resulting test file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assertion&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DAG imports without error&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task count&lt;/td&gt;
&lt;td&gt;5 (marker + 3 in group + dbt_run)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task ids&lt;/td&gt;
&lt;td&gt;expected set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency graph&lt;/td&gt;
&lt;td&gt;wait_for_marker → extract_load → dbt_run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLA&lt;/td&gt;
&lt;td&gt;90 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/dags/test_orders_ingest_daily.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DagBag&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;module&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DagBag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Load all DAGs; fail fast if any DAG file has a parse error.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DagBag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag_folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dags/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;include_examples&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_orders_ingest_daily_imports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;DAG file must load cleanly (no syntax / import errors).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders_ingest_daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dag_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;import_errors&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;import_errors&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_orders_ingest_daily_task_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Task list must match the docstring.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;dag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders_ingest_daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;expected_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wait_for_marker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extract_load.extract_orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extract_load.write_to_s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extract_load.verify_s3_row_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;actual_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;actual_ids&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unexpected task set — actual: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actual_ids&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, expected: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected_ids&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_orders_ingest_daily_dependencies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Dependency graph must be wait_for_marker -&amp;gt; extract_load -&amp;gt; dbt_run.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;dag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders_ingest_daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wait_for_marker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;extract_first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extract_load.extract_orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dbt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Marker feeds into the first task of the task group
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;extract_first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;downstream_list&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wait_for_marker should feed extract_load.extract_orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# verify_s3_row_count feeds into dbt_run
&lt;/span&gt;    &lt;span class="n"&gt;verify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extract_load.verify_s3_row_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;dbt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;downstream_list&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify_s3_row_count should feed dbt_run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_orders_ingest_daily_sla&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;SLA must be 90 minutes on every task.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;dag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders_ingest_daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sla&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; SLA &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; != 90 min&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_orders_ingest_daily_retries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Retries must be 3 on every task (via default_args).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;dag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders_ingest_daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; retries &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; != 3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_orders_ingest_daily_sensor_reschedule_mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The S3KeySensor must run in &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reschedule&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; mode (release worker slot).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;dag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders_ingest_daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sensor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wait_for_marker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;sensor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reschedule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sensor mode &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sensor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; != &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reschedule&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; — worker slots not released&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;dagbag&lt;/code&gt; fixture loads every DAG under &lt;code&gt;dags/&lt;/code&gt; and surfaces import errors. &lt;code&gt;include_examples=False&lt;/code&gt; skips the Airflow-shipped examples.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_orders_ingest_daily_imports&lt;/code&gt; fails fast on any parse error. This is the single highest-value test — most LLM-generated DAG bugs are import-time errors.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_orders_ingest_daily_task_list&lt;/code&gt; asserts the exact set of task ids. If the LLM later "improves" the DAG and adds a task, this test surfaces the change in review.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_orders_ingest_daily_dependencies&lt;/code&gt; walks the dependency graph and asserts the two critical edges: marker → first extract task, verify → dbt_run. A three-node DAG generates three edges; this covers the two that matter.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_orders_ingest_daily_sla&lt;/code&gt; and &lt;code&gt;test_orders_ingest_daily_retries&lt;/code&gt; verify the production-hygiene defaults from &lt;code&gt;default_args&lt;/code&gt;. These often regress silently when someone edits the DAG.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_orders_ingest_daily_sensor_reschedule_mode&lt;/code&gt; catches the "sensor is still in &lt;code&gt;poke&lt;/code&gt; mode and hogging worker slots" regression — a specific production incident that this test prevents from recurring.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Assertion&lt;/th&gt;
&lt;th&gt;Fails when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;imports&lt;/td&gt;
&lt;td&gt;DAG loads cleanly&lt;/td&gt;
&lt;td&gt;any syntax / import error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task_list&lt;/td&gt;
&lt;td&gt;exact 5 task ids&lt;/td&gt;
&lt;td&gt;task added / removed / renamed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dependencies&lt;/td&gt;
&lt;td&gt;marker → extract → dbt_run edges&lt;/td&gt;
&lt;td&gt;dep graph rewired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sla&lt;/td&gt;
&lt;td&gt;90 min on every task&lt;/td&gt;
&lt;td&gt;default_args edited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retries&lt;/td&gt;
&lt;td&gt;3 on every task&lt;/td&gt;
&lt;td&gt;default_args edited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sensor_reschedule_mode&lt;/td&gt;
&lt;td&gt;S3KeySensor uses reschedule&lt;/td&gt;
&lt;td&gt;someone reverts to poke&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every LLM-authored DAG ships with a pytest fixture that asserts (a) import, (b) task list, (c) dependency graph, (d) production-hygiene defaults (SLA, retries, sensor mode). The LLM drafts the fixture from the docstring; the reviewer verifies.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the LLM-proposed sensor pattern
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A common LLM anti-pattern is to write a polling loop inside a &lt;code&gt;PythonOperator&lt;/code&gt; instead of using an explicit sensor. The docstring-first pattern preempts this by naming the sensor. Walk through the fix.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anti-pattern.&lt;/strong&gt; &lt;code&gt;while True: if s3.head_object(...): break; time.sleep(60)&lt;/code&gt; inside a task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; Explicit &lt;code&gt;S3KeySensor&lt;/code&gt; operator; docstring names the sensor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discipline.&lt;/strong&gt; &lt;code&gt;.cursorrules&lt;/code&gt; do-not clause: "never poll in a PythonOperator — use the appropriate Sensor."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Show the anti-pattern, the fix, and the &lt;code&gt;.cursorrules&lt;/code&gt; clause that preempts it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Anti-pattern&lt;/th&gt;
&lt;th&gt;Correct pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Task type&lt;/td&gt;
&lt;td&gt;@task with while loop&lt;/td&gt;
&lt;td&gt;S3KeySensor operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker slot&lt;/td&gt;
&lt;td&gt;held throughout poll&lt;/td&gt;
&lt;td&gt;released with mode='reschedule'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;operator kwarg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retries&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;operator kwarg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI visibility&lt;/td&gt;
&lt;td&gt;opaque&lt;/td&gt;
&lt;td&gt;first-class node&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ANTI-PATTERN — never do this
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.decorators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="nd"&gt;@dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bad_dag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bad_dag&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for_s3_marker&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Wait for the _SUCCESS marker (WRONG — polls in a PythonOperator).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.amazon.aws.hooks.s3&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;S3Hook&lt;/span&gt;
        &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;S3Hook&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders/2026-07-30/_SUCCESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# holds the worker slot for hours
&lt;/span&gt;
    &lt;span class="nf"&gt;wait_for_s3_marker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;bad_dag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bad_dag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# CORRECT — use the S3KeySensor
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.decorators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.amazon.aws.sensors.s3&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;S3KeySensor&lt;/span&gt;

&lt;span class="nd"&gt;@dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;good_dag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;good_dag&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nc"&gt;S3KeySensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wait_for_s3_marker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bucket_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders/{{ ds }}/_SUCCESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bucket_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;aws_conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;poke_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# 6-hour ceiling
&lt;/span&gt;        &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reschedule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# releases worker slot between pokes
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;good_dag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;good_dag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# .cursorrules — the clause that preempts this&lt;/span&gt;
&lt;span class="gu"&gt;## Do-nots&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never poll in a PythonOperator or @task. Use the appropriate Sensor
  (S3KeySensor, SnowflakeSensor, TimeDeltaSensor).
&lt;span class="p"&gt;-&lt;/span&gt; Every Sensor MUST set &lt;span class="sb"&gt;`mode='reschedule'`&lt;/span&gt; unless the sensor is expected
  to complete within &lt;span class="sb"&gt;`poke_interval`&lt;/span&gt; seconds — release worker slots.
&lt;span class="p"&gt;-&lt;/span&gt; Every Sensor MUST set &lt;span class="sb"&gt;`timeout`&lt;/span&gt; explicitly — no infinite pokes.
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The anti-pattern is subtle because it works — the task waits for the marker and eventually completes. The problem is operational: it holds a worker slot for hours, it doesn't surface in the Airflow UI as a sensor node, and it doesn't participate in the SLA / retry policy the way sensors do.&lt;/li&gt;
&lt;li&gt;The correct pattern uses &lt;code&gt;S3KeySensor&lt;/code&gt; from &lt;code&gt;airflow.providers.amazon.aws.sensors.s3&lt;/code&gt;. &lt;code&gt;mode="reschedule"&lt;/code&gt; releases the worker slot between pokes — the scheduler wakes the task every &lt;code&gt;poke_interval&lt;/code&gt; seconds without keeping a slot warm.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timeout=60 * 60 * 6&lt;/code&gt; sets a 6-hour ceiling. Without it, a stuck upstream would wedge the task forever. Explicit timeouts are a senior discipline.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;poke_interval=60&lt;/code&gt; sets the poll cadence. For an &lt;code&gt;_SUCCESS&lt;/code&gt; marker that typically appears within 30 minutes, 60 seconds is a reasonable balance between latency and API load.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;.cursorrules&lt;/code&gt; do-not clauses ("never poll in a PythonOperator," "every Sensor MUST set mode='reschedule'") are what preempt the anti-pattern in the LLM's suggestions. Without them, LLMs default to polling loops ~30% of the time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Anti-pattern&lt;/th&gt;
&lt;th&gt;Correct pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Worker slot&lt;/td&gt;
&lt;td&gt;held for hours&lt;/td&gt;
&lt;td&gt;released between pokes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI visibility&lt;/td&gt;
&lt;td&gt;one opaque task&lt;/td&gt;
&lt;td&gt;first-class sensor node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry semantics&lt;/td&gt;
&lt;td&gt;inside try/except&lt;/td&gt;
&lt;td&gt;operator kwarg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout enforcement&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;operator kwarg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLA compatibility&lt;/td&gt;
&lt;td&gt;broken (custom loop)&lt;/td&gt;
&lt;td&gt;first-class&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every wait-for-external-condition step is a Sensor, not a PythonOperator with a loop. &lt;code&gt;.cursorrules&lt;/code&gt; do-not clause + explicit &lt;code&gt;mode="reschedule"&lt;/code&gt; + explicit &lt;code&gt;timeout=&lt;/code&gt; are the three defenses that preempt this LLM failure mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on Airflow DAG authoring
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You inherit an Airflow deployment where half the DAGs were LLM-authored by junior engineers. Sensors are polling in PythonOperators, retry policies are inconsistent, and there's no pytest coverage. Walk me through the standardisation — the docstring-first template, the &lt;code&gt;.cursorrules&lt;/code&gt; do-not clauses, the pytest fixture pattern, and the CI checks that catch regressions."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a docstring-first template, TaskFlow-only &lt;code&gt;.cursorrules&lt;/code&gt;, and mandatory pytest fixtures
&lt;/h3&gt;



&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dags/_template.py — the standard DAG template, versioned in the repo
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
DAG: &amp;lt;dag_id&amp;gt;

Purpose:
    &amp;lt;what data flows where — one sentence&amp;gt;

Schedule:
    &amp;lt;@daily / cron&amp;gt;

Upstream:
    &amp;lt;what triggers this DAG — sensor keys, external DAG, TimeDeltaSensor&amp;gt;

Owner:
    &amp;lt;team&amp;gt;; on-call: &amp;lt;pagerduty rotation&amp;gt;

Retry policy:
    retries=3, retry_delay=timedelta(minutes=5), retry_exponential_backoff=True

SLA:
    &amp;lt;N&amp;gt; minutes; page if breached

Tasks:
    1. &amp;lt;task_id_1&amp;gt; — &amp;lt;purpose&amp;gt;
    2. &amp;lt;task_id_2&amp;gt; — &amp;lt;purpose&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;    ...&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.decorators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_group&lt;/span&gt;

&lt;span class="n"&gt;DEFAULT_ARGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;team&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry_delay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry_exponential_backoff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_on_failure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sla&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nd"&gt;@dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;dag_id&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DEFAULT_ARGS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;schedule&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;domain&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;doc_md&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;__doc__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nf"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/dag-checks.yml — CI guardrails&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dag-checks&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dag-parse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dag import test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;python -c "&lt;/span&gt;
          &lt;span class="s"&gt;from airflow.models import DagBag&lt;/span&gt;
          &lt;span class="s"&gt;db = DagBag('dags/', include_examples=False)&lt;/span&gt;
          &lt;span class="s"&gt;if db.import_errors:&lt;/span&gt;
              &lt;span class="s"&gt;for f, e in db.import_errors.items():&lt;/span&gt;
                  &lt;span class="s"&gt;print(f'{f}: {e}')&lt;/span&gt;
              &lt;span class="s"&gt;raise SystemExit(1)&lt;/span&gt;
          &lt;span class="s"&gt;print(f'loaded {len(db.dag_ids)} dags')&lt;/span&gt;
          &lt;span class="s"&gt;"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest for dags&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest tests/dags/ -q&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dag-lint (deprecated APIs)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if grep -rn "airflow.operators.python_operator" dags/; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "deprecated API found"; exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
          &lt;span class="s"&gt;if grep -rn "PythonOperator" dags/ | grep -v test_; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "PythonOperator found — prefer @task"; exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# .cursorrules additions for DAG authoring&lt;/span&gt;
&lt;span class="gu"&gt;## Airflow do-nots&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never import from airflow.operators.python_operator (deprecated 2.8, removed 3.0).
&lt;span class="p"&gt;-&lt;/span&gt; Never use PythonOperator — always @task (TaskFlow API).
&lt;span class="p"&gt;-&lt;/span&gt; Never poll in a @task or PythonOperator — use the appropriate Sensor.
&lt;span class="p"&gt;-&lt;/span&gt; Every Sensor must set &lt;span class="sb"&gt;`mode='reschedule'`&lt;/span&gt; and explicit &lt;span class="sb"&gt;`timeout`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Every network operator (SnowflakeOperator, S3Copy, etc.) inherits &lt;span class="sb"&gt;`retries=3`&lt;/span&gt; from default_args — never override to a lower value.
&lt;span class="p"&gt;-&lt;/span&gt; Every DAG file has a module docstring with: purpose, schedule, upstream, owner, retry policy, SLA, task list.
&lt;span class="p"&gt;-&lt;/span&gt; Every DAG file has a matching tests/dags/test_&lt;span class="nt"&gt;&amp;lt;dag_id&amp;gt;&lt;/span&gt;.py fixture asserting: import, task list, dependency graph, SLA, retries.
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DAG template&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dags/_template.py&lt;/code&gt; versioned&lt;/td&gt;
&lt;td&gt;new DAGs start from a compliant scaffold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docstring&lt;/td&gt;
&lt;td&gt;7-line block: purpose, schedule, upstream, owner, retries, SLA, tasks&lt;/td&gt;
&lt;td&gt;prompts the LLM correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TaskFlow API&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;td&gt;via &lt;code&gt;.cursorrules&lt;/code&gt; do-not + CI grep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensor mode&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reschedule&lt;/code&gt; + explicit &lt;code&gt;timeout&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;releases worker slots; no infinite pokes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pytest fixture&lt;/td&gt;
&lt;td&gt;mandatory sibling in &lt;code&gt;tests/dags/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;CI blocks merge if missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI grep for deprecated API&lt;/td&gt;
&lt;td&gt;blocks merge on &lt;code&gt;airflow.operators.python_operator&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;prevents regression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.cursorrules&lt;/code&gt; do-not clauses&lt;/td&gt;
&lt;td&gt;7 clauses covering the top failure modes&lt;/td&gt;
&lt;td&gt;LLM self-checks against them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the 30-day standardisation, every new DAG ships from the template, TaskFlow API is universal, sensor polling in PythonOperators falls to zero, and pytest coverage rises from ~30% to ~95%. The two engineers who were skeptical of pytest for DAGs become the loudest advocates once the first regression is caught pre-merge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline (week 0)&lt;/th&gt;
&lt;th&gt;After 30 days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DAGs following template&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAGs using TaskFlow API&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensors in &lt;code&gt;mode='reschedule'&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAGs with pytest coverage&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deprecated-API grep hits&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-merge DAG defects / month&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Docstring-first template&lt;/strong&gt;&lt;/strong&gt; — the module docstring is the LLM's prompt. Standardising the docstring shape via a template forces the LLM to produce compliant DAGs. Skipping the template forces every author to reinvent the docstring shape, and the LLM's DAGs drift accordingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;TaskFlow-only &lt;code&gt;.cursorrules&lt;/code&gt;&lt;/strong&gt;&lt;/strong&gt; — pinning &lt;code&gt;@dag&lt;/code&gt;/&lt;code&gt;@task&lt;/code&gt; decorators and prohibiting &lt;code&gt;PythonOperator&lt;/code&gt; / &lt;code&gt;airflow.operators.python_operator&lt;/code&gt; in the rules file (plus enforcing via CI grep) eliminates the deprecated-API regression entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Mandatory pytest fixtures&lt;/strong&gt;&lt;/strong&gt; — asserting import, task list, dependency graph, SLA, retries in a per-DAG pytest file surfaces regressions before merge. The tests are cheap to write (LLM scaffolds them) and cheap to run (~10 s per DAG).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Sensor mode = reschedule + explicit timeout&lt;/strong&gt;&lt;/strong&gt; — the two config kwargs that turn sensors from "worker-slot-hogging polling loops" into "polite scheduler-managed waits." &lt;code&gt;.cursorrules&lt;/code&gt; clause + CI grep enforce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one DAG template file, ~30 lines of &lt;code&gt;.cursorrules&lt;/code&gt; additions, one CI workflow, one pytest fixture per DAG. The payback is 100% TaskFlow adoption, 100% correct sensor mode, and a ~6× drop in post-merge DAG defects. Compared to letting LLM-authored DAGs land ad-hoc, this is O(1) config for O(N × DAG) engineer-hours saved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;ETL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;ETL problems for Airflow DAG design&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;API&lt;/span&gt;
&lt;span&gt;Topic — api-integration&lt;/span&gt;
&lt;strong&gt;API-integration problems for sensor-driven pipelines&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/api-integration" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Guardrails, review discipline &amp;amp; the SQL-engineer's Copilot workflow
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The three-lane guardrail — compile, lint, human review — is what makes LLM-authored SQL safe to ship
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;sql pair programming&lt;/code&gt; with an LLM is only safe when every completion passes through three independent lanes before merge — the compile lane (&lt;code&gt;dbt parse&lt;/code&gt; + &lt;code&gt;dbt compile&lt;/code&gt;) catches unresolved refs and invalid Jinja, the lint lane (&lt;code&gt;sqlfluff&lt;/code&gt; + &lt;code&gt;sqlmesh diff&lt;/code&gt;) catches style and semantic drift, and the human review lane catches the intent mismatches that neither compile nor lint can see — and skipping any one of the three lanes is what turns a productive Copilot workflow into a source of subtle production defects that only surface days later in dashboards or reconciliation reports&lt;/strong&gt;. Every senior engineer who has been burned by an LLM's hallucination has responded by hardening one of these three lanes; the ones who haven't been burned yet are lucky, not skilled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fam5wp4m2a5yd46w6ur6q.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fam5wp4m2a5yd46w6ur6q.jpeg" alt="Iconographic guardrail diagram — an LLM-sparkle glyph moving through three parallel lanes labelled compile, lint, review; each lane has a check or reject gate, and a green PR badge exits the third lane." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The three lanes of the guardrail — what each catches and what it misses.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compile lane.&lt;/strong&gt; &lt;code&gt;dbt parse&lt;/code&gt; + &lt;code&gt;dbt compile --select state:modified&lt;/code&gt;. Catches: unresolved &lt;code&gt;ref()&lt;/code&gt;, invalid Jinja, missing macros, invalid &lt;code&gt;source()&lt;/code&gt;, syntax errors. Misses: semantically-wrong SQL that happens to be syntactically valid (invented column names that exist elsewhere in the schema, wrong join keys, off-by-one filters).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lint lane.&lt;/strong&gt; &lt;code&gt;sqlfluff lint --dialect snowflake&lt;/code&gt; + &lt;code&gt;sqlmesh diff&lt;/code&gt; (or &lt;code&gt;dbt-checkpoint&lt;/code&gt;). Catches: style drift (SELECT * in marts, missing schema.yml entries, inconsistent CTE naming), semantic drift (a model's row count moved by &amp;gt;5%), deprecated API references. Misses: semantically-wrong SQL that leaves row count unchanged (silently wrong aggregation, subtle join-cardinality bugs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human review lane.&lt;/strong&gt; A person reads the diff, verifies against the docstring / comment contract, runs at least one EXPLAIN, and considers "does this SQL do what the intent says?" Catches: intent-code mismatches, semantic bugs that don't move row counts, novel bugs that no lint rule anticipated. Misses: nothing systematically, but is expensive and inconsistent across reviewers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The four review disciplines every senior SQL engineer signs up for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read every line.&lt;/strong&gt; LLM-authored SQL that "looks right" is exactly the SQL that ships hallucinations. Read every line as if it were written by an unfamiliar junior — because it effectively was.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run every query at least once.&lt;/strong&gt; In a dev / staging environment, against a representative dataset. The compile lane says "the SQL is valid"; running says "the SQL returns what the comment claims."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify the EXPLAIN plan.&lt;/strong&gt; For any query on a mart or dimension table, look at the plan. Hallucinated columns don't show up as missing joins in the plan; they show up as unexpected Cartesian products or full scans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify the row count.&lt;/strong&gt; If the LLM edited a model, compare the row count before and after in a &lt;code&gt;--defer&lt;/code&gt; compile. Row-count drift is the single most sensitive signal for semantic regression.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The daily workflow — the SQL engineer's Copilot day.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Morning.&lt;/strong&gt; Pull main, run &lt;code&gt;dbt parse&lt;/code&gt; locally to seed the state manifest for &lt;code&gt;--defer&lt;/code&gt;. Reset Cursor tabs; reindex codebase if any schema change was merged overnight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task loop.&lt;/strong&gt; For each task: write comment / docstring, wait for ghost text, verify, iterate, commit locally (precommit runs compile + lint + hallucination scan), push, open PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review.&lt;/strong&gt; For every PR (yours or teammates'), read every line, verify against the docstring, run one query, note the EXPLAIN plan, approve or request changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End of day.&lt;/strong&gt; Close all tabs; sync &lt;code&gt;.cursorrules&lt;/code&gt; if any convention changed today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly.&lt;/strong&gt; Reindex codebase; grep for stale names in &lt;code&gt;.cursorrules&lt;/code&gt;; review the hallucination-rate metric; add new failure modes to the do-not clauses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on guardrails.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What's your review checklist for LLM-authored SQL?" — read every line, run every query, verify EXPLAIN, check row count.&lt;/li&gt;
&lt;li&gt;"How do you catch semantically-wrong-but-syntactically-valid SQL?" — sqlmesh diff (row-count drift) + human review against the docstring.&lt;/li&gt;
&lt;li&gt;"How do you handle LLM-authored code in incident review?" — treat it identically to human-authored code; root cause the failure mode (usually stale context or missing do-not clause); patch &lt;code&gt;.cursorrules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"How do you know your guardrails work?" — the hallucination-rate metric: do-not-pattern violations per merged PR, tracked weekly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the daily SQL engineer's Copilot workflow
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Codify the day-to-day workflow of a senior analytics engineer who ships ~2 dbt PRs and ~1 Airflow DAG PR per day, all LLM-assisted. Every step has a discipline; walk through them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Morning setup.&lt;/strong&gt; ~5 minutes to seed the local dbt state manifest, reset Cursor context, review overnight PRs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task loop.&lt;/strong&gt; ~30–60 minutes per task, four-phase: prompt, iterate, precommit, PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review loop.&lt;/strong&gt; ~10–15 minutes per teammate PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End of day.&lt;/strong&gt; ~5 minutes to sync &lt;code&gt;.cursorrules&lt;/code&gt; and close tabs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the daily workflow as a checklist, with time budgets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Morning setup&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;fresh state manifest + fresh Cursor context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task loop&lt;/td&gt;
&lt;td&gt;30–60 min × N tasks&lt;/td&gt;
&lt;td&gt;authoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review loop&lt;/td&gt;
&lt;td&gt;10–15 min × M PRs&lt;/td&gt;
&lt;td&gt;verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End of day&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;hygiene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;td&gt;metric review + rules-file update&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# copilot_daily.sh — the SQL engineer's daily rhythm&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;help&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in

  &lt;/span&gt;morning&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;# 5-minute morning setup&lt;/span&gt;
    git checkout main &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git pull
    dbt deps
    dbt parse &lt;span class="nt"&gt;--profiles-dir&lt;/span&gt; profiles
    &lt;span class="c"&gt;# snapshot the state manifest for --defer during the day&lt;/span&gt;
    &lt;span class="nb"&gt;cp &lt;/span&gt;target/manifest.json ./state/manifest.json
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"state manifest snapshotted at &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="c"&gt;# remind me to reset Cursor&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"→ In Cursor: Cmd-K Cmd-W (close all), Cmd-Shift-P → 'Cursor: Reindex Codebase'"&lt;/span&gt;
    &lt;span class="p"&gt;;;&lt;/span&gt;

  task&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;# start a new task: create a branch, remind me of the discipline&lt;/span&gt;
    &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"task name: "&lt;/span&gt; task
    git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;task&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
Discipline reminder for this task:
  1. Comment/docstring FIRST (5-clause: dialect, source, grain, filters, output)
  2. Wait for ghost text; verify against comment; iterate on comment not on SQL
  3. Before commit: precommit runs dbt parse + sqlfluff + hallucination scan
  4. PR body: paste the comment header + link to the intent doc
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;    &lt;span class="p"&gt;;;&lt;/span&gt;

  precommit&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;# explicit precommit run (git will also invoke this automatically)&lt;/span&gt;
    pre-commit run &lt;span class="nt"&gt;--all-files&lt;/span&gt;
    &lt;span class="p"&gt;;;&lt;/span&gt;

  review&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;# helper for reviewing a teammate PR&lt;/span&gt;
    &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"PR number: "&lt;/span&gt; &lt;span class="nb"&gt;pr
    &lt;/span&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;checkout &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pr&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    gh &lt;span class="nb"&gt;pr &lt;/span&gt;diff &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pr&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | less
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Review checklist:"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [ ] Read every line"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [ ] Comment contract matches the SQL"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [ ] Ran query in dev at least once"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [ ] Checked EXPLAIN plan for any new join"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [ ] Row count drift acceptable (&amp;lt; 5%)"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  [ ] .cursorrules do-nots not violated"&lt;/span&gt;
    &lt;span class="p"&gt;;;&lt;/span&gt;

  evening&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;# 5-minute end of day&lt;/span&gt;
    git status
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git status &lt;span class="nt"&gt;--porcelain&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WARN: uncommitted changes"&lt;/span&gt;
    &lt;span class="k"&gt;fi&lt;/span&gt;
    &lt;span class="c"&gt;# sync .cursorrules if any convention changed&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;git log &lt;span class="nt"&gt;--since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;today &lt;span class="nt"&gt;--oneline&lt;/span&gt; dbt_project.yml profiles.yml | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; .&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"→ dbt config changed today — review .cursorrules"&lt;/span&gt;
    &lt;span class="k"&gt;fi&lt;/span&gt;
    &lt;span class="p"&gt;;;&lt;/span&gt;

  weekly&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;# 30-minute weekly hygiene&lt;/span&gt;
    python scripts/copilot_hallucination_scan.py &lt;span class="nt"&gt;--window&lt;/span&gt; 7d
    python scripts/hallucination_rate.py &lt;span class="nt"&gt;--pr-window&lt;/span&gt; 7d
    &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"TODO(rules)"&lt;/span&gt; .cursorrules &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no pending rules updates"&lt;/span&gt;
    &lt;span class="p"&gt;;;&lt;/span&gt;

  &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"usage: &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt; {morning|task|precommit|review|evening|weekly}"&lt;/span&gt;
    &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;morning&lt;/code&gt; seeds the local &lt;code&gt;state/manifest.json&lt;/code&gt; for &lt;code&gt;--defer&lt;/code&gt; compile runs later in the day. This is what makes &lt;code&gt;dbt compile --select state:modified --defer&lt;/code&gt; fast — it compares the current state to the morning snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;task&lt;/code&gt; creates a branch and prints the four-step discipline reminder. The comment-first / iterate-on-comment mantra is the highest-leverage habit; printing it at task-start makes it hard to skip.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;precommit&lt;/code&gt; explicitly runs the pre-commit hooks. In practice, &lt;code&gt;git commit&lt;/code&gt; runs them automatically, but running them manually before commit lets you fix issues without a rejected commit.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;review&lt;/code&gt; checks out a teammate PR, shows the diff, and prints the six-item review checklist. Every checklist item corresponds to a failure mode that has shipped in production somewhere; the checklist is a memento mori of past incidents.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;evening&lt;/code&gt; catches uncommitted work and prompts a &lt;code&gt;.cursorrules&lt;/code&gt; sync if dbt config changed today. This is what keeps the rules file synchronised with the actual repo state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;weekly&lt;/code&gt; runs the hallucination-rate metric. Trending do-not violations upward is the earliest signal that the rules file needs an update or that a new failure mode has appeared.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;morning&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task&lt;/td&gt;
&lt;td&gt;30–60 min × 2–3&lt;/td&gt;
&lt;td&gt;daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;precommit&lt;/td&gt;
&lt;td&gt;30 s (auto)&lt;/td&gt;
&lt;td&gt;per commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;review&lt;/td&gt;
&lt;td&gt;10–15 min × 3–5&lt;/td&gt;
&lt;td&gt;daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;evening&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;weekly&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;td&gt;Mondays&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Codify the daily rhythm as a shell script. Print the discipline reminders at task-start; print the review checklist at PR-checkout time. The disciplines that live only in your head fade; the disciplines that print themselves on your terminal every morning stick.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — precommit hooks that catch what CI can't
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The precommit stage is where the fastest, cheapest guardrails run — before the code leaves the engineer's machine. Failed precommits produce fast feedback; failed CI produces slow feedback and a rejected PR. The senior discipline is to move as many checks as possible from CI to precommit. Walk through a production precommit config.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compile lane.&lt;/strong&gt; &lt;code&gt;dbt parse&lt;/code&gt; (~5 s) + &lt;code&gt;dbt compile --select state:modified --defer&lt;/code&gt; (~10 s per changed model).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lint lane.&lt;/strong&gt; &lt;code&gt;sqlfluff&lt;/code&gt; (~2 s per file) + &lt;code&gt;sqlmesh diff&lt;/code&gt; (~5 s if enabled).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom lane.&lt;/strong&gt; Hallucination scan (~1 s) + repo-idiom greps (~1 s).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total.&lt;/strong&gt; ~15–30 s per commit — fast enough to run on every commit; slow enough to catch what CI would otherwise catch 5 minutes later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the production &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; for a dbt + Airflow repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;Lane&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dbt-parse&lt;/td&gt;
&lt;td&gt;compile&lt;/td&gt;
&lt;td&gt;~5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt-compile-changed&lt;/td&gt;
&lt;td&gt;compile&lt;/td&gt;
&lt;td&gt;~10 s / model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqlfluff-lint&lt;/td&gt;
&lt;td&gt;lint&lt;/td&gt;
&lt;td&gt;~2 s / file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqlfluff-fix&lt;/td&gt;
&lt;td&gt;lint&lt;/td&gt;
&lt;td&gt;~2 s / file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;copilot-hallucination-scan&lt;/td&gt;
&lt;td&gt;custom&lt;/td&gt;
&lt;td&gt;~1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dag-lint-deprecated&lt;/td&gt;
&lt;td&gt;custom&lt;/td&gt;
&lt;td&gt;~1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;python black + isort&lt;/td&gt;
&lt;td&gt;style&lt;/td&gt;
&lt;td&gt;~2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .pre-commit-config.yaml — production&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# --- Lane 1: COMPILE ---&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-parse&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt parse (unresolved refs / bad Jinja)&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt parse --profiles-dir profiles --project-dir dbt_project&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\.(sql|yml|yaml)$'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-compile-changed&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt compile (changed models only, --defer)&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;bash -c '&lt;/span&gt;
          &lt;span class="s"&gt;cd dbt_project&lt;/span&gt;
          &lt;span class="s"&gt;dbt compile --profiles-dir ../profiles \&lt;/span&gt;
                      &lt;span class="s"&gt;--select state:modified \&lt;/span&gt;
                      &lt;span class="s"&gt;--defer --state ../state&lt;/span&gt;
          &lt;span class="s"&gt;'&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dbt_project/models/.*\.sql$'&lt;/span&gt;

  &lt;span class="c1"&gt;# --- Lane 2: LINT ---&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/sqlfluff/sqlfluff&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.2.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sqlfluff-lint&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;--dialect&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;snowflake&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;--templater&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;dbt&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;additional_dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dbt-snowflake'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sqlfluff-templater-dbt'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sqlfluff-fix&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;--dialect&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;snowflake&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;--templater&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;dbt&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;--force&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;additional_dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dbt-snowflake'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sqlfluff-templater-dbt'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# --- Lane 3: CUSTOM (repo idiom + hallucination) ---&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;copilot-hallucination-scan&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;copilot hallucination scan&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/copilot_hallucination_scan.py&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dag-lint-deprecated&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dag lint (no deprecated Airflow APIs)&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;bash -c '&lt;/span&gt;
          &lt;span class="s"&gt;if grep -rn "airflow\.operators\.python_operator" dags/; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "deprecated airflow API found"; exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
          &lt;span class="s"&gt;if grep -rn "^from airflow.operators import PythonOperator" dags/; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "PythonOperator import found — use @task"; exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
          &lt;span class="s"&gt;exit 0&lt;/span&gt;
          &lt;span class="s"&gt;'&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="c1"&gt;# --- Style ---&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/psf/black&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24.8.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;black&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;--line-length&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;100'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/PyCQA/isort&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5.13.2&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;isort&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;--profile&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;black&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lane 1 (compile) has two hooks. &lt;code&gt;dbt-parse&lt;/code&gt; is fast (~5 s) and catches unresolved &lt;code&gt;ref()&lt;/code&gt;, invalid Jinja, and missing macros. &lt;code&gt;dbt-compile-changed&lt;/code&gt; uses &lt;code&gt;--select state:modified --defer&lt;/code&gt; to compile only the models whose upstream state has changed — this keeps precommit fast even in a 300-model repo.&lt;/li&gt;
&lt;li&gt;Lane 2 (lint) uses &lt;code&gt;sqlfluff&lt;/code&gt; with the dbt templater — this lets sqlfluff understand &lt;code&gt;{{ ref() }}&lt;/code&gt; and &lt;code&gt;{{ source() }}&lt;/code&gt; without treating them as syntax errors. &lt;code&gt;sqlfluff-lint&lt;/code&gt; reports; &lt;code&gt;sqlfluff-fix&lt;/code&gt; auto-corrects.&lt;/li&gt;
&lt;li&gt;Lane 3 (custom) runs the hallucination-flag scan and the DAG deprecated-API grep. Both are cheap (~1 s each) and catch the top-N specific failure modes that the generic lint tools miss.&lt;/li&gt;
&lt;li&gt;The style hooks (&lt;code&gt;black&lt;/code&gt;, &lt;code&gt;isort&lt;/code&gt;) are last so the auto-fixers run after the semantic checks pass. This ordering avoids the "black reformatted the file and now sqlfluff has different line numbers in errors" confusion.&lt;/li&gt;
&lt;li&gt;Total precommit cost is ~15–30 seconds per commit. This is fast enough to run every commit; slow enough that engineers rarely bother to &lt;code&gt;--no-verify&lt;/code&gt;. The right cost curve.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lane&lt;/th&gt;
&lt;th&gt;Hooks&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compile&lt;/td&gt;
&lt;td&gt;dbt-parse, dbt-compile-changed&lt;/td&gt;
&lt;td&gt;~15 s&lt;/td&gt;
&lt;td&gt;unresolved refs, invalid Jinja&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lint&lt;/td&gt;
&lt;td&gt;sqlfluff-lint, sqlfluff-fix&lt;/td&gt;
&lt;td&gt;~5 s&lt;/td&gt;
&lt;td&gt;style + templated-SQL syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;hallucination-scan, dag-lint&lt;/td&gt;
&lt;td&gt;~2 s&lt;/td&gt;
&lt;td&gt;specific failure modes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Style&lt;/td&gt;
&lt;td&gt;black, isort&lt;/td&gt;
&lt;td&gt;~2 s&lt;/td&gt;
&lt;td&gt;code style&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Precommit runs every commit and must stay under ~30 seconds total. Move as many checks as possible from CI to precommit; keep only the expensive checks (full &lt;code&gt;dbt build&lt;/code&gt;, &lt;code&gt;dbt test&lt;/code&gt;) in CI. Fast feedback matters; slow rejection cycles do not.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the LLM-authored SQL incident review pattern
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; When an incident traces to LLM-authored SQL, the review must go beyond "revert the PR." The senior discipline is: (a) identify the exact hallucination or drift, (b) trace it to a missing guardrail, (c) patch the guardrail, (d) grep the repo for the same pattern elsewhere. Walk through a real incident pattern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom.&lt;/strong&gt; A dashboard reports 3% row count drop overnight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause.&lt;/strong&gt; A dbt model merged yesterday joined on &lt;code&gt;customer_key&lt;/code&gt; (invented by Copilot) instead of &lt;code&gt;customer_id&lt;/code&gt;; the join silently returned an empty set for a segment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detection.&lt;/strong&gt; The &lt;code&gt;sqlmesh diff&lt;/code&gt; in CI flagged the model's row count moved by 3%, but the warning was ignored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; Revert; add a &lt;code&gt;.cursorrules&lt;/code&gt; do-not clause "never invent column names — ask for schema.yml"; add a precommit hook that verifies every JOIN's ON clause references columns from the referenced tables; grep the repo for other JOIN-on-invented-columns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Write the incident review + guardrail patch, and demonstrate the repo-wide grep that would catch the same pattern elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Incident&lt;/td&gt;
&lt;td&gt;3% row-count drop on &lt;code&gt;mart_customer_orders&lt;/code&gt; dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root cause&lt;/td&gt;
&lt;td&gt;JOIN on &lt;code&gt;customer_key&lt;/code&gt; (invented); should be &lt;code&gt;customer_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection lane that fired&lt;/td&gt;
&lt;td&gt;sqlmesh diff warning (ignored)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection lane that missed&lt;/td&gt;
&lt;td&gt;sqlfluff, dbt parse, human review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix&lt;/td&gt;
&lt;td&gt;revert; patch &lt;code&gt;.cursorrules&lt;/code&gt;; add JOIN-column-verify hook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# INCIDENT-2026-07-29.md — post-incident review&lt;/span&gt;

&lt;span class="gu"&gt;## Timeline&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 2026-07-28 15:03  PR #4821 merged: &lt;span class="sb"&gt;`int_orders__enriched.sql`&lt;/span&gt; refactor
&lt;span class="p"&gt;-&lt;/span&gt; 2026-07-28 15:15  CI sqlmesh diff warned: row_count -3.1% (ignored)
&lt;span class="p"&gt;-&lt;/span&gt; 2026-07-28 22:00  overnight dbt run applies the model to prod
&lt;span class="p"&gt;-&lt;/span&gt; 2026-07-29 08:12  dashboard "Customer Orders — Daily" flagged 3% drop
&lt;span class="p"&gt;-&lt;/span&gt; 2026-07-29 08:35  root cause: JOIN on &lt;span class="sb"&gt;`customer_key`&lt;/span&gt; — column does not exist
                     on dim_customers; silently returned NULL, filtered out
&lt;span class="p"&gt;-&lt;/span&gt; 2026-07-29 08:50  reverted PR #4821; deployed hotfix

&lt;span class="gu"&gt;## Root cause&lt;/span&gt;
Copilot generated a JOIN clause &lt;span class="sb"&gt;`ON o.customer_id = c.customer_key`&lt;/span&gt;.
&lt;span class="sb"&gt;`c.customer_key`&lt;/span&gt; does not exist on dim_customers (correct column is
&lt;span class="sb"&gt;`customer_id`&lt;/span&gt;). Snowflake accepted the SQL because &lt;span class="sb"&gt;`customer_key`&lt;/span&gt; looked
like it could be a valid column name; the join silently returned NULL
for that side; the WHERE filter dropped the NULLs; row count fell 3%.

The Copilot completion was made in a session where the engineer had not
opened dim_customers's schema.yml in the context window. Copilot guessed
the column name from stem-name heuristics.

&lt;span class="gu"&gt;## Guardrail gaps&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="sb"&gt;`.cursorrules`&lt;/span&gt; did not have "never invent column names" as a do-not.
&lt;span class="p"&gt;2.&lt;/span&gt; precommit did not verify JOIN ON columns exist in referenced tables.
&lt;span class="p"&gt;3.&lt;/span&gt; sqlmesh diff warning was fired but not blocking.
&lt;span class="p"&gt;4.&lt;/span&gt; human review approved without opening the schema.yml.

&lt;span class="gu"&gt;## Fix&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Added to &lt;span class="sb"&gt;`.cursorrules`&lt;/span&gt;:
   "Never invent a column name. Before proposing a JOIN, verify both
    columns exist by opening schema.yml or asking the user."
&lt;span class="p"&gt;2.&lt;/span&gt; Added precommit hook &lt;span class="sb"&gt;`verify_join_columns.py`&lt;/span&gt; that parses new/changed
   SQL, extracts JOIN ON clauses, and cross-references the columns
   against the referenced models' schema.yml.
&lt;span class="p"&gt;3.&lt;/span&gt; Made sqlmesh diff warnings blocking on row count movements &amp;gt; 2%.
&lt;span class="p"&gt;4.&lt;/span&gt; Updated PR review template: "opened schema.yml for every JOIN? [ ]"

&lt;span class="gu"&gt;## Repo-wide sweep&lt;/span&gt;
Grep run to catch the same pattern in other models:
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;python scripts/verify_join_columns.py dbt_project/models/*&lt;em&gt;/&lt;/em&gt;.sql&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Result: 2 additional models flagged with likely-invented column names
in JOINs; both fixed in follow-up PR #4835.
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/verify_join_columns.py — the new precommit hook
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Parse every SQL file and verify JOIN ON columns exist in the referenced
models&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; schema.yml entries. Fails loudly on any suspicious join.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Load schema.yml -&amp;gt; {model_name: [column_names]}
&lt;/span&gt;&lt;span class="n"&gt;schema_columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;yml_path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_project/models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yml_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
            &lt;span class="n"&gt;schema_columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Simple regex — {{ ref('model_name') }} &amp;lt;alias&amp;gt;
&lt;/span&gt;&lt;span class="n"&gt;REF&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\{\{\s*ref\(\s*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;([^&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]+)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\s*\)\s*\}\}\s+(\w+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;JOIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JOIN\s+\{\{\s*ref\(\s*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;([^&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]+)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\s*\)\s*\}\}\s+(\w+)\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:USING\s*\((\w+)\)|ON\s+([\w\.]+)\s*=\s*([\w\.]+))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Walk staged files
&lt;/span&gt;&lt;span class="n"&gt;violations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# Build alias -&amp;gt; model map from the FROM / JOIN clauses
&lt;/span&gt;    &lt;span class="n"&gt;alias_to_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REF&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;JOIN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;using_col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;left_expr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right_expr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# For each column in the ON clause, verify it exists in its model
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left_expr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right_expr&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rpartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alias_to_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;schema_columns&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;schema_columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: JOIN references &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; — &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;column not in schema.yml for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;using_col&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;alias_to_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;schema_columns&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;using_col&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;schema_columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                    &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: JOIN USING (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;using_col&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) — &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;column not in schema.yml for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify_join_columns flagged:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The post-incident timeline records the causal chain: PR merged → sqlmesh warning ignored → overnight run → dashboard alarm → root cause identified → revert. Writing this timeline is the first discipline; it forces you to identify every guardrail that could have caught it.&lt;/li&gt;
&lt;li&gt;The root cause identifies the specific failure mode (invented column name) and the human contributor (schema.yml not opened during Copilot session). Naming both — the model failure and the human process failure — is required.&lt;/li&gt;
&lt;li&gt;The guardrail-gap section lists the four lanes that should have caught this: &lt;code&gt;.cursorrules&lt;/code&gt;, precommit, CI, human review. Every incident maps to at least one gap; often multiple.&lt;/li&gt;
&lt;li&gt;The fix section is the patch to each lane: add a do-not clause, add a precommit hook, upgrade a CI warning to blocking, update the PR template. Multi-lane fix for a multi-lane failure.&lt;/li&gt;
&lt;li&gt;The repo-wide sweep uses the new precommit hook to grep for the same pattern elsewhere. This is what turns a one-incident fix into a class-of-incident fix.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lane&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;th&gt;Patch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.cursorrules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no "never invent column names" clause&lt;/td&gt;
&lt;td&gt;added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;precommit&lt;/td&gt;
&lt;td&gt;no JOIN-column verifier&lt;/td&gt;
&lt;td&gt;added &lt;code&gt;verify_join_columns.py&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI&lt;/td&gt;
&lt;td&gt;sqlmesh warning was non-blocking&lt;/td&gt;
&lt;td&gt;upgraded to blocking on &amp;gt; 2% drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;human review&lt;/td&gt;
&lt;td&gt;schema.yml not opened&lt;/td&gt;
&lt;td&gt;updated PR template checkbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;repo sweep&lt;/td&gt;
&lt;td&gt;pattern lurked in 2 other models&lt;/td&gt;
&lt;td&gt;fixed in PR #4835&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every LLM-related incident review ends with (a) named root cause, (b) named guardrail gap per lane, (c) patched lane, (d) repo-wide grep for the same pattern. Skipping the grep is the most common way an incident recurs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on guardrails and review discipline
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You've inherited an analytics-engineering team that adopted Copilot six months ago. Three production incidents have traced to LLM-authored SQL — invented column names, wrong dialect, deprecated Airflow operator. Walk me through the three-lane guardrail you'd standardise, the precommit config, the CI upgrades, the review-discipline PR template, and the metrics you'd track to know it's working."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a three-lane precommit + CI, a review PR template, and a weekly hallucination-rate metric
&lt;/h3&gt;



&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .pre-commit-config.yaml — three lanes&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-parse&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt parse&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt parse --profiles-dir profiles --project-dir dbt_project&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-compile-changed&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt compile (state:modified, --defer)&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash -c 'cd dbt_project &amp;amp;&amp;amp; dbt compile --profiles-dir ../profiles --select state:modified --defer --state ../state'&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;copilot-hallucination-scan&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;copilot hallucination scan&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/copilot_hallucination_scan.py&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;verify-join-columns&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;verify JOIN columns exist in schema.yml&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/verify_join_columns.py&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\.sql$'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dag-lint-deprecated&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dag lint (no deprecated Airflow APIs)&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash -c 'if grep -rn "airflow\.operators\.python_operator" dags/; then echo "deprecated API"; exit 1; fi'&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/sqlfluff/sqlfluff&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.2.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sqlfluff-lint&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;--dialect&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;snowflake&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;--templater&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;dbt&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;additional_dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dbt-snowflake'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sqlfluff-templater-dbt'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/pr-checks.yml — CI (heavier, blocking)&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pr-checks&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dbt-build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;0&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fetch state manifest from main&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git fetch origin main:main&lt;/span&gt;
          &lt;span class="s"&gt;git checkout main -- state/manifest.json || true&lt;/span&gt;
          &lt;span class="s"&gt;git checkout -&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt build (modified + downstream)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cd dbt_project&lt;/span&gt;
          &lt;span class="s"&gt;dbt build --profiles-dir ../profiles \&lt;/span&gt;
                    &lt;span class="s"&gt;--select state:modified+ \&lt;/span&gt;
                    &lt;span class="s"&gt;--defer --state ../state \&lt;/span&gt;
                    &lt;span class="s"&gt;--fail-fast&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sqlmesh diff (row-count drift, BLOCKING &amp;gt; 2%)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;python scripts/sqlmesh_diff.py --threshold 0.02&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;airflow dagbag import&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;python -c "&lt;/span&gt;
          &lt;span class="s"&gt;from airflow.models import DagBag&lt;/span&gt;
          &lt;span class="s"&gt;db = DagBag('dags/', include_examples=False)&lt;/span&gt;
          &lt;span class="s"&gt;assert not db.import_errors, db.import_errors&lt;/span&gt;
          &lt;span class="s"&gt;"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest for dags&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest tests/dags/ -q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- .github/pull_request_template.md --&amp;gt;&lt;/span&gt;
&lt;span class="gu"&gt;## Description&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- what does this PR do and why --&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## LLM co-authorship&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] This PR includes Copilot / Cursor-generated code
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Comment-first prompts were used
&lt;span class="p"&gt;-&lt;/span&gt; [ ] &lt;span class="sb"&gt;`.cursorrules`&lt;/span&gt; do-nots were respected

&lt;span class="gu"&gt;## Guardrail lanes&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Precommit passed (dbt parse, compile, sqlfluff, hallucination scan)
&lt;span class="p"&gt;-&lt;/span&gt; [ ] CI passed (dbt build --select state:modified+, sqlmesh diff, DAG imports, pytest)
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Ran at least one query in dev against representative data
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Checked EXPLAIN plan for any new JOIN
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Opened schema.yml for every JOINed model
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Row-count drift (from sqlmesh diff) is &amp;lt; 2% or explained in the description

&lt;span class="gu"&gt;## For DAGs&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Module docstring: purpose, schedule, upstream, owner, retries, SLA, tasks
&lt;span class="p"&gt;-&lt;/span&gt; [ ] TaskFlow API (@dag / @task), not PythonOperator
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Every Sensor has mode='reschedule' and explicit timeout
&lt;span class="p"&gt;-&lt;/span&gt; [ ] pytest fixture at tests/dags/test_&lt;span class="nt"&gt;&amp;lt;dag_id&amp;gt;&lt;/span&gt;.py asserts import + task list + deps
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lane&lt;/th&gt;
&lt;th&gt;Where it runs&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compile (precommit)&lt;/td&gt;
&lt;td&gt;dev machine&lt;/td&gt;
&lt;td&gt;unresolved refs, invalid Jinja&lt;/td&gt;
&lt;td&gt;~15 s / commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lint (precommit)&lt;/td&gt;
&lt;td&gt;dev machine&lt;/td&gt;
&lt;td&gt;style, templated-SQL syntax&lt;/td&gt;
&lt;td&gt;~5 s / commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom (precommit)&lt;/td&gt;
&lt;td&gt;dev machine&lt;/td&gt;
&lt;td&gt;wrong dialect, deprecated API, invented JOIN columns&lt;/td&gt;
&lt;td&gt;~2 s / commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compile (CI)&lt;/td&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;full modified-subgraph compile&lt;/td&gt;
&lt;td&gt;~1–3 min / PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqlmesh diff (CI)&lt;/td&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;row-count drift &amp;gt; 2%&lt;/td&gt;
&lt;td&gt;~30 s / PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAG import (CI)&lt;/td&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;DAG parse errors&lt;/td&gt;
&lt;td&gt;~10 s / PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pytest (CI)&lt;/td&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;task list, deps, SLA, retries&lt;/td&gt;
&lt;td&gt;~10 s / DAG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human review&lt;/td&gt;
&lt;td&gt;PR&lt;/td&gt;
&lt;td&gt;intent-code mismatch, EXPLAIN sanity&lt;/td&gt;
&lt;td&gt;~15 min / PR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the 30-day standardisation, the three prior incidents' failure modes are caught at precommit (wrong dialect, deprecated API) or precommit + CI (invented JOIN columns), and the weekly hallucination-rate metric drops from ~15% to ~2%. The team ships more PRs faster with fewer post-merge defects — the three-lane guardrail turns the LLM workflow from "risky productivity" into "safe productivity."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After (30 d)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Precommit failure rate&lt;/td&gt;
&lt;td&gt;8% (mostly style)&lt;/td&gt;
&lt;td&gt;4% (mostly semantic, caught early)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI failure rate&lt;/td&gt;
&lt;td&gt;15% (mostly dbt build)&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-merge defects / 100 PRs&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucination-flag rate / merged PR&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRs / engineer / week&lt;/td&gt;
&lt;td&gt;3.5&lt;/td&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean PR lifetime&lt;/td&gt;
&lt;td&gt;2.1 d&lt;/td&gt;
&lt;td&gt;0.9 d&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Three-lane guardrail&lt;/strong&gt;&lt;/strong&gt; — compile, lint, human review each catch a different class of failure. Compile catches syntax; lint catches style + row-count drift; review catches intent mismatch. Missing any one lane leaves a class of defect uncovered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Move as many checks as possible to precommit&lt;/strong&gt;&lt;/strong&gt; — precommit gives 15-second feedback; CI gives 3-minute feedback. Fast feedback is the difference between "engineer fixes it while still in flow" and "engineer forgets what they were doing and comes back tomorrow."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Blocking sqlmesh diff on row-count drift&lt;/strong&gt;&lt;/strong&gt; — turning a warning into a blocker is the specific fix for the "silently wrong SQL that compiles fine" class of incident. 2% threshold catches the meaningful drifts without over-firing on legitimate refactors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;PR template checkboxes&lt;/strong&gt;&lt;/strong&gt; — the template turns implicit disciplines (open schema.yml, verify EXPLAIN, check row count) into explicit checkboxes reviewers can hold each other accountable to. The template is the code review's &lt;code&gt;.cursorrules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one precommit config (~50 lines), one CI workflow (~30 lines), one PR template (~20 lines), one weekly metric script. The payback is a ~7× drop in hallucination-flag rate and 2× PR throughput. Compared to letting each engineer figure out their own review discipline, this is O(1) config for O(N × week) engineer-hours saved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — cte/sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;CTE and refactor problems for review discipline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/cte/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — aggregation/sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Aggregation problems for LLM-assisted authoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/aggregation/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — LLM-native SQL authoring recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Editor selection by task shape.&lt;/strong&gt; For single-file mechanical edits, Cursor inline or Copilot inline; sub-300-ms ghost text keeps flow. For multi-file reasoning tasks, Cursor Composer or Copilot Chat with &lt;code&gt;@Codebase&lt;/code&gt;/&lt;code&gt;@workspace&lt;/code&gt; context injection. For overnight or repo-wide refactors, Codex CLI, Claude Code, or Aider with the strictest 3-lane guardrail. Never let agentic mode land PRs without human review; never spend Composer latency on tasks that fit in inline ghost text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;.cursorrules&lt;/code&gt; template that ships hallucination rate ~4× lower.&lt;/strong&gt; Sections in order: role + voice, repo shape, naming conventions, SQL style + dialect, dbt idioms, Airflow idioms, do-nots, guardrail expectations. Under ~1500 tokens. Versioned in the repo root; reviewed in PRs like any config. Do-nots are more effective than do's — "never invent a column name" beats "prefer real column names." Include "when you don't know, say so and ask" as the final clause to convert silent hallucination into loud "please paste the schema."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The comment-first Copilot prompt.&lt;/strong&gt; Five-clause header comment: dialect, source table + upstream refs, grain (one row per what), filters (WHERE conditions), output columns. Write the comment first; wait for ghost text; verify against the comment; iterate on the comment (not on the SQL) if the completion drifts. This one discipline is the single most transferable Copilot habit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dbt model docstring pattern.&lt;/strong&gt; Every model has a &lt;code&gt;schema.yml&lt;/code&gt; entry with description + columns + tests. Every macro has a &lt;code&gt;{% docs %}&lt;/code&gt; block. Every dbt-file top-of-file comment names dialect + source refs + grain — this is the Copilot inline anchor, redundant with &lt;code&gt;.cursorrules&lt;/code&gt; but load-bearing when tabs are stale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Airflow DAG scaffold prompt.&lt;/strong&gt; Module docstring with seven fields: DAG id, purpose, schedule, upstream (sensor keys or trigger DAG), owner + on-call rotation, retry policy (retries + delay + exponential backoff), SLA, task list. Then &lt;code&gt;@dag&lt;/code&gt; decorator; then &lt;code&gt;@task&lt;/code&gt; / &lt;code&gt;@task_group&lt;/code&gt; decorators. Then dependency graph with explicit &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;. Then a matching pytest fixture at &lt;code&gt;tests/dags/test_&amp;lt;dag_id&amp;gt;.py&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The precommit three-lane block.&lt;/strong&gt; Lane 1 (compile): &lt;code&gt;dbt parse&lt;/code&gt; + &lt;code&gt;dbt compile --select state:modified --defer&lt;/code&gt;. Lane 2 (lint): &lt;code&gt;sqlfluff lint --dialect &amp;lt;dialect&amp;gt; --templater dbt&lt;/code&gt;. Lane 3 (custom): hallucination-scan for wrong-dialect + deprecated-API patterns, plus &lt;code&gt;verify_join_columns.py&lt;/code&gt; for JOIN-column existence. Total ~15–30 s per commit; catches ~90% of what CI would catch 3 minutes later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CI heavier lane.&lt;/strong&gt; &lt;code&gt;dbt build --select state:modified+&lt;/code&gt; (compile + tests on modified subgraph), sqlmesh diff with 2% row-count-drift threshold as blocking, DAG import test, pytest for DAGs. Runs on every PR; blocks merge on any failure. Fetch the main-branch state manifest before running so &lt;code&gt;--defer&lt;/code&gt; works against fresh state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The PR review template.&lt;/strong&gt; Checkboxes: LLM co-authorship, comment-first prompt used, &lt;code&gt;.cursorrules&lt;/code&gt; respected, precommit passed, CI passed, ran query in dev, checked EXPLAIN, opened schema.yml for every JOIN, row-count drift &amp;lt; 2% or explained. For DAGs: TaskFlow API, sensor &lt;code&gt;mode='reschedule'&lt;/code&gt; + timeout, pytest fixture asserts task list + deps + SLA + retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The daily rhythm.&lt;/strong&gt; Morning: pull main, snapshot &lt;code&gt;state/manifest.json&lt;/code&gt;, reset Cursor tabs, reindex codebase. Task loop: comment/docstring first, ghost text, verify, precommit, PR. Review loop: read every line, verify comment contract, run one query, check EXPLAIN. Evening: close tabs, sync &lt;code&gt;.cursorrules&lt;/code&gt; if any convention changed. Weekly: hallucination-rate metric, &lt;code&gt;.cursorrules&lt;/code&gt; update if new failure modes appeared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hallucination-rate metric.&lt;/strong&gt; Weekly script that greps merged-PR diffs for known do-not patterns (wrong dialect, deprecated Airflow API, hardcoded schema, SELECT * in mart, JOIN on unknown column). Report absolute count + rate. Trending upward = missing rules clause or missing precommit hook. Target: &amp;lt; 3% flag rate on merged PRs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The incident-review-to-guardrail-patch loop.&lt;/strong&gt; Every LLM-related incident ends with: (a) named root cause, (b) named guardrail gap per lane, (c) patched lane (new &lt;code&gt;.cursorrules&lt;/code&gt; clause, new precommit hook, new CI check, or updated PR template), (d) repo-wide grep for the same pattern in other models / DAGs. Skipping the grep is the most common way an incident recurs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context-window hygiene.&lt;/strong&gt; Close unused Cursor tabs at end of session. Reindex codebase after schema changes. Pin &lt;code&gt;.cursorrules&lt;/code&gt; + &lt;code&gt;dbt_project.yml&lt;/code&gt; + &lt;code&gt;profiles.yml&lt;/code&gt; via &lt;code&gt;.cursor/settings.json&lt;/code&gt; &lt;code&gt;autoInclude&lt;/code&gt;. Grep for stale model names in &lt;code&gt;.cursorrules&lt;/code&gt; weekly. Keep the rules file under ~1500 tokens; verbose examples go in &lt;code&gt;docs/&lt;/code&gt; and get pulled via &lt;code&gt;@Docs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to escalate from inline to Composer to CLI.&lt;/strong&gt; Inline for single-file mechanical (add a column, refactor a filter). Composer / Chat for multi-file with reasoning (rename model across 5 references, generate schema.yml from a new model). CLI / agentic for repo-wide (deprecate a macro across 40 models, generate exposures for every dashboard). Each escalation multiplies both leverage and required review depth; never escalate without proportional guardrail depth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "when you don't know, say so" clause.&lt;/strong&gt; Both &lt;code&gt;.cursorrules&lt;/code&gt; and the comment-first pattern should invite the LLM to ask for clarification instead of guessing. Silent hallucination is expensive; loud "please paste the schema for dim_customers" is cheap. Rewarding the ask, not the guess, is the highest-leverage prompt-engineering move.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Cursor and why does it matter for SQL engineers in 2026?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; is a VS Code fork with LLM-native features built in from the ground up: &lt;code&gt;.cursorrules&lt;/code&gt; for repo-scoped prompt injection, &lt;code&gt;@Codebase&lt;/code&gt; / &lt;code&gt;@Docs&lt;/code&gt; / &lt;code&gt;@Files&lt;/code&gt; symbol injection for context-window control, inline ghost-text completion tuned for sub-300-ms latency, and Composer for multi-file agentic edits. It matters for &lt;code&gt;cursor for sql&lt;/code&gt; engineers because SQL authoring in a dbt monorepo is a context-heavy task — the LLM must know the model layer hierarchy, the naming conventions, the dbt macros installed, the SQL dialect (Snowflake vs BigQuery vs Postgres), the schema.yml contracts — and Cursor's context-injection model is uniquely well-suited to that shape. Every senior data engineer in 2026 who has adopted &lt;code&gt;.cursorrules&lt;/code&gt; + comment-first prompting reports 2–3× PR throughput on analytics-engineering work with stable or improving defect rates. Cursor doesn't replace SQL literacy; it amplifies it, and the engineers who configure it well outproduce the ones who don't by a wide margin.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Copilot vs Cursor for dbt authoring — which should I pick?
&lt;/h3&gt;

&lt;p&gt;Both, actually — they solve different sub-problems. &lt;strong&gt;GitHub Copilot inline&lt;/strong&gt; is unmatched for sub-300-ms ghost-text completions at the caret: line-by-line SQL, Jinja fills, dbt macro completions, small edits. Its &lt;code&gt;github copilot sql&lt;/code&gt; completions are fastest and most predictable when you use the comment-first pattern. &lt;strong&gt;Cursor&lt;/strong&gt; is unmatched for repo-scoped context (&lt;code&gt;.cursorrules&lt;/code&gt;, &lt;code&gt;@Codebase&lt;/code&gt;), multi-file edits (Composer), and integrated Chat with &lt;code&gt;@Docs&lt;/code&gt; injection. The senior workflow uses both: Cursor as the editor with &lt;code&gt;.cursorrules&lt;/code&gt; and Composer for multi-file work, plus GitHub Copilot enabled inside Cursor for the fastest ghost-text completions. &lt;code&gt;cursor rules for sql&lt;/code&gt; are what pin dialect + naming; Copilot's inline model is what makes each line arrive quickly. Never pick one and disable the other — the failure modes are complementary and the productivity multiplier is compounded.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I keep an LLM from hallucinating column names in my SQL?
&lt;/h3&gt;

&lt;p&gt;Four defenses, in order of leverage. &lt;strong&gt;First&lt;/strong&gt;, put the SQL dialect and the source tables' expected columns in a file-header comment or in &lt;code&gt;.cursorrules&lt;/code&gt;; this is the cheapest fix. &lt;strong&gt;Second&lt;/strong&gt;, use Cursor's &lt;code&gt;@Codebase &amp;lt;model&amp;gt; schema.yml&lt;/code&gt; symbol injection (or open the schema.yml in a sibling tab in Copilot) so the LLM sees the actual column list. &lt;strong&gt;Third&lt;/strong&gt;, add a precommit hook (&lt;code&gt;verify_join_columns.py&lt;/code&gt;) that parses JOIN ON clauses and cross-references the columns against schema.yml — this catches invented column names before commit. &lt;strong&gt;Fourth&lt;/strong&gt;, in &lt;code&gt;.cursorrules&lt;/code&gt;, add the do-not clause "never invent a column name; ask for the schema.yml if unclear" — this converts silent hallucination into loud clarification requests. Combine all four and the invented-column-name failure mode falls from ~1 in 10 completions to ~1 in 100. The single incident every senior data engineer remembers is the one where an invented column name silently shipped and dropped 3% of dashboard rows overnight; the four defenses above are the composite fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I trust Copilot or Cursor to write Airflow DAGs?
&lt;/h3&gt;

&lt;p&gt;Yes, with the docstring-first discipline and a three-lane guardrail. &lt;code&gt;airflow dag copilot&lt;/code&gt; authoring works well when (a) you write a top-of-file module docstring naming purpose, schedule, upstream, owner, retry policy, SLA, and task list — the LLM scaffolds the DAG from that; (b) &lt;code&gt;.cursorrules&lt;/code&gt; do-not clauses prohibit &lt;code&gt;airflow.operators.python_operator&lt;/code&gt; (deprecated), mandate TaskFlow API (&lt;code&gt;@dag&lt;/code&gt;/&lt;code&gt;@task&lt;/code&gt;), and require &lt;code&gt;mode='reschedule'&lt;/code&gt; + explicit &lt;code&gt;timeout&lt;/code&gt; on every Sensor; (c) every DAG ships with a matching &lt;code&gt;tests/dags/test_&amp;lt;dag_id&amp;gt;.py&lt;/code&gt; pytest fixture asserting import, task list, dependency graph, SLA, retries. CI runs a DAGBag import test that catches parse errors, plus pytest, plus a grep for deprecated APIs. With those three lanes in place, LLM-authored DAGs are as safe as hand-authored ones — the LLM just gets you to the reviewed draft ~5× faster. Without them, LLM-authored DAGs ship polling loops inside PythonOperators and deprecated operator imports on a regular basis.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should a &lt;code&gt;.cursorrules&lt;/code&gt; file contain for a dbt + Airflow repo?
&lt;/h3&gt;

&lt;p&gt;Seven sections in this order. &lt;strong&gt;Role + voice&lt;/strong&gt; — "you are pair-programming with a senior analytics engineer on the &lt;code&gt;&amp;lt;repo&amp;gt;&lt;/code&gt; monorepo." &lt;strong&gt;Repo shape&lt;/strong&gt; — the folder layout, dbt project structure, DAG folder. &lt;strong&gt;Naming conventions&lt;/strong&gt; — model prefixes (&lt;code&gt;stg_&lt;/code&gt;, &lt;code&gt;int_&lt;/code&gt;, &lt;code&gt;dim_&lt;/code&gt;, &lt;code&gt;fct_&lt;/code&gt;), macro naming, DAG id conventions. &lt;strong&gt;SQL style + dialect&lt;/strong&gt; — Snowflake / BigQuery / Postgres, CTE-first, ORDER BY at the end, QUALIFY over subqueries. &lt;strong&gt;dbt idioms&lt;/strong&gt; — schema.yml required per model, incremental config template, macro documentation via &lt;code&gt;{% docs %}&lt;/code&gt;. &lt;strong&gt;Airflow idioms&lt;/strong&gt; — TaskFlow API only, sensor operators over polling loops, retry policy on network operators. &lt;strong&gt;Do-nots&lt;/strong&gt; — the specific anti-patterns to prohibit (invented column names, deprecated APIs, hardcoded schema references). &lt;strong&gt;Guardrail expectations&lt;/strong&gt; — what tools will check the completion (&lt;code&gt;dbt parse&lt;/code&gt;, &lt;code&gt;sqlfluff&lt;/code&gt;, human review) so the LLM self-checks. Keep the whole file under ~1500 tokens; version it in the repo root; review it in PRs whenever a convention changes. This is the highest-leverage config in the whole &lt;code&gt;llm-native sql&lt;/code&gt; stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is LLM-native SQL authoring safe for production data pipelines?
&lt;/h3&gt;

&lt;p&gt;Yes, when the three-lane guardrail is in place; no, when any lane is missing. The compile lane (&lt;code&gt;dbt parse&lt;/code&gt; + &lt;code&gt;dbt compile --select state:modified --defer&lt;/code&gt;) catches unresolved refs and invalid Jinja. The lint lane (&lt;code&gt;sqlfluff&lt;/code&gt; + &lt;code&gt;sqlmesh diff&lt;/code&gt; with 2% row-count-drift threshold) catches style and semantic drift. The human review lane catches intent-code mismatches that neither compile nor lint can see. Every production incident traced to LLM-authored SQL — and every senior data engineer has at least one — maps to one or more missing lanes: the wrong-dialect hallucination that shipped because the precommit didn't check dialect, the invented column name that shipped because sqlmesh diff was non-blocking, the deprecated Airflow operator that shipped because the CI grep didn't exist. The pattern is always the same: identify the missing lane, add the check, grep the repo for the same pattern elsewhere. With three lanes and a weekly hallucination-rate metric, LLM-authored SQL is safer than hand-authored SQL because the lanes catch what humans miss; without three lanes, it's a source of slow-motion production incidents that only surface days later in dashboards or reconciliation reports. Configure the lanes; trust the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; for the CTE, join, window-function, and aggregation problems that map directly onto the comment-first Copilot pattern.&lt;/li&gt;
&lt;li&gt;Rehearse on the &lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;SQL-generation practice library →&lt;/a&gt; for prompt-hygiene, schema-aware generation, and hallucination-resistance drills.&lt;/li&gt;
&lt;li&gt;Sharpen the pipeline axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;ETL practice library →&lt;/a&gt; for the Airflow DAG scaffolding, sensor-driven, and incremental-load patterns senior interviewers love.&lt;/li&gt;
&lt;li&gt;Stack the design axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;design practice library →&lt;/a&gt; for the repo-scoped tooling and guardrail-architecture scenarios that every LLM-workflow interview probes.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the &lt;code&gt;.cursorrules&lt;/code&gt; + three-lane guardrail against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lock in LLM-native SQL authoring muscle memory&lt;/h3&gt;

&lt;p&gt;Docs explain features. PipeCode drills explain the discipline — when a comment-first prompt is worth its 30 seconds, when `.cursorrules` prevents a hallucinated column, when a three-lane guardrail catches what code review misses, when the docstring-first DAG scaffold turns a 60-minute task into a 10-minute review. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — pattern-first practice tuned for the LLM-native SQL, dbt, and Airflow authoring workflows senior data engineers actually ship.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice SQL problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice ETL problems →&lt;/a&gt;&lt;/p&gt;



&lt;/ol&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>dbt Copilot, Snowflake Cortex Analyst &amp; Databricks AI/BI Genie: LLM Assistants for Analytics Teams</title>
      <dc:creator>Gowtham Potureddi</dc:creator>
      <pubDate>Fri, 31 Jul 2026 04:20:16 +0000</pubDate>
      <link>https://dev.to/gowthampotureddi/dbt-copilot-snowflake-cortex-analyst-databricks-aibi-genie-llm-assistants-for-analytics-teams-l6l</link>
      <guid>https://dev.to/gowthampotureddi/dbt-copilot-snowflake-cortex-analyst-databricks-aibi-genie-llm-assistants-for-analytics-teams-l6l</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;code&gt;llm assistants for analytics&lt;/code&gt;&lt;/strong&gt; are the pick-one UX decision that decides whether your analytics engineers spend the next quarter authoring boilerplate models by hand or shipping curated semantic layers that a business user can talk to in English — and it is the single tooling decision senior data engineers get wrong most often because "just add ChatGPT" is not a strategy. Every warehouse vendor now bundles an LLM assistant into the console, every transformation vendor bundles one into the IDE, and every self-serve BI vendor now bundles one into the dashboard editor. The engineering trade-off does not live in "should we have an LLM assistant" — every stack with more than one analyst needs some form of natural-language front door — but in &lt;em&gt;which&lt;/em&gt; assistant you deploy to &lt;em&gt;which&lt;/em&gt; persona and &lt;em&gt;what&lt;/em&gt; grounding source you feed it.&lt;/p&gt;

&lt;p&gt;This guide is the senior-DE walkthrough you wished existed the first time an interviewer asked "walk me through the three vendor LLM assistants and their grounding stories," or "our CFO wants to ask questions in English — where do we put the semantic layer?", or "explain how you'd stop &lt;code&gt;text-to-sql&lt;/code&gt; from hallucinating a join across two tables that share a column name but not a foreign key." It walks through the three canonical vendor assistants — &lt;code&gt;dbt copilot&lt;/code&gt; (project-graph-grounded authoring inside dbt Cloud), &lt;code&gt;snowflake cortex analyst&lt;/code&gt; (YAML-semantic-model-grounded &lt;code&gt;natural language to sql&lt;/code&gt; REST API), and &lt;code&gt;databricks ai/bi genie&lt;/code&gt; (Unity-Catalog-scoped natural-language BI over sample values and column comments) — the "four axes" interviewers actually probe (grounding source, governance boundary, latency budget, human review), the canonical setup for each, and the stacked pattern where &lt;code&gt;dbt cloud ai&lt;/code&gt; authors the &lt;code&gt;semantic layer&lt;/code&gt; upstream and Cortex Analyst / Genie serve it downstream. Each section pairs a teaching block with a Solution-Tail interview answer — code, a step-by-step trace, an output table, then a concept-by-concept breakdown of why it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob15kudmlzti7gx3u975.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob15kudmlzti7gx3u975.jpeg" alt="PipeCode blog header for LLM assistants for analytics — bold white headline 'LLM Assistants for Analytics' over a hero composition of four small glyph medallions (dbt-glyph, snowflake glyph, databricks glyph, LLM sparkle) arranged on a wheel around a central purple 'PICK ONE' seal, on a dark gradient." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you want &lt;strong&gt;hands-on reps&lt;/strong&gt; immediately after reading, drill the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt;, rehearse the &lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;SQL-generation practice library →&lt;/a&gt;, and sharpen the analytics axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/data-analysis" rel="noopener noreferrer"&gt;data-analysis practice library →&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;On this page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why LLM assistants are the new UX layer for analytics teams in 2026&lt;/li&gt;
&lt;li&gt;dbt Copilot — model-authoring, docs, and semantic-layer completions&lt;/li&gt;
&lt;li&gt;Snowflake Cortex Analyst — governed text-to-SQL over your semantic model&lt;/li&gt;
&lt;li&gt;Databricks AI/BI Genie — natural-language BI over Unity Catalog&lt;/li&gt;
&lt;li&gt;Picking / stacking the assistants — decision matrix + interview signals&lt;/li&gt;
&lt;li&gt;Cheat sheet — LLM assistant recipes&lt;/li&gt;
&lt;li&gt;Frequently asked questions&lt;/li&gt;
&lt;li&gt;Practice on PipeCode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Why LLM assistants are the new UX layer for analytics teams in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Four assistants, four wildly different grounding stories — the pick that binds your semantic layer for years
&lt;/h3&gt;

&lt;p&gt;The one-sentence invariant: &lt;strong&gt;an &lt;code&gt;llm data assistant&lt;/code&gt; for analytics is a picking exercise between authoring-time completions grounded on your dbt project graph, serving-time text-to-SQL grounded on a hand-authored YAML semantic model, natural-language BI grounded on Unity Catalog comments and sample values, or an ungrounded generic chatbot that hallucinates joins on ambiguous column names — and each assistant trades grounding fidelity against governance boundary, target persona, and latency budget in a way that surfaces as either a productivity boost or a compliance incident depending on how you deploy it&lt;/strong&gt;. The assistant you pick in month one becomes the grounding contract you fight to maintain in year three, because every downstream question, every trained business user, and every executive dashboard hard-codes assumptions about which metric definition is "correct" and which synonyms map to which columns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes interviewers actually probe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grounding source.&lt;/strong&gt; dbt Copilot grounds on the compiled &lt;code&gt;manifest.json&lt;/code&gt; and &lt;code&gt;catalog.json&lt;/code&gt; — every &lt;code&gt;ref()&lt;/code&gt;, every source, every existing macro is context. Cortex Analyst grounds on a hand-authored YAML &lt;code&gt;semantic layer&lt;/code&gt; file that declares tables, dimensions, measures, time-dimensions, filters, and synonyms. Genie grounds on Unity Catalog table comments, column comments, sample values, and curated example queries. A generic LLM (raw ChatGPT / Claude API) grounds on whatever the user pastes into the prompt — usually nothing. Interviewers open with this question because grounding is what separates a useful assistant from a hallucinating one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance boundary.&lt;/strong&gt; Cortex Analyst executes the generated SQL with the caller's Snowflake role — row-level policies and column masks bind automatically. Genie executes with Unity Catalog governance — same story. dbt Copilot only &lt;em&gt;drafts&lt;/em&gt; SQL; a human commits and dbt Cloud runs it under the project's warehouse credentials. Generic LLMs have no governance boundary — they'll suggest queries against tables the user has no grant on, and it's on you to enforce access at the warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target persona.&lt;/strong&gt; dbt Copilot is aimed at &lt;strong&gt;analytics engineers&lt;/strong&gt; — the people writing models. Cortex Analyst is aimed at &lt;strong&gt;analysts and developers&lt;/strong&gt; — anyone writing SQL against the semantic model or embedding the REST API in an internal app. Genie is aimed at &lt;strong&gt;business users&lt;/strong&gt; — sales ops, marketing analysts, finance partners who never learned SQL. Deploying the wrong tool to the wrong persona is the fastest way to erode trust: a business user getting a hallucinated Copilot completion assumes it's correct; an analyst getting a Genie chat window feels handcuffed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency + human review.&lt;/strong&gt; dbt Copilot completions are inline, ~500 ms; a human always reviews before commit. Cortex Analyst REST calls are 2-5 seconds and return the generated SQL for programmatic review before execution (or you can auto-execute — your call). Genie is 3-8 seconds per turn, and end users see the SQL below the chart so they can flag surprises. Generic LLMs vary wildly; usually no built-in review loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The 2026 reality — every vendor ships an assistant, but only three ship one with a real grounding story.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;dbt Copilot&lt;/strong&gt; is the default authoring assistant for any team using dbt Cloud IDE in 2026. It reads your compiled project graph on every completion, so &lt;code&gt;ref('dim_customers')&lt;/code&gt; autocompletes and generated SQL references real columns from your &lt;code&gt;catalog.json&lt;/code&gt;. Prior to Copilot, the equivalent was "GitHub Copilot in your IDE guessing." Ground-truth grounding is what separates a project-aware assistant from a hallucinating one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake Cortex Analyst&lt;/strong&gt; is the default text-to-SQL layer for teams whose analytics warehouse is Snowflake. It's a REST API on top of Cortex; you author a &lt;code&gt;semantic_model.yaml&lt;/code&gt; describing your tables, measures, and dimensions; you POST a natural-language question; it returns generated SQL, an interpretation, and optionally a chart spec. The Snowflake role executing the query is the caller's role — governance is inherited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databricks AI/BI Genie&lt;/strong&gt; is the default natural-language BI experience inside Databricks. Genie spaces are scoped to a specific set of Unity Catalog tables; business users chat with the space; Genie generates SQL grounded on the tables' comments, sample values, and any curated example queries you've supplied. AI/BI Dashboards embed Genie as a "ask a follow-up" pane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic LLM wrappers&lt;/strong&gt; — Cursor / VS Code + raw Claude / GPT-4o — are alive and well for one-off analysis, exploration, and coding help, but they lack the grounding contract that vendor assistants supply. They belong in the &lt;em&gt;engineer's&lt;/em&gt; toolbox, not in the &lt;em&gt;business user's&lt;/em&gt; dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What interviewers listen for.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you name &lt;strong&gt;all three vendor assistants&lt;/strong&gt; without prompting? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you say &lt;strong&gt;"grounding"&lt;/strong&gt; in the first sentence when the interviewer asks about hallucination? — required answer.&lt;/li&gt;
&lt;li&gt;Do you push back on &lt;strong&gt;"we'll just wrap an LLM"&lt;/strong&gt; with the governance question — "whose role does the SQL run under?" — senior signal.&lt;/li&gt;
&lt;li&gt;Do you name the &lt;strong&gt;semantic layer&lt;/strong&gt; as the thing every assistant needs to be grounded on, not as "a dbt feature"? — senior signal.&lt;/li&gt;
&lt;li&gt;Do you describe LLM assistants as &lt;strong&gt;"UX layers over a grounding source"&lt;/strong&gt; rather than as vague "AI features"? — required answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worked example — the four-axis comparison table
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The single most useful artifact for an LLM-assistant interview is a memorised 4×4 comparison table. Every senior discussion converges on this table within the first ten minutes; having it in your head is what separates a fluent answer from a stumbling one. Walk through building the table for a hypothetical B2B SaaS analytics team migrating from raw SQL to LLM-assisted workflows across three personas.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Team shape.&lt;/strong&gt; 3 analytics engineers (author dbt models), 6 analysts (write SQL, build dashboards), ~50 business users (marketing, sales ops, finance) who never write SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warehouse.&lt;/strong&gt; Snowflake — primary; a Databricks lakehouse for ML feature engineering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transformation.&lt;/strong&gt; dbt Cloud managed project against the Snowflake warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance.&lt;/strong&gt; SOC2 Type II; row-level policies on customer data; column masks on PII.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Build the four-assistant comparison for this team and assign the right assistant to each persona.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assistant&lt;/th&gt;
&lt;th&gt;Grounding source&lt;/th&gt;
&lt;th&gt;Persona&lt;/th&gt;
&lt;th&gt;Governance&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dbt Copilot&lt;/td&gt;
&lt;td&gt;manifest.json + catalog.json&lt;/td&gt;
&lt;td&gt;analytics engineer&lt;/td&gt;
&lt;td&gt;project-scoped; human commits&lt;/td&gt;
&lt;td&gt;~500 ms inline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cortex Analyst&lt;/td&gt;
&lt;td&gt;YAML semantic model&lt;/td&gt;
&lt;td&gt;analyst / dev&lt;/td&gt;
&lt;td&gt;caller's Snowflake role&lt;/td&gt;
&lt;td&gt;2-5 s per REST call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI/BI Genie&lt;/td&gt;
&lt;td&gt;Unity Catalog + example queries&lt;/td&gt;
&lt;td&gt;business user&lt;/td&gt;
&lt;td&gt;Unity Catalog policies&lt;/td&gt;
&lt;td&gt;3-8 s per turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic LLM&lt;/td&gt;
&lt;td&gt;none (whatever prompt says)&lt;/td&gt;
&lt;td&gt;engineer (ad hoc)&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;1-10 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Semantic layer YAML fragment — the grounding contract for Cortex Analyst&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue_model&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Grounded semantic model over fct_orders + dim_customers for the&lt;/span&gt;
  &lt;span class="s"&gt;finance and sales-ops teams. Every measure is documented; every&lt;/span&gt;
  &lt;span class="s"&gt;synonym is enumerated; every FK relationship is declared.&lt;/span&gt;

&lt;span class="na"&gt;tables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fct_orders&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;One row per order; grain is order_id.&lt;/span&gt;
    &lt;span class="na"&gt;base_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PROD_ANALYTICS&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;MART&lt;/span&gt;
      &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;FCT_ORDERS&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DATE&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;day&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;order day&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TEXT&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;geo&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;territory&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sales region&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SUM(total_cents) / &lt;/span&gt;&lt;span class="m"&gt;100.0&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NUMBER&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gross revenue in USD (dollars, not cents).&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;sales&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;gross revenue&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;total sales&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;time_dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DATE&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;day&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;order day&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The team's three personas each need a different UX. Analytics engineers live inside dbt Cloud IDE — dbt Copilot is where their productivity gain lives, and the grounding source is &lt;em&gt;already the artifact they maintain&lt;/em&gt; (the dbt project). No extra work.&lt;/li&gt;
&lt;li&gt;Analysts live in Snowsight (Snowflake's UI) and internal apps that call the warehouse programmatically. They need &lt;code&gt;natural language to sql&lt;/code&gt; that respects the metric definitions the analytics engineers curated. Cortex Analyst is grounded on a YAML semantic model — someone has to author that YAML, but it's a bounded artifact scoped to one metric domain (revenue, funnel, retention) at a time.&lt;/li&gt;
&lt;li&gt;Business users live in dashboards. They want to ask "why did revenue drop in the west region last week?" and get an answer with a chart. Genie spaces are scoped to specific Unity Catalog tables and grounded on table + column comments and curated example queries. The important word is &lt;em&gt;scoped&lt;/em&gt;: a Genie space over the marketing tables cannot join to finance tables, which prevents cross-domain hallucinations.&lt;/li&gt;
&lt;li&gt;The generic LLM row is the strawman. Every candidate proposes "let's just wrap Claude" at some point. Naming the grounding gap ("Claude doesn't know your column names, your metric definitions, or your row-level policies") is the senior response.&lt;/li&gt;
&lt;li&gt;The most common architectural mistake is deploying one assistant to all three personas. A business user getting a raw Cortex Analyst text-to-SQL prompt without a chart wrapper feels lost; an analytics engineer inside a Genie chat window feels handcuffed. Match the assistant to the persona.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Persona&lt;/th&gt;
&lt;th&gt;Recommended assistant&lt;/th&gt;
&lt;th&gt;Grounding source&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Analytics engineer&lt;/td&gt;
&lt;td&gt;dbt Copilot&lt;/td&gt;
&lt;td&gt;manifest.json + catalog.json&lt;/td&gt;
&lt;td&gt;authoring layer; drafts models + docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyst&lt;/td&gt;
&lt;td&gt;Cortex Analyst REST&lt;/td&gt;
&lt;td&gt;semantic_model.yaml&lt;/td&gt;
&lt;td&gt;governed text-to-SQL for internal apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business user&lt;/td&gt;
&lt;td&gt;Genie space&lt;/td&gt;
&lt;td&gt;Unity Catalog + example queries&lt;/td&gt;
&lt;td&gt;chat + chart; scoped to marketing tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineer (ad hoc)&lt;/td&gt;
&lt;td&gt;generic LLM (Claude / Cursor)&lt;/td&gt;
&lt;td&gt;prompt only&lt;/td&gt;
&lt;td&gt;exploration; never in production&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Never pick an LLM assistant based on "which one has the most demos." Pick it based on (grounding × persona × governance × latency) — the four axes. Draw the assignment table on a whiteboard first; the vendor falls out of the constraints.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — what interviewers actually probe
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The senior data / analytics engineering interview on LLM assistants has a predictable structure: the interviewer opens with an ambiguous question ("how would you let our business users query the warehouse in English?"), then progressively narrows to test whether you know the axes. Candidates who name the grounding source in sentence one score highest; candidates who describe "a Slack bot with ChatGPT" score lowest. Walk through the interview grading rubric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous opener.&lt;/strong&gt; "How would you let a non-technical exec ask our warehouse questions?" — invites you to name a vendor + grounding source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 1.&lt;/strong&gt; "How do you stop it from hallucinating joins?" — probes grounding axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 2.&lt;/strong&gt; "How do you enforce row-level access?" — probes governance axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 3.&lt;/strong&gt; "What's the latency budget?" — probes latency + persona axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-up 4.&lt;/strong&gt; "How would you evaluate whether the assistant is answering correctly?" — probes review + eval-harness axis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Draft a 5-minute senior LLM-assistant answer that covers all four axes without waiting to be asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interview signal&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;Senior answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vendor + grounding named&lt;/td&gt;
&lt;td&gt;"we'd use ChatGPT"&lt;/td&gt;
&lt;td&gt;"Cortex Analyst grounded on a YAML semantic model"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucination story&lt;/td&gt;
&lt;td&gt;"we'd fine-tune it"&lt;/td&gt;
&lt;td&gt;"grounded on declared measures + synonyms; no free-form joins"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;"we'd add auth"&lt;/td&gt;
&lt;td&gt;"runs under caller's Snowflake role; RLS and column masks inherited"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency budget&lt;/td&gt;
&lt;td&gt;"should be fast"&lt;/td&gt;
&lt;td&gt;"2-5 s per REST call; async for long queries"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluation&lt;/td&gt;
&lt;td&gt;"we'd try it"&lt;/td&gt;
&lt;td&gt;"golden-question set; ANSWER-vs-EXPECTED SQL diff; semantic equivalence"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Senior LLM-assistant answer template (5 minutes)
================================================

Minute 1 — name the assistant + grounding source up front
  "For business-user natural-language BI on our Snowflake stack,
   I'd deploy Snowflake Cortex Analyst, grounded on a hand-authored
   YAML semantic model that declares our tables, measures, dimensions,
   time-dimensions, and synonyms. For the dashboard experience I'd
   wrap it in an internal app that shows the generated SQL and the
   interpretation alongside the answer."

Minute 2 — hallucination containment
  "The YAML semantic model is the grounding contract. The LLM cannot
   generate a measure that isn't declared, can't join tables that
   don't have a declared FK, can't reference columns that aren't in
   the model. Synonyms map user-facing language ('sales', 'revenue',
   'gross') to the same underlying measure. This is qualitatively
   different from wrapping a generic LLM — the grounding source
   bounds the hypothesis space."

Minute 3 — governance
  "Cortex Analyst returns generated SQL, which is then executed via
   Snowflake with the caller's role. Row-access policies, column
   masks, and warehouse grants all bind automatically. The LLM never
   sees the row values — it only sees the schema described by the
   YAML. This is critical for SOC2 and HIPAA workloads."

Minute 4 — latency + human review
  "REST latency is 2-5 s per turn. For interactive chart panes we
   render a spinner and stream the interpretation; for programmatic
   use we return the generated SQL for a rules-based safety check
   before we execute it. Every response ships the SQL below the
   chart so power users can spot-check."

Minute 5 — evaluation harness
  "We maintain a golden-question suite — 100-500 questions per
   semantic model, each with an expected SQL query. On every model
   change we run the suite: generate SQL, semantically diff against
   expected (canonicalise, ignore alias order, compare join graph +
   filter set + measure), report exact-match rate + semantic-match
   rate. A regression on either metric blocks the merge."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Minute 1 is the crucial framing. Naming the assistant &lt;em&gt;and&lt;/em&gt; the grounding source immediately — "Cortex Analyst grounded on a YAML semantic model" — signals you know what makes an assistant useful. Weak candidates name a tool without a grounding story ("we'd use ChatGPT / Claude / a copilot").&lt;/li&gt;
&lt;li&gt;Minute 2 addresses hallucination before the interviewer asks. The naive "we'll fine-tune it" answer is a red flag — fine-tuning does not stop hallucination on a schema the model has never seen. The senior answer is "the grounding source is the safety rail; the LLM can only generate against what's declared."&lt;/li&gt;
&lt;li&gt;Minute 3 is the governance probe. Cortex Analyst (and Genie) executing under the caller's role is the killer feature vs a generic LLM app that has its own service account. Naming row-access policies and column masks unprompted is senior signal.&lt;/li&gt;
&lt;li&gt;Minute 4 is the latency + human-review argument. Every generated response ships the SQL for human spot-check; this is the "trust but verify" contract. Answering "our users don't need to see the SQL" is a red flag — they always need to see it, even if 90% of them never look.&lt;/li&gt;
&lt;li&gt;Minute 5 is the evaluation-harness answer. The single biggest professional-signal marker is "we have a golden-question suite and we run it on every model change." Candidates who name the eval harness get graded senior; candidates who don't get graded mid.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grading criterion&lt;/th&gt;
&lt;th&gt;Weak score&lt;/th&gt;
&lt;th&gt;Senior score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Names vendor + grounding in minute 1&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names hallucination containment&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names governance mechanism&lt;/td&gt;
&lt;td&gt;occasional&lt;/td&gt;
&lt;td&gt;mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names latency budget + review UX&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names evaluation harness&lt;/td&gt;
&lt;td&gt;rare&lt;/td&gt;
&lt;td&gt;senior signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The senior LLM-assistant answer is a 5-minute monologue that covers all four axes without waiting for the follow-ups. Rehearse it once; deploy it every time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the "pick the assistant" decision tree
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; Given a new analytics workload, the senior architect runs a 4-question decision tree in their head. Codifying the tree makes the interview answer reproducible: any interviewer can hand you a scenario and you can walk the tree out loud. Walk through the tree with three canonical scenarios: authoring a new dbt project, embedding text-to-SQL in an internal analyst tool, and giving a marketing team a chat-with-your-data dashboard.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Q1.&lt;/strong&gt; Is the user an analytics engineer authoring transformations? → yes = dbt Copilot; no = go to Q2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q2.&lt;/strong&gt; Is the warehouse Snowflake and the user an analyst or developer needing SQL? → yes = Cortex Analyst REST API; no = go to Q3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q3.&lt;/strong&gt; Is the warehouse Databricks and the user a business user wanting a chart? → yes = AI/BI Genie space; no = go to Q4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q4.&lt;/strong&gt; Does the user need one-off exploration outside a governed workload? → yes = generic LLM in Cursor / VS Code; no = tell them "no LLM yet — build the grounding first."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q5 (parallel branch).&lt;/strong&gt; Is the semantic layer defined? → no = author it &lt;em&gt;first&lt;/em&gt; (dbt semantic models + a YAML export for Cortex Analyst + Unity Catalog comments for Genie).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Walk the decision tree for the three scenarios and record the assistant each ends up with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Q1 (author?)&lt;/th&gt;
&lt;th&gt;Q2 (Snowflake analyst?)&lt;/th&gt;
&lt;th&gt;Q3 (Databricks BI?)&lt;/th&gt;
&lt;th&gt;Q5 (semantic layer?)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Author new dbt project&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;in progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyst internal tool on Snowflake&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes (revenue_model.yaml)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketing chat dashboard on Databricks&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (comments + examples)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Decision-tree helper (illustrative)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pick_llm_assistant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_authoring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;is_snowflake_analyst&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;is_databricks_business_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;semantic_layer_defined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;is_exploration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the primary LLM assistant recommendation.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;picks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_authoring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt Copilot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_snowflake_analyst&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;semantic_layer_defined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author revenue_model.yaml before enabling Cortex Analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Snowflake Cortex Analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_databricks_business_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;semantic_layer_defined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;populate UC comments + example queries before enabling Genie&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Databricks AI/BI Genie&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_exploration&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generic LLM (Cursor / Claude)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no assistant matches; refine the persona question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;picks&lt;/span&gt;


&lt;span class="c1"&gt;# Walk the three scenarios
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_llm_assistant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# → ['dbt Copilot']
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_llm_assistant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# → ['Snowflake Cortex Analyst']
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick_llm_assistant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# → ['Databricks AI/BI Genie']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scenario 1 — an analytics engineer authoring a new dbt project. Q1 short-circuits at "yes" → dbt Copilot. This is the modern default for the authoring persona. Copilot grounds on whatever exists in the project graph; on a greenfield project the completions become richer as the project fills out.&lt;/li&gt;
&lt;li&gt;Scenario 2 — an analyst embedding SQL into an internal margin-analysis app on Snowflake. Q2 = yes → Cortex Analyst REST API. The Q5 guard is critical: without a &lt;code&gt;semantic_layer&lt;/code&gt; YAML, Cortex Analyst has nothing to ground on and its output collapses to whatever the LLM guesses from table names. Author the YAML before enabling the assistant.&lt;/li&gt;
&lt;li&gt;Scenario 3 — a marketing team wanting a chart-with-conversation experience on their Databricks lakehouse. Q3 = yes → Genie space. Same Q5 guard: without populated Unity Catalog comments and a handful of curated example queries, the space returns brittle answers. The setup is "populate comments → curate examples → enable Genie".&lt;/li&gt;
&lt;li&gt;The parallel Q5 branch (semantic layer) is orthogonal to the primary pick. Every serving-layer assistant (Cortex Analyst, Genie) requires a defined semantic layer to ground on. Refusing to enable the assistant until the semantic layer is ready is senior signal — it prevents shipping a hallucinating tool.&lt;/li&gt;
&lt;li&gt;If none of Q1-Q4 pass, the honest answer is "the request isn't specific enough — refine the persona." Deploying an assistant to "everyone" is the fastest way to burn goodwill; scope it to a persona and a use case first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Primary assistant&lt;/th&gt;
&lt;th&gt;Prereq&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Author dbt project&lt;/td&gt;
&lt;td&gt;dbt Copilot&lt;/td&gt;
&lt;td&gt;dbt Cloud IDE enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyst app on Snowflake&lt;/td&gt;
&lt;td&gt;Cortex Analyst REST&lt;/td&gt;
&lt;td&gt;revenue_model.yaml authored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketing chat on Databricks&lt;/td&gt;
&lt;td&gt;AI/BI Genie space&lt;/td&gt;
&lt;td&gt;Unity Catalog comments + example queries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; The five-question decision tree is a whiteboard-friendly answer. Practice walking it end-to-end so an interviewer can hand you any scenario and get a vendor name in under 60 seconds. And &lt;em&gt;always&lt;/em&gt; name the semantic-layer prerequisite for the serving-layer assistants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on LLM assistant selection
&lt;/h3&gt;

&lt;p&gt;A senior interviewer often opens with: "You inherit a Snowflake + dbt analytics stack with 40 dashboards, 6 analysts, and a leadership team that wants to 'ask questions in English.' The previous team shipped a raw ChatGPT slack bot; it hallucinates joins and leaks PII. Walk me through the LLM-assistant strategy you'd deploy, the grounding sources you'd author, and the evaluation harness you'd ship on day one."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a stacked assistant deployment — dbt Copilot for authors, Cortex Analyst for analysts, and a golden-question eval harness
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. Semantic model YAML — the grounding contract for Cortex Analyst&lt;/span&gt;
&lt;span class="c1"&gt;#    File: semantic_models/revenue_model.yaml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue_model&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Governed revenue semantic model for the finance + sales-ops teams.&lt;/span&gt;
  &lt;span class="s"&gt;Grounded on fct_orders (grain: order_id) joined to dim_customers,&lt;/span&gt;
  &lt;span class="s"&gt;dim_products, and the date spine. Every measure is declared; every&lt;/span&gt;
  &lt;span class="s"&gt;synonym is enumerated. Cortex Analyst is not allowed to generate&lt;/span&gt;
  &lt;span class="s"&gt;a measure that isn't in this file.&lt;/span&gt;

&lt;span class="na"&gt;tables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fct_orders&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;One row per order; grain is order_id.&lt;/span&gt;
    &lt;span class="na"&gt;base_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PROD_ANALYTICS&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;MART&lt;/span&gt;
      &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;FCT_ORDERS&lt;/span&gt;
    &lt;span class="na"&gt;primary_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;order_id&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DATE&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;day&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;order day&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;transaction date&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TEXT&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;geo&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;territory&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sales region&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_segment&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_segment&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TEXT&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;segment&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;customer tier&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;time_dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DATE&lt;/span&gt;

    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SUM(total_cents) / &lt;/span&gt;&lt;span class="m"&gt;100.0&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NUMBER&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gross revenue in USD.&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;sales&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;gross revenue&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;total sales&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;top line&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_count&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COUNT(*)&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NUMBER&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Number of orders.&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;orders&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;transactions&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;num orders&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unique_customers&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COUNT(DISTINCT customer_id)&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NUMBER&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Distinct customers with at least one order.&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;customers&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;buyers&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;unique buyers&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;verified_queries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue_by_region_last_quarter&lt;/span&gt;
    &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;What was our revenue by region last quarter?&lt;/span&gt;
    &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;SELECT region, SUM(total_cents)/100.0 AS revenue&lt;/span&gt;
      &lt;span class="s"&gt;FROM   PROD_ANALYTICS.MART.FCT_ORDERS&lt;/span&gt;
      &lt;span class="s"&gt;WHERE  order_date BETWEEN DATE_TRUNC('quarter', DATEADD('quarter', -1, CURRENT_DATE))&lt;/span&gt;
                            &lt;span class="s"&gt;AND LAST_DAY(DATEADD('quarter', -1, CURRENT_DATE))&lt;/span&gt;
      &lt;span class="s"&gt;GROUP  BY region&lt;/span&gt;
      &lt;span class="s"&gt;ORDER  BY revenue DESC;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. Golden-question evaluation harness (runs in CI on every YAML change)
#    File: eval/run_golden_questions.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sql_metadata&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Parser&lt;/span&gt;   &lt;span class="c1"&gt;# canonicalises SQL for semantic diff
&lt;/span&gt;
&lt;span class="n"&gt;CORTEX_URL&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;account&amp;gt;.snowflakecomputing.com/api/v2/cortex/analyst/message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;SEMANTIC_YAML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@PROD_ANALYTICS.SEMANTIC_MODELS/revenue_model.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canonicalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Reduce SQL to (tables, join_graph, filter_set, measure_set, group_by).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;with_names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_names&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_semantic_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_golden_question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CORTEX_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}]}],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic_model_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SEMANTIC_YAML&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;sql_returned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
         &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actual_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;sql_returned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic_match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;score_semantic_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sql_returned&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;questions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval/golden_questions.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/run/secrets/snowflake_pat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;run_golden_question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;n&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;exact&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic_match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;partial&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic_match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exact-match: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exact&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exact&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic-avg: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exact&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;golden-question regression; blocking merge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Sample&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;golden-question&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;file&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What was our revenue by region last quarter?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"expected_sql"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SELECT region, SUM(total_cents)/100.0 AS revenue FROM PROD_ANALYTICS.MART.FCT_ORDERS WHERE order_date BETWEEN DATE_TRUNC('quarter', DATEADD('quarter', -1, CURRENT_DATE)) AND LAST_DAY(DATEADD('quarter', -1, CURRENT_DATE)) GROUP BY region ORDER BY revenue DESC"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"How many unique customers placed orders in the enterprise segment last month?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"expected_sql"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SELECT COUNT(DISTINCT customer_id) AS unique_customers FROM PROD_ANALYTICS.MART.FCT_ORDERS WHERE customer_segment = 'enterprise' AND order_date &amp;gt;= DATE_TRUNC('month', DATEADD('month', -1, CURRENT_DATE)) AND order_date &amp;lt; DATE_TRUNC('month', CURRENT_DATE)"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Before (raw ChatGPT bot)&lt;/th&gt;
&lt;th&gt;After (stacked assistants + eval)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Grounding&lt;/td&gt;
&lt;td&gt;none (prompt only)&lt;/td&gt;
&lt;td&gt;dbt project graph + revenue_model.yaml + UC comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Author persona&lt;/td&gt;
&lt;td&gt;not addressed&lt;/td&gt;
&lt;td&gt;dbt Copilot inline in dbt Cloud IDE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyst persona&lt;/td&gt;
&lt;td&gt;ChatGPT bot&lt;/td&gt;
&lt;td&gt;Cortex Analyst REST wrapped in analyst tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business-user persona&lt;/td&gt;
&lt;td&gt;ChatGPT bot&lt;/td&gt;
&lt;td&gt;(future) Genie space over marketing tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;bot has service account&lt;/td&gt;
&lt;td&gt;Cortex Analyst executes as caller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucination containment&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;YAML declares measures + synonyms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluation&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;golden-question suite in CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reg-block on regression&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;exact-match &amp;lt; 90% blocks merge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the migration, the analytics engineers get inline dbt Copilot completions grounded on the compiled project; the analyst app calls Cortex Analyst with &lt;code&gt;revenue_model.yaml&lt;/code&gt; as the grounding source and every generated SQL runs under the caller's Snowflake role; the golden-question harness runs in CI on every YAML change and blocks merges that break more than 10% of the questions. The raw ChatGPT slack bot is retired.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hallucinated joins in generated SQL&lt;/td&gt;
&lt;td&gt;~40% of responses&lt;/td&gt;
&lt;td&gt;&amp;lt; 5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PII leak risk&lt;/td&gt;
&lt;td&gt;service account had wide grants&lt;/td&gt;
&lt;td&gt;caller role only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance boundary&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;RLS + column masks inherited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic-match on golden questions&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;td&gt;95%+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-to-first-generated-SQL&lt;/td&gt;
&lt;td&gt;8 s&lt;/td&gt;
&lt;td&gt;3-5 s (Cortex) / 500 ms (Copilot)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Stacked assistants&lt;/strong&gt;&lt;/strong&gt; — dbt Copilot at the authoring layer, Cortex Analyst at the serving layer, Genie (future) at the business-user layer. Each assistant grounds on the artifact its persona already produces or reads; no assistant is asked to do the work of another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;YAML semantic model&lt;/strong&gt;&lt;/strong&gt; — the declarative grounding contract. Measures are defined once, synonyms are enumerated, joins are declared. Cortex Analyst cannot generate a measure that isn't in the YAML; the hypothesis space is bounded by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Caller-role execution&lt;/strong&gt;&lt;/strong&gt; — Cortex Analyst returns the generated SQL, then Snowflake executes it under the caller's role. Row-access policies, column masks, warehouse grants all bind automatically. The LLM never touches row values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Verified queries block&lt;/strong&gt;&lt;/strong&gt; — the &lt;code&gt;verified_queries&lt;/code&gt; section of the YAML acts as few-shot examples for the model &lt;em&gt;and&lt;/em&gt; as documentation for humans. Cortex Analyst is more likely to produce a correct answer for a question that closely matches a verified query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Golden-question harness&lt;/strong&gt;&lt;/strong&gt; — the deterministic regression check. Every YAML change runs the suite; semantic-diff against expected SQL; block merges below the threshold. This is what turns an LLM assistant from a demo into a shippable product. Cost: O(1) API call per golden question per CI run; typical suites run in under 60 seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems on semantic-layer and text-to-SQL patterns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL Generation&lt;/span&gt;
&lt;span&gt;Topic — sql-generation&lt;/span&gt;
&lt;strong&gt;SQL-generation and natural-language-to-SQL problems&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  2. dbt Copilot — model-authoring, docs, and semantic-layer completions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;dbt copilot&lt;/code&gt; grounds on the project graph — the authoring assistant that turns manifest.json into inline completions
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;dbt copilot&lt;/code&gt; is the LLM assistant embedded in the dbt Cloud IDE that grounds every completion, refactor, and generation on the compiled dbt project (manifest.json, catalog.json, existing macros, upstream refs) — it drafts model SQL, YAML docs, tests, and semantic-layer metric definitions with knowledge of your actual column names and lineage, and its productivity gain is entirely proportional to how well-modelled your existing project already is&lt;/strong&gt;. Every analytics engineer using dbt Cloud in 2026 has Copilot available; the difference between teams getting value and teams generating hallucinated &lt;code&gt;ref()&lt;/code&gt; calls is whether the project's manifest is fresh and the seeds / sources are complete.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbhxxocm8i93jsw3r2d9.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbhxxocm8i93jsw3r2d9.jpeg" alt="Iconographic dbt Copilot diagram — a dbt project IDE panel on the left with a manifest.json card feeding an LLM sparkle, and generated model.sql + schema.yml cards on the right; a chip warns 'grounded on project graph'." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for dbt Copilot.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grounding.&lt;/strong&gt; Compiled project graph — &lt;code&gt;manifest.json&lt;/code&gt; (every &lt;code&gt;ref()&lt;/code&gt;, every &lt;code&gt;source()&lt;/code&gt;, every macro, every test) plus &lt;code&gt;catalog.json&lt;/code&gt; (columns, data types, row counts). Copilot re-reads the manifest on every completion; a stale manifest = stale completions. This is the strongest grounding of any authoring assistant on the market.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persona.&lt;/strong&gt; Analytics engineers writing dbt models, tests, YAML docs, and semantic-model metrics. Not aimed at business users; not aimed at analysts writing ad-hoc SQL. If your team doesn't author dbt models, Copilot is not the assistant for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance.&lt;/strong&gt; Project-scoped, not warehouse-scoped. Copilot only &lt;em&gt;drafts&lt;/em&gt; SQL — a human commits and dbt Cloud runs it under the project's warehouse credentials. Copilot itself has no runtime governance boundary; the safety comes from the human review + CI pipeline (dbt test, sqlfluff, code review).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency + review.&lt;/strong&gt; Inline, ~500 ms per completion. Every suggestion is reviewed by the analytics engineer at author time; there is no auto-commit path. This "always reviewed" contract is what makes Copilot safe for authoring workloads even without a runtime governance layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What Copilot actually generates — the four common patterns.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model SQL from a natural-language prompt.&lt;/strong&gt; "Build an incremental &lt;code&gt;fct_orders&lt;/code&gt; from &lt;code&gt;stg_orders&lt;/code&gt; and &lt;code&gt;stg_order_lines&lt;/code&gt;, grain order_id, with a is_backfill flag." Copilot reads the referenced staging models' columns, drafts a SELECT with the right joins, adds the incremental config, and inserts a Jinja &lt;code&gt;is_incremental()&lt;/code&gt; block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YAML docs and tests.&lt;/strong&gt; "Document &lt;code&gt;fct_orders&lt;/code&gt; and add tests for the primary key + not-null on customer_id." Copilot reads the model's columns from the catalog, drafts a YAML block with descriptions and adds &lt;code&gt;unique&lt;/code&gt; + &lt;code&gt;not_null&lt;/code&gt; tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactors.&lt;/strong&gt; "Refactor this CTE into a separate staging model." Copilot extracts the CTE, creates a new staging file, wires the &lt;code&gt;ref()&lt;/code&gt;, and rewrites the calling model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic-layer metrics.&lt;/strong&gt; "Define a &lt;code&gt;total_revenue&lt;/code&gt; metric on &lt;code&gt;fct_orders&lt;/code&gt;." Copilot drafts a &lt;code&gt;semantic_models.yml&lt;/code&gt; metric block with the measure expression, dimension bindings, and description.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The manifest freshness problem — the failure mode Copilot inherits from dbt.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The mechanism.&lt;/strong&gt; Copilot reads the &lt;em&gt;last-compiled&lt;/em&gt; manifest.json. If you haven't compiled since adding a new source or column, Copilot doesn't know about it — completions reference the old graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix.&lt;/strong&gt; Compile on save, or trust the dbt Cloud IDE's live compile (enabled by default). If completions look wrong, the first debugging step is "did the manifest recompile?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The subtle case.&lt;/strong&gt; A colleague pushes a new source table; you pull; you haven't compiled. Copilot completions for a model that references the new source will silently miss it. The fix is a &lt;code&gt;dbt compile&lt;/code&gt; before serious authoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on dbt Copilot.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What does dbt Copilot ground on?" — required answer: compiled manifest.json + catalog.json.&lt;/li&gt;
&lt;li&gt;"How do you stop it from hallucinating column names?" — required answer: keep the catalog fresh (&lt;code&gt;dbt docs generate&lt;/code&gt; after schema changes) and compile before authoring.&lt;/li&gt;
&lt;li&gt;"When would you pick dbt Copilot over GitHub Copilot?" — required answer: when the productivity comes from project-graph awareness (refs, sources, columns) that a generic code assistant doesn't have.&lt;/li&gt;
&lt;li&gt;"What about governance?" — required answer: Copilot is authoring-only; runtime governance is handled by the dbt Cloud project's warehouse credentials + code review.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — prompting dbt Copilot to draft an &lt;code&gt;fct_orders&lt;/code&gt; incremental model
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical dbt Copilot flow: you open a new model file &lt;code&gt;models/marts/fct_orders.sql&lt;/code&gt;, describe what you want in a comment or a chat prompt, and Copilot drafts a full incremental model that references your existing staging models. Walk through the flow and what Copilot pulls from the project graph.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Preconditions.&lt;/strong&gt; &lt;code&gt;stg_orders.sql&lt;/code&gt; and &lt;code&gt;stg_order_lines.sql&lt;/code&gt; exist and are compiled; both have documented columns in &lt;code&gt;models/staging/schema.yml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The prompt.&lt;/strong&gt; "Build an incremental fct_orders joining stg_orders and stg_order_lines by order_id, grain order_id, materialised as incremental with unique_key=order_id and on_schema_change=fail."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copilot's context.&lt;/strong&gt; Reads manifest.json to find &lt;code&gt;stg_orders&lt;/code&gt; and &lt;code&gt;stg_order_lines&lt;/code&gt;; reads catalog.json to know their columns; reads existing macros to reuse &lt;code&gt;is_incremental()&lt;/code&gt; boilerplate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Show the natural-language prompt, the drafted model, and the YAML block Copilot generates alongside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt&lt;/td&gt;
&lt;td&gt;"incremental fct_orders joining stg_orders + stg_order_lines, grain order_id"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Referenced models&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;stg_orders&lt;/code&gt;, &lt;code&gt;stg_order_lines&lt;/code&gt; (already compiled)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target materialisation&lt;/td&gt;
&lt;td&gt;incremental, unique_key=order_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML expected&lt;/td&gt;
&lt;td&gt;fct_orders block with description + PK test&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- models/marts/fct_orders.sql&lt;/span&gt;
&lt;span class="c1"&gt;-- Prompt: incremental fct_orders joining stg_orders + stg_order_lines,&lt;/span&gt;
&lt;span class="c1"&gt;--         grain order_id, materialised as incremental with unique_key.&lt;/span&gt;

&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;materialized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'incremental'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;unique_key&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'order_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_schema_change&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'fail'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_incremental&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'1970-01-01'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="p"&gt;}})&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endif&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;order_lines&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt;
        &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total_quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_order_lines'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;select&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;left&lt;/span&gt; &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;order_lines&lt;/span&gt; &lt;span class="n"&gt;ol&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# models/marts/schema.yml (drafted alongside the model)&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;

&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fct_orders&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;Fact table at the grain of one row per order, joined to summed&lt;/span&gt;
      &lt;span class="s"&gt;order-line totals. Materialised as incremental keyed on&lt;/span&gt;
      &lt;span class="s"&gt;updated_at from stg_orders. Failing on schema change (add columns&lt;/span&gt;
      &lt;span class="s"&gt;via explicit migrations, not silent evolution).&lt;/span&gt;

    &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_id&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Primary key.&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Foreign key to dim_customers.&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;relationships&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('dim_customers')&lt;/span&gt;
              &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_cents&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sum of quantity * unit_price across all order lines.&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Business-effective order date (from stg_orders).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copilot reads the prompt (either as a chat message or as a leading SQL comment). It parses the intent: "incremental fact table, grain order_id, joining two staging models."&lt;/li&gt;
&lt;li&gt;It queries the project graph via manifest.json to locate &lt;code&gt;stg_orders&lt;/code&gt; and &lt;code&gt;stg_order_lines&lt;/code&gt;. It reads catalog.json to enumerate their columns — &lt;code&gt;order_id&lt;/code&gt;, &lt;code&gt;customer_id&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;region&lt;/code&gt;, &lt;code&gt;customer_segment&lt;/code&gt;, &lt;code&gt;order_date&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;updated_at&lt;/code&gt; on &lt;code&gt;stg_orders&lt;/code&gt;; &lt;code&gt;order_id&lt;/code&gt;, &lt;code&gt;quantity&lt;/code&gt;, &lt;code&gt;unit_price&lt;/code&gt; on &lt;code&gt;stg_order_lines&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Copilot drafts the model with the correct &lt;code&gt;ref()&lt;/code&gt; calls (not raw table names), the &lt;code&gt;{{ config }}&lt;/code&gt; block matching the prompt's &lt;code&gt;unique_key = order_id&lt;/code&gt;, and the &lt;code&gt;is_incremental()&lt;/code&gt; guard around the &lt;code&gt;updated_at&lt;/code&gt; filter. Because it knows the columns, it aggregates &lt;code&gt;stg_order_lines&lt;/code&gt; correctly (&lt;code&gt;sum(quantity)&lt;/code&gt;, &lt;code&gt;sum(quantity * unit_price)&lt;/code&gt;) and joins on &lt;code&gt;order_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Copilot also drafts a &lt;code&gt;schema.yml&lt;/code&gt; entry with per-column descriptions and tests — &lt;code&gt;unique + not_null&lt;/code&gt; on &lt;code&gt;order_id&lt;/code&gt; (the primary key from the prompt), &lt;code&gt;not_null + relationships&lt;/code&gt; on &lt;code&gt;customer_id&lt;/code&gt; (inferred from column naming + upstream &lt;code&gt;dim_customers&lt;/code&gt; in the graph). This is the "generate docs while generating code" superpower that generic Copilots don't have.&lt;/li&gt;
&lt;li&gt;Copilot returns &lt;em&gt;drafts&lt;/em&gt; — the analytics engineer reviews, adjusts column casing, edits descriptions, and commits. There is no auto-commit. The productivity gain is "80% of the boilerplate, without the schema hallucination."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Copilot output&lt;/th&gt;
&lt;th&gt;Human review changes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model SQL&lt;/td&gt;
&lt;td&gt;correct refs, incremental config, join&lt;/td&gt;
&lt;td&gt;usually 0-2 line tweaks (formatting, order)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;schema.yml&lt;/td&gt;
&lt;td&gt;descriptions + PK/FK tests&lt;/td&gt;
&lt;td&gt;expand descriptions to add business context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt compile&lt;/td&gt;
&lt;td&gt;passes on first try (~90% of cases)&lt;/td&gt;
&lt;td&gt;fix column-case mismatches when they occur&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt test&lt;/td&gt;
&lt;td&gt;PK + not-null tests pass&lt;/td&gt;
&lt;td&gt;add domain-specific tests (status in list, revenue &amp;gt; 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any dbt Copilot workflow, keep the manifest fresh (compile on save), keep the catalog fresh (&lt;code&gt;dbt docs generate&lt;/code&gt; after schema changes), and treat Copilot output as a first draft — always review before commit. The productivity gain is boilerplate elimination, not blind trust.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — drafting semantic-layer metrics from a natural-language spec
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; dbt's semantic layer (semantic_models.yml) declares metrics, measures, and dimensions that downstream tools (Cube, MetricFlow, Cortex Analyst via YAML export) consume. Authoring these YAML blocks by hand is tedious; Copilot excels at drafting them from the underlying fact model. Walk through prompting Copilot to define a &lt;code&gt;total_revenue&lt;/code&gt; metric on &lt;code&gt;fct_orders&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The upstream fact.&lt;/strong&gt; &lt;code&gt;fct_orders&lt;/code&gt; with columns including &lt;code&gt;total_cents&lt;/code&gt;, &lt;code&gt;order_date&lt;/code&gt;, &lt;code&gt;region&lt;/code&gt;, &lt;code&gt;customer_segment&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The prompt.&lt;/strong&gt; "Define a semantic model on fct_orders with a total_revenue metric (sum of total_cents / 100), a order_count metric, dimensions region and customer_segment, and a time-dimension on order_date."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The output.&lt;/strong&gt; A &lt;code&gt;semantic_models.yml&lt;/code&gt; block with entities, dimensions, measures, and metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Show the Copilot output and the down-line consumption in Cortex Analyst / MetricFlow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fact model&lt;/td&gt;
&lt;td&gt;fct_orders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric 1&lt;/td&gt;
&lt;td&gt;total_revenue = sum(total_cents) / 100.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric 2&lt;/td&gt;
&lt;td&gt;order_count = count(order_id)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dimensions&lt;/td&gt;
&lt;td&gt;region, customer_segment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time dimension&lt;/td&gt;
&lt;td&gt;order_date&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# models/marts/semantic_models.yml (drafted by Copilot)&lt;/span&gt;
&lt;span class="na"&gt;semantic_models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sm_orders&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;Semantic model over fct_orders exposing revenue and order-count&lt;/span&gt;
      &lt;span class="s"&gt;metrics, sliced by region and customer_segment, timestamped on&lt;/span&gt;
      &lt;span class="s"&gt;order_date. Consumed by dbt semantic layer and exported for&lt;/span&gt;
      &lt;span class="s"&gt;Snowflake Cortex Analyst.&lt;/span&gt;

    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('fct_orders')&lt;/span&gt;

    &lt;span class="na"&gt;entities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;primary&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_id&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;foreign&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;

    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;categorical&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_segment&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;categorical&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_segment&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;time&lt;/span&gt;
        &lt;span class="na"&gt;type_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;time_granularity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;day&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;

    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_cents&lt;/span&gt;
        &lt;span class="na"&gt;agg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_cents&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_count&lt;/span&gt;
        &lt;span class="na"&gt;agg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_id&lt;/span&gt;

&lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_revenue&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gross revenue in USD (sum of total_cents divided by 100).&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;derived&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Total Revenue&lt;/span&gt;
    &lt;span class="na"&gt;type_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_cents / &lt;/span&gt;&lt;span class="m"&gt;100.0&lt;/span&gt;
      &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_cents&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_count&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Number of orders.&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;simple&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Orders&lt;/span&gt;
    &lt;span class="na"&gt;type_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;measure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Consuming the metric via the dbt semantic-layer JDBC endpoint
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dbt_metricflow&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MetricFlowClient&lt;/span&gt;   &lt;span class="c1"&gt;# illustrative
&lt;/span&gt;
&lt;span class="n"&gt;mf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MetricFlowClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_revenue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;group_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_date__month&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{ Dimension(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;order_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) }} &amp;gt;= &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2026-01-01&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_date__month&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_pandas&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="c1"&gt;#           order_date__month  region  total_revenue  order_count
# 0        2026-01-01           EAST      1_240_450         3_412
# 1        2026-01-01           WEST      1_082_710         2_998
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copilot reads the prompt and understands the target: a semantic model with entities, dimensions, measures, and metrics. It queries the project graph to find &lt;code&gt;fct_orders&lt;/code&gt; and enumerate its columns via catalog.json.&lt;/li&gt;
&lt;li&gt;It drafts the &lt;code&gt;entities&lt;/code&gt; block: &lt;code&gt;order&lt;/code&gt; as the primary entity (order_id is the grain), &lt;code&gt;customer&lt;/code&gt; as a foreign entity (linking to dim_customers). Getting these entity types right is the piece a generic Copilot cannot do — it requires knowledge of the referenced model's PK.&lt;/li&gt;
&lt;li&gt;It drafts the &lt;code&gt;dimensions&lt;/code&gt; block with the right types (&lt;code&gt;categorical&lt;/code&gt; for text, &lt;code&gt;time&lt;/code&gt; for order_date with &lt;code&gt;time_granularity: day&lt;/code&gt;) and &lt;code&gt;expr&lt;/code&gt; bindings pointing to the columns.&lt;/li&gt;
&lt;li&gt;It drafts the &lt;code&gt;measures&lt;/code&gt; block — the raw aggregations &lt;code&gt;sum(total_cents)&lt;/code&gt; and &lt;code&gt;count(order_id)&lt;/code&gt; — and then the &lt;code&gt;metrics&lt;/code&gt; block that composes measures into user-facing metric definitions with labels and descriptions. &lt;code&gt;total_revenue&lt;/code&gt; is a &lt;em&gt;derived&lt;/em&gt; metric that divides &lt;code&gt;total_cents&lt;/code&gt; by 100; &lt;code&gt;order_count&lt;/code&gt; is a &lt;em&gt;simple&lt;/em&gt; metric that wraps the measure directly.&lt;/li&gt;
&lt;li&gt;The consuming code (via MetricFlow JDBC) calls &lt;code&gt;total_revenue&lt;/code&gt; by name without needing to know the underlying arithmetic. This is the semantic-layer contract: downstream tools query metrics; the layer resolves them to SQL against the fact table.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fact model&lt;/td&gt;
&lt;td&gt;fct_orders&lt;/td&gt;
&lt;td&gt;dbt semantic model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic model&lt;/td&gt;
&lt;td&gt;sm_orders (dimensions + measures)&lt;/td&gt;
&lt;td&gt;metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric&lt;/td&gt;
&lt;td&gt;total_revenue (derived)&lt;/td&gt;
&lt;td&gt;MetricFlow / Cortex Analyst&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query&lt;/td&gt;
&lt;td&gt;mf.query(metrics=..., group_by=...)&lt;/td&gt;
&lt;td&gt;pandas DataFrame&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Author semantic-layer metrics with Copilot's help; review the entity types (primary / foreign / natural) carefully — they're the piece Copilot occasionally gets wrong when the fact-table PK is ambiguous. Once authored, downstream consumption is metric-by-name; the arithmetic lives in one place.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the manifest-freshness failure mode
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A team has dbt Copilot enabled. An analytics engineer pulls a colleague's branch that added a new source (&lt;code&gt;raw.stripe_payouts&lt;/code&gt;). Without recompiling, they ask Copilot to build a &lt;code&gt;stg_stripe_payouts&lt;/code&gt; model referencing the new source. Copilot has no knowledge of the new source in its manifest — it hallucinates a &lt;code&gt;ref()&lt;/code&gt; to a similarly-named model that doesn't exist. Walk through the diagnosis and the fix.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom.&lt;/strong&gt; Copilot's suggested &lt;code&gt;select * from {{ ref('stripe_payouts') }}&lt;/code&gt; fails with "Compilation Error — model 'stripe_payouts' not found".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause.&lt;/strong&gt; The pulled branch added &lt;code&gt;sources.yml&lt;/code&gt; for &lt;code&gt;raw.stripe_payouts&lt;/code&gt;, but the local manifest.json is from before the pull — Copilot doesn't know about the new source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; Run &lt;code&gt;dbt compile&lt;/code&gt;; Copilot re-reads the manifest and now knows the source. The correct completion is &lt;code&gt;select * from {{ source('raw', 'stripe_payouts') }}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Codify the "always compile after pulling" workflow into the team's CI + local hooks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Enforcement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local dev&lt;/td&gt;
&lt;td&gt;pre-commit hook: &lt;code&gt;dbt compile&lt;/code&gt; on staged .sql / .yml changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI&lt;/td&gt;
&lt;td&gt;GitHub Action: &lt;code&gt;dbt compile&lt;/code&gt; on every PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDE&lt;/td&gt;
&lt;td&gt;dbt Cloud IDE live-compile: enabled by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runbook&lt;/td&gt;
&lt;td&gt;"if Copilot hallucinates, run &lt;code&gt;dbt compile&lt;/code&gt; first"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .pre-commit-config.yaml — local enforcement&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-compile&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt compile&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash -c 'cd dbt_project &amp;amp;&amp;amp; dbt compile --quiet'&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^dbt_project/(models|macros|seeds|snapshots|analyses)/.*\.(sql|yml)$'&lt;/span&gt;
        &lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;commit&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/dbt-ci.yml — CI enforcement&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-ci&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install dbt-snowflake==1.9.0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd dbt_project &amp;amp;&amp;amp; dbt deps&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd dbt_project &amp;amp;&amp;amp; dbt compile --profiles-dir ci_profiles&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Fail if manifest is stale relative to source&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# If a model references a source that isn't in the manifest,&lt;/span&gt;
          &lt;span class="s"&gt;# `dbt compile` raises. This is our stale-manifest tripwire.&lt;/span&gt;
          &lt;span class="s"&gt;echo "compile OK; manifest is fresh."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Team runbook (one-liner in CONTRIBUTING.md)&lt;/span&gt;
&lt;span class="c"&gt;# "If dbt Copilot suggests a ref() that fails, run: dbt compile"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The root cause is that dbt Copilot's grounding artifact — manifest.json — was stale relative to the source of truth (the .yml files on disk). Copilot cannot re-read what hasn't been compiled; the failure surfaces as hallucinated model names or missing columns.&lt;/li&gt;
&lt;li&gt;The pre-commit hook forces &lt;code&gt;dbt compile&lt;/code&gt; on any change to &lt;code&gt;.sql&lt;/code&gt; / &lt;code&gt;.yml&lt;/code&gt; files under &lt;code&gt;models/&lt;/code&gt;, &lt;code&gt;macros/&lt;/code&gt;, &lt;code&gt;seeds/&lt;/code&gt;, &lt;code&gt;snapshots/&lt;/code&gt;, &lt;code&gt;analyses/&lt;/code&gt;. This ensures the manifest is fresh on every commit; Copilot never sees a stale graph in the middle of authoring.&lt;/li&gt;
&lt;li&gt;The CI enforcement is the belt-and-braces: even if a developer skips the pre-commit hook, PR-level compilation catches the drift before merge. This also validates that new sources are complete (columns declared, types set) — an incomplete source in the YAML makes the manifest technically fresh but functionally useless to Copilot.&lt;/li&gt;
&lt;li&gt;The dbt Cloud IDE's live-compile toggle (enabled by default) mostly obviates the local hook — the IDE recompiles on every save. The pre-commit hook still matters for developers who work outside the Cloud IDE (VS Code, Cursor).&lt;/li&gt;
&lt;li&gt;The runbook line — "if Copilot hallucinates, run &lt;code&gt;dbt compile&lt;/code&gt; first" — is the debug-first-step every team member should know. Nine times out of ten, hallucinated completions are stale-manifest completions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Manifest state&lt;/th&gt;
&lt;th&gt;Copilot behaviour&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fresh&lt;/td&gt;
&lt;td&gt;grounded completions&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stale (missed source)&lt;/td&gt;
&lt;td&gt;hallucinated &lt;code&gt;ref()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dbt compile&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stale (missed columns)&lt;/td&gt;
&lt;td&gt;wrong column names&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dbt docs generate&lt;/code&gt; (catalog)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing (never compiled)&lt;/td&gt;
&lt;td&gt;generic SQL completions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dbt compile&lt;/code&gt; first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Treat manifest freshness as the #1 dbt Copilot quality-of-life lever. A pre-commit hook + CI compile + Cloud IDE live-compile is the belt-and-braces setup that keeps completions grounded. When Copilot surprises you, run &lt;code&gt;dbt compile&lt;/code&gt; before you start debugging the prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on dbt Copilot
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Your team just onboarded dbt Copilot in dbt Cloud. After two weeks, analytics engineers report that half the completions reference columns that don't exist and half the semantic-model drafts miss the primary-key entity. Design the workflow, the CI checks, and the code-review rubric that fixes both problems and makes Copilot productive."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using freshness hooks + a Copilot code-review rubric + a semantic-model-first authoring pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. dbt project config — enforce full compile + catalog on every dev iteration&lt;/span&gt;
&lt;span class="c1"&gt;# dbt_project.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod_analytics&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.0.0'&lt;/span&gt;
&lt;span class="na"&gt;config-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;

&lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;

&lt;span class="na"&gt;require-dbt-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;=1.9.0'&lt;/span&gt;

&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;+on_schema_change&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fail&lt;/span&gt;    &lt;span class="c1"&gt;# catch schema drift on incremental models&lt;/span&gt;
  &lt;span class="na"&gt;+persist_docs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="kc"&gt;true&lt;/span&gt;            &lt;span class="c1"&gt;# persist YAML descriptions to the warehouse&lt;/span&gt;

&lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Turn on strict-mode compile warnings&lt;/span&gt;
  &lt;span class="na"&gt;strict&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# .pre-commit-config.yaml — enforce compile + docs generate&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-compile&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt compile&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash -c 'cd dbt_project &amp;amp;&amp;amp; dbt compile --quiet'&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^dbt_project/(models|macros|seeds|snapshots)/.*\.(sql|yml)$'&lt;/span&gt;
        &lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;commit&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt-docs&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt docs generate&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash -c 'cd dbt_project &amp;amp;&amp;amp; dbt docs generate --quiet --no-compile'&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^dbt_project/(models|macros|seeds|snapshots)/.*\.(sql|yml)$'&lt;/span&gt;
        &lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- 2. Copilot code-review rubric (CONTRIBUTING.md excerpt) --&amp;gt;&lt;/span&gt;
&lt;span class="gu"&gt;### Reviewing a PR that used dbt Copilot&lt;/span&gt;

Every PR authored with Copilot must be reviewed for the four grounding hazards:
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**Ref hallucination.**&lt;/span&gt; Grep the diff for &lt;span class="sb"&gt;`ref('...')`&lt;/span&gt;; verify each referenced
   model exists in &lt;span class="sb"&gt;`manifest.json`&lt;/span&gt; at HEAD. Common failure: Copilot references a
   model on a colleague's unmerged branch.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Column hallucination.**&lt;/span&gt; Grep for column names in the diff; verify each
   exists in the referenced model's YAML or in &lt;span class="sb"&gt;`catalog.json`&lt;/span&gt;. Common failure:
   Copilot invents plural-vs-singular column names (&lt;span class="sb"&gt;`orders`&lt;/span&gt; vs &lt;span class="sb"&gt;`order_id`&lt;/span&gt;).
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Test / doc mismatch.**&lt;/span&gt; If the PR added a &lt;span class="sb"&gt;`schema.yml`&lt;/span&gt; block, verify the
   &lt;span class="sb"&gt;`columns:`&lt;/span&gt; list matches the model's actual SELECT columns 1:1.
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Semantic-model entity types.**&lt;/span&gt; If the PR added or edited a
   &lt;span class="sb"&gt;`semantic_models.yml`&lt;/span&gt; block, verify: exactly one &lt;span class="sb"&gt;`type: primary`&lt;/span&gt; entity per
   model; the &lt;span class="sb"&gt;`expr:`&lt;/span&gt; on the primary entity matches the model's grain
   (unique_key).

Any of the four issues =&amp;gt; request changes; do not merge.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 3. Automated Copilot-drift checker (runs in CI)
# scripts/check_copilot_drift.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;MANIFEST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_project/target/manifest.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CATALOG&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_project/target/catalog.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;known_models&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;known_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MANIFEST&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;catalog&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CATALOG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;models&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;known_models&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sql_file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_project/models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sql_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ref\([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\"]([^&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\"]+)[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\"]\)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;ref_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ref_model&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;sql_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref_model&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; — &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no Copilot drift detected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Effect on Copilot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pre-commit &lt;code&gt;dbt compile&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;manifest fresh on every commit&lt;/td&gt;
&lt;td&gt;grounded refs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pre-push &lt;code&gt;dbt docs generate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;catalog fresh on every push&lt;/td&gt;
&lt;td&gt;grounded columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot review rubric&lt;/td&gt;
&lt;td&gt;4-check human review&lt;/td&gt;
&lt;td&gt;catches hallucinated refs + columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI drift checker&lt;/td&gt;
&lt;td&gt;script fails PR on unknown ref&lt;/td&gt;
&lt;td&gt;last-line defense&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt Cloud IDE live compile&lt;/td&gt;
&lt;td&gt;manifest refreshes on save&lt;/td&gt;
&lt;td&gt;inline grounded completions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;on_schema_change: fail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;incremental models refuse silent drift&lt;/td&gt;
&lt;td&gt;catches column deletions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persist_docs: columns: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;YAML descriptions land in warehouse&lt;/td&gt;
&lt;td&gt;Genie / Cortex reuse them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the rollout, Copilot completions reference real models 99%+ of the time, semantic-model entity types are correct on first draft 85%+ of the time (with the remaining 15% caught by the code-review rubric), and the CI drift checker acts as the belt-and-braces safety net for anyone who bypasses the local hooks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hallucinated &lt;code&gt;ref()&lt;/code&gt; rate&lt;/td&gt;
&lt;td&gt;~50% of PRs&lt;/td&gt;
&lt;td&gt;&amp;lt; 1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong-column rate&lt;/td&gt;
&lt;td&gt;~30% of PRs&lt;/td&gt;
&lt;td&gt;&amp;lt; 5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic-model entity errors&lt;/td&gt;
&lt;td&gt;~40%&lt;/td&gt;
&lt;td&gt;~15% (caught in review)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot productivity gain&lt;/td&gt;
&lt;td&gt;net-negative (review cost)&lt;/td&gt;
&lt;td&gt;+30% author velocity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downstream tool grounding (Genie/Cortex)&lt;/td&gt;
&lt;td&gt;broken&lt;/td&gt;
&lt;td&gt;consistent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Manifest freshness&lt;/strong&gt;&lt;/strong&gt; — Copilot's grounding source is the compiled artifact, not the on-disk YAML. A pre-commit &lt;code&gt;dbt compile&lt;/code&gt; guarantees the manifest is fresh; a stale manifest is the root cause of most Copilot hallucinations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Catalog freshness&lt;/strong&gt;&lt;/strong&gt; — column-level grounding lives in &lt;code&gt;catalog.json&lt;/code&gt;, which requires &lt;code&gt;dbt docs generate&lt;/code&gt; (an extra step). Column hallucinations drop dramatically when catalog is regenerated on every schema change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Copilot review rubric&lt;/strong&gt;&lt;/strong&gt; — the four checks (ref, column, doc, entity) codify the "trust but verify" contract. A human always reviews Copilot output; the rubric ensures the review is systematic, not ad hoc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;CI drift checker&lt;/strong&gt;&lt;/strong&gt; — the automated last-line defense. Even if pre-commit hooks are skipped and review misses a hallucinated ref, the CI script fails the PR before merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — pre-commit &lt;code&gt;dbt compile&lt;/code&gt; adds ~5-30 seconds per commit; CI drift check adds ~2 seconds; total dev-loop overhead ~&amp;lt;1 minute per PR. Compared to shipping hallucinated models to production, this cost is negligible. Net O(project size) per compile, O(files changed) per drift check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;ETL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — etl&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;ETL and dbt-modelling problems for analytics engineers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/etl" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;SQL Generation&lt;/span&gt;
&lt;span&gt;Topic — sql-generation&lt;/span&gt;
&lt;strong&gt;SQL-generation problems with grounded prompts&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Snowflake Cortex Analyst — governed text-to-SQL over your semantic model
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;snowflake cortex analyst&lt;/code&gt; is a REST-API text-to-SQL layer grounded on a YAML semantic model — governance runs through the caller's role
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;snowflake cortex analyst&lt;/code&gt; is a REST API on Snowflake Cortex that accepts a natural-language question plus a reference to a YAML &lt;code&gt;semantic layer&lt;/code&gt; file, returns generated SQL, an interpretation string, and optionally a chart spec — the LLM is bounded by the tables, measures, dimensions, and synonyms declared in the YAML, and every generated SQL is executed by Snowflake under the &lt;em&gt;caller's&lt;/em&gt; role, so row-access policies and column masks bind automatically without any extra plumbing&lt;/strong&gt;. This is qualitatively different from any generic text-to-SQL wrapper: the grounding is declarative and the governance is inherited.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnyty2e2zg6qwsuduhvfa.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnyty2e2zg6qwsuduhvfa.jpeg" alt="Iconographic Snowflake Cortex Analyst diagram — a YAML semantic model card feeding a Cortex sparkle in the middle, with a REST call arrow from a user, returning generated SQL and an interpretation card; a warning chip 'schema drift risk'." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for Cortex Analyst.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grounding.&lt;/strong&gt; A YAML file — the &lt;code&gt;semantic_model.yaml&lt;/code&gt; — declares &lt;code&gt;tables&lt;/code&gt;, &lt;code&gt;dimensions&lt;/code&gt;, &lt;code&gt;measures&lt;/code&gt;, &lt;code&gt;time_dimensions&lt;/code&gt;, &lt;code&gt;filters&lt;/code&gt;, &lt;code&gt;entities&lt;/code&gt;, &lt;code&gt;relationships&lt;/code&gt;, and &lt;code&gt;verified_queries&lt;/code&gt; (few-shot exemplars). The LLM cannot generate a measure that isn't declared; it cannot join tables without a declared relationship; it cannot reference columns that aren't in the YAML. This bounds the hypothesis space to what the semantic-layer author has approved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persona.&lt;/strong&gt; Analysts and internal-app developers. Cortex Analyst is a REST API, not a UI — you consume it from a Streamlit app, a Slack bot, an internal analyst tool, or Snowsight's Cortex Analyst panel. Not aimed at business users directly (they get the wrapping app's UI, not the raw REST).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance.&lt;/strong&gt; The generated SQL is &lt;em&gt;not&lt;/em&gt; executed by Cortex — the caller executes it under their own Snowflake role. Every row-access policy, column mask, warehouse grant, and network policy applies exactly as if the analyst had written the SQL by hand. Cortex sees the schema (via YAML) but never the row values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency + review.&lt;/strong&gt; 2-5 seconds per turn (REST call). Every response ships the generated SQL for programmatic review before execution — you can auto-execute, or you can apply a rules-based safety check first (e.g., disallow &lt;code&gt;DELETE&lt;/code&gt;, cap &lt;code&gt;LIMIT&lt;/code&gt;, require &lt;code&gt;WHERE&lt;/code&gt; on partitioned columns).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The YAML semantic model — the grounding contract in detail.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tables.&lt;/strong&gt; Each &lt;code&gt;table&lt;/code&gt; block declares a base table (database.schema.name), a description, a primary key, and lists of dimensions, measures, and time-dimensions. Descriptions are prompt context; they matter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dimensions.&lt;/strong&gt; Categorical columns the LLM can &lt;code&gt;GROUP BY&lt;/code&gt; or &lt;code&gt;WHERE&lt;/code&gt; on. Each dimension declares an &lt;code&gt;expr&lt;/code&gt; (usually just the column name, but can be a SQL expression like &lt;code&gt;INITCAP(region)&lt;/code&gt;) and a list of &lt;code&gt;synonyms&lt;/code&gt; for user-facing language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measures.&lt;/strong&gt; Numeric aggregations. &lt;code&gt;expr: SUM(total_cents) / 100.0&lt;/code&gt; becomes a callable measure named &lt;code&gt;revenue&lt;/code&gt;. Every measure has a description and synonyms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-dimensions.&lt;/strong&gt; Special case of dimensions — used for period filters ("last quarter", "year to date") and time-grain groupings. Declare them explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verified queries.&lt;/strong&gt; A section listing &lt;code&gt;(question, sql)&lt;/code&gt; pairs that act as few-shot exemplars for the LLM. If a user question closely matches a verified question, the LLM is much more likely to produce the exact expected SQL. Curate 10-50 per semantic model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships.&lt;/strong&gt; For multi-table models, declare foreign-key relationships so the LLM can join safely. Without a declared relationship, Cortex Analyst refuses to join.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The REST API — request + response shape.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Endpoint.&lt;/strong&gt; &lt;code&gt;POST https://&amp;lt;account&amp;gt;.snowflakecomputing.com/api/v2/cortex/analyst/message&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request.&lt;/strong&gt; &lt;code&gt;{ "messages": [{ "role": "user", "content": [{ "type": "text", "text": "revenue by region last quarter" }] }], "semantic_model_file": "@DB.SCHEMA.STAGE/revenue_model.yaml" }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response.&lt;/strong&gt; JSON with &lt;code&gt;message.content&lt;/code&gt; array containing objects of &lt;code&gt;type&lt;/code&gt; &lt;code&gt;text&lt;/code&gt; (interpretation), &lt;code&gt;sql&lt;/code&gt; (generated SQL statement + &lt;code&gt;confidence&lt;/code&gt;), and optionally &lt;code&gt;suggestion&lt;/code&gt; (follow-up question suggestions).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth.&lt;/strong&gt; Snowflake PAT (Personal Access Token) or OAuth JWT. The token carries the user's role; the SQL is executed under that role when the caller runs it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on Cortex Analyst.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How is Cortex Analyst different from wrapping GPT-4o with a system prompt?" — required answer: grounded on declarative YAML semantic model; measures and joins are bounded; runs under caller's role.&lt;/li&gt;
&lt;li&gt;"How do you keep the YAML in sync with the warehouse?" — required answer: generate from dbt semantic models (or a &lt;code&gt;dbt-cortex&lt;/code&gt; exporter); CI-verify column existence via &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"How do you evaluate accuracy?" — required answer: golden-question harness with semantic-diff scoring.&lt;/li&gt;
&lt;li&gt;"What about PII?" — required answer: caller-role execution + column masks + never expose row values in the YAML.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — authoring &lt;code&gt;revenue_model.yaml&lt;/code&gt; and calling the REST API
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Cortex Analyst deployment: (1) author a &lt;code&gt;revenue_model.yaml&lt;/code&gt; declaring tables + measures + dimensions + synonyms + verified queries, (2) upload it to a Snowflake stage, (3) POST natural-language questions to the REST endpoint, (4) execute the returned SQL under the caller's role, (5) render the result. Walk through every step.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model scope.&lt;/strong&gt; &lt;code&gt;fct_orders&lt;/code&gt; + &lt;code&gt;dim_customers&lt;/code&gt; joined by &lt;code&gt;customer_id&lt;/code&gt;, with measures &lt;code&gt;revenue&lt;/code&gt; and &lt;code&gt;unique_customers&lt;/code&gt; and dimensions &lt;code&gt;region&lt;/code&gt;, &lt;code&gt;customer_segment&lt;/code&gt;, and a time-dimension on &lt;code&gt;order_date&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer.&lt;/strong&gt; A Streamlit app for the finance team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth.&lt;/strong&gt; Snowflake PAT stored in Streamlit secrets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Author the YAML, show the REST call, and render the result in Streamlit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fact&lt;/td&gt;
&lt;td&gt;PROD_ANALYTICS.MART.FCT_ORDERS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dim&lt;/td&gt;
&lt;td&gt;PROD_ANALYTICS.MART.DIM_CUSTOMERS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Measures&lt;/td&gt;
&lt;td&gt;revenue, unique_customers, order_count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dimensions&lt;/td&gt;
&lt;td&gt;region, customer_segment, industry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-dim&lt;/td&gt;
&lt;td&gt;order_date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verified queries&lt;/td&gt;
&lt;td&gt;3 (revenue by region, unique customers by segment, monthly trend)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# semantic_models/revenue_model.yaml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue_model&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Governed revenue semantic model for the finance + sales-ops teams.&lt;/span&gt;
  &lt;span class="s"&gt;Grounded on fct_orders joined to dim_customers by customer_id.&lt;/span&gt;

&lt;span class="na"&gt;tables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fct_orders&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;One row per order; grain is order_id.&lt;/span&gt;
    &lt;span class="na"&gt;base_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PROD_ANALYTICS&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;MART&lt;/span&gt;
      &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;FCT_ORDERS&lt;/span&gt;
    &lt;span class="na"&gt;primary_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;order_id&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TEXT&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;geo&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;territory&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sales region&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_segment&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_segment&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TEXT&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;segment&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;customer tier&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;time_dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_date&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DATE&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;day&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;order day&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SUM(total_cents) / &lt;/span&gt;&lt;span class="m"&gt;100.0&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NUMBER&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gross revenue in USD.&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;sales&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;gross revenue&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;total sales&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;top line&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_count&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COUNT(*)&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NUMBER&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Number of orders.&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;orders&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;transactions&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unique_customers&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COUNT(DISTINCT customer_id)&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NUMBER&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Distinct customers with at least one order.&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;customers&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;buyers&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;unique buyers&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dim_customers&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;One row per customer.&lt;/span&gt;
    &lt;span class="na"&gt;base_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PROD_ANALYTICS&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;MART&lt;/span&gt;
      &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;DIM_CUSTOMERS&lt;/span&gt;
    &lt;span class="na"&gt;primary_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;industry&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;industry&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TEXT&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;vertical&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sector&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;relationships&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;left_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;fct_orders&lt;/span&gt;
    &lt;span class="na"&gt;left_column&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
    &lt;span class="na"&gt;right_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dim_customers&lt;/span&gt;
    &lt;span class="na"&gt;right_column&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
    &lt;span class="na"&gt;join_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;left_outer&lt;/span&gt;
    &lt;span class="na"&gt;relationship_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;many_to_one&lt;/span&gt;

&lt;span class="na"&gt;verified_queries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue_by_region_last_quarter&lt;/span&gt;
    &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;What was our revenue by region last quarter?&lt;/span&gt;
    &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;SELECT region, SUM(total_cents)/100.0 AS revenue&lt;/span&gt;
      &lt;span class="s"&gt;FROM   PROD_ANALYTICS.MART.FCT_ORDERS&lt;/span&gt;
      &lt;span class="s"&gt;WHERE  order_date BETWEEN DATE_TRUNC('quarter', DATEADD('quarter', -1, CURRENT_DATE))&lt;/span&gt;
                            &lt;span class="s"&gt;AND LAST_DAY(DATEADD('quarter', -1, CURRENT_DATE))&lt;/span&gt;
      &lt;span class="s"&gt;GROUP  BY region&lt;/span&gt;
      &lt;span class="s"&gt;ORDER  BY revenue DESC&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unique_customers_by_industry&lt;/span&gt;
    &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;How many unique customers do we have by industry?&lt;/span&gt;
    &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;SELECT c.industry, COUNT(DISTINCT o.customer_id) AS unique_customers&lt;/span&gt;
      &lt;span class="s"&gt;FROM   PROD_ANALYTICS.MART.FCT_ORDERS o&lt;/span&gt;
      &lt;span class="s"&gt;LEFT   JOIN PROD_ANALYTICS.MART.DIM_CUSTOMERS c&lt;/span&gt;
             &lt;span class="s"&gt;ON c.customer_id = o.customer_id&lt;/span&gt;
      &lt;span class="s"&gt;GROUP  BY c.industry&lt;/span&gt;
      &lt;span class="s"&gt;ORDER  BY unique_customers DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Upload the YAML to a stage&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;STAGE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;PROD_ANALYTICS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SEMANTIC_MODELS&lt;/span&gt;
  &lt;span class="n"&gt;FILE_FORMAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'CSV'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;-- format ignored; stage is for raw files&lt;/span&gt;

&lt;span class="c1"&gt;-- Then via SnowSQL or the web UI:&lt;/span&gt;
&lt;span class="n"&gt;PUT&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;semantic_models&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;revenue_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;PROD_ANALYTICS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SEMANTIC_MODELS&lt;/span&gt;
  &lt;span class="n"&gt;AUTO_COMPRESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;FALSE&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# streamlit_app.py — analyst-facing UI
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt;

&lt;span class="n"&gt;ACCOUNT&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abcxyz-us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;CORTEX_URL&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACCOUNT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.snowflakecomputing.com/api/v2/cortex/analyst/message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;SEMANTIC&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@PROD_ANALYTICS.SEMANTIC_MODELS/revenue_model.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask_cortex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CORTEX_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic_model_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SEMANTIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run this query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;fetch_pandas_all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataframe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;suggestion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Follow-up: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;suggestions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revenue Analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ask a revenue question:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake_pat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ask_cortex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snowflake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ACCOUNT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;authenticator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oauth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake_role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;warehouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANALYST_WH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The YAML file is the grounding contract. &lt;code&gt;tables&lt;/code&gt; block declares the two fact/dim tables with their primary keys; &lt;code&gt;relationships&lt;/code&gt; block declares the FK from &lt;code&gt;fct_orders.customer_id&lt;/code&gt; → &lt;code&gt;dim_customers.customer_id&lt;/code&gt; so Cortex can safely generate joins. Without the relationship block, joins are refused.&lt;/li&gt;
&lt;li&gt;Every measure lists synonyms — &lt;code&gt;revenue&lt;/code&gt; has &lt;code&gt;[sales, gross revenue, total sales, top line]&lt;/code&gt;. When a user asks "what was our top line last quarter?", the LLM maps "top line" → &lt;code&gt;revenue&lt;/code&gt; via the synonym list, then generates SQL against the declared measure expression.&lt;/li&gt;
&lt;li&gt;Verified queries are few-shot exemplars. They serve two purposes: (a) they anchor the LLM's output for questions matching the verified pattern, dramatically increasing exact-match rate; (b) they document the "canonical" way to answer common questions, so a human reviewer can spot-check unusual generations.&lt;/li&gt;
&lt;li&gt;The Streamlit app POSTs the user's question to the Cortex Analyst REST endpoint with &lt;code&gt;semantic_model_file&lt;/code&gt; pointing to the stage-hosted YAML. The response contains a &lt;code&gt;message.content&lt;/code&gt; array with a &lt;code&gt;text&lt;/code&gt; interpretation block ("Here's revenue by region for last quarter…"), a &lt;code&gt;sql&lt;/code&gt; block with the generated statement, and optional &lt;code&gt;suggestion&lt;/code&gt; blocks with follow-up prompts.&lt;/li&gt;
&lt;li&gt;Critically, the SQL is &lt;em&gt;not&lt;/em&gt; executed by Cortex — the app shows it, waits for the analyst to click "Run this query", then executes via the Snowflake connector under the analyst's own role. RLS, column masks, warehouse grants — all bind automatically. Cortex never sees a single row of data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Generated SQL&lt;/th&gt;
&lt;th&gt;Executed under&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"revenue by region last quarter"&lt;/td&gt;
&lt;td&gt;SELECT region, SUM(total_cents)/100 ... GROUP BY region&lt;/td&gt;
&lt;td&gt;analyst's role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"unique customers by industry"&lt;/td&gt;
&lt;td&gt;JOIN dim_customers + GROUP BY industry&lt;/td&gt;
&lt;td&gt;analyst's role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"top line by segment this year"&lt;/td&gt;
&lt;td&gt;recognises "top line" = revenue; SELECT customer_segment, ...&lt;/td&gt;
&lt;td&gt;analyst's role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"delete old orders"&lt;/td&gt;
&lt;td&gt;REFUSED — DDL/DML outside the declared measures&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any Cortex Analyst deployment, invest time up front in synonyms (they're the difference between "grounded" and "user-frustrated") and verified queries (they're the difference between "10% exact-match" and "80% exact-match"). Store the YAML in a Snowflake stage under version control; treat schema changes as YAML PRs.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the schema-drift failure mode
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A team ships &lt;code&gt;revenue_model.yaml&lt;/code&gt; referencing &lt;code&gt;fct_orders.region&lt;/code&gt;. Two months later, an analytics engineer renames &lt;code&gt;region&lt;/code&gt; to &lt;code&gt;sales_region&lt;/code&gt; in dbt and re-materialises &lt;code&gt;fct_orders&lt;/code&gt;. The YAML is out of date. Cortex Analyst still generates SQL referencing &lt;code&gt;region&lt;/code&gt;; the SQL executes and returns "SQL compilation error: invalid identifier 'REGION'". Every analyst using the app sees the error. Walk through the diagnosis and the fix — plus the CI check that would have prevented it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom.&lt;/strong&gt; All Cortex-generated SQL fails with "invalid identifier 'REGION'".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause.&lt;/strong&gt; The warehouse column was renamed but the YAML wasn't updated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; Update the YAML; re-upload; verify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevention.&lt;/strong&gt; CI check that queries &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt; and verifies every YAML column exists in the referenced table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Ship the CI check that catches schema drift before it reaches production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;INFORMATION_SCHEMA query&lt;/td&gt;
&lt;td&gt;source of truth for real columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML parser&lt;/td&gt;
&lt;td&gt;extract declared columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diff&lt;/td&gt;
&lt;td&gt;flag columns in YAML but not warehouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI gate&lt;/td&gt;
&lt;td&gt;fail PR on drift&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/check_cortex_yaml_drift.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;YAML_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic_models/revenue_model.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_yaml_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return {fully_qualified_table: {declared_columns}}.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="n"&gt;bt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;fq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;measures&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time_dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
                &lt;span class="c1"&gt;# measures may have SQL expressions; extract identifiers only for leaf columns
&lt;/span&gt;                &lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isidentifier&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                    &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fq&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_warehouse_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT column_name
            FROM   &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.INFORMATION_SCHEMA.COLUMNS
            WHERE  table_schema = %s
              AND  table_name   = %s
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snowflake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abcxyz-us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ci_reader&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;authenticator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;externalbrowser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# or OAuth token
&lt;/span&gt;        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANALYTICS_READER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CI_WH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;yaml_cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_yaml_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;YAML_FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;declared&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;yaml_cols&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_warehouse_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;declared&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;fq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCHEMA DRIFT: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fq&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; declares columns not in warehouse: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no schema drift; revenue_model.yaml is consistent with warehouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/cortex-yaml-drift.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cortex-yaml-drift&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;semantic_models/**/*.yaml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dbt_project/models/**/*.sql'&lt;/span&gt;    &lt;span class="c1"&gt;# trigger on model changes too&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;drift-check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install snowflake-connector-python[pandas] pyyaml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure Snowflake OAuth&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "SNOWFLAKE_TOKEN=${{ secrets.SNOWFLAKE_CI_TOKEN }}" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/check_cortex_yaml_drift.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The root cause is that the YAML semantic model and the warehouse schema drifted independently. dbt owns the warehouse schema; the YAML owns the LLM grounding contract. Without a link, either can change without the other noticing.&lt;/li&gt;
&lt;li&gt;The drift-check script parses the YAML, extracts every declared column (from &lt;code&gt;dimensions&lt;/code&gt;, &lt;code&gt;measures&lt;/code&gt;, &lt;code&gt;time_dimensions&lt;/code&gt;), then queries Snowflake's &lt;code&gt;INFORMATION_SCHEMA.COLUMNS&lt;/code&gt; for the referenced tables. Any column in the YAML but not in the warehouse = drift.&lt;/li&gt;
&lt;li&gt;Note the &lt;code&gt;expr.isidentifier()&lt;/code&gt; guard: measure &lt;code&gt;expr&lt;/code&gt; values can be SQL expressions (e.g., &lt;code&gt;SUM(total_cents) / 100.0&lt;/code&gt;); the drift check only validates simple column identifiers. A stricter check would parse the expressions with a SQL parser (&lt;code&gt;sqlglot&lt;/code&gt;) to extract every referenced column, but simple-identifier coverage catches 80% of drift cases.&lt;/li&gt;
&lt;li&gt;The CI job runs on any PR that touches &lt;code&gt;semantic_models/*.yaml&lt;/code&gt; OR &lt;code&gt;dbt_project/models/*.sql&lt;/code&gt;. This means a dbt model rename triggers the same drift check as a YAML edit, so drift is caught in whichever direction it originates.&lt;/li&gt;
&lt;li&gt;On drift, the script exits 1 with a clear error message. The developer must either update the YAML or revert the dbt change; either way, the drift is blocked before merge. Over time, teams standardise on "dbt is the source of truth; YAML follows" or "YAML is the source of truth; dbt columns must not be renamed without a YAML PR." Either policy works; the drift check enforces whichever one the team picks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Warehouse&lt;/th&gt;
&lt;th&gt;YAML&lt;/th&gt;
&lt;th&gt;Drift check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add &lt;code&gt;industry&lt;/code&gt; column to dim_customers&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rename &lt;code&gt;region&lt;/code&gt; → &lt;code&gt;sales_region&lt;/code&gt; in dbt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sales_region&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;region&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drop unused &lt;code&gt;notes&lt;/code&gt; column in dbt&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edit YAML to reference a typo column&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;typo&lt;/td&gt;
&lt;td&gt;fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correct fix (dbt + YAML in one PR)&lt;/td&gt;
&lt;td&gt;consistent&lt;/td&gt;
&lt;td&gt;consistent&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; For any Cortex Analyst deployment, ship the schema-drift check on day one. It's a 50-line Python script and one GitHub Action; the cost is trivial and the failure mode it prevents (silent hallucinated SQL that fails on execution) is a production outage.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — synonym curation for user-friendly text-to-SQL
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A finance team's users ask questions like "what's our MRR by geo?" and "show me churned logos this quarter." The YAML declares &lt;code&gt;revenue&lt;/code&gt; (a measure) and &lt;code&gt;region&lt;/code&gt; (a dimension), but nothing about "MRR", "geo", "churned logos", or "logos." Cortex Analyst either fails to answer or hallucinates a &lt;code&gt;SELECT&lt;/code&gt; against a table that doesn't exist. The fix is aggressive synonym curation: enumerate every business-user phrase and map it to the canonical measure or dimension. Walk through the curation workflow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom.&lt;/strong&gt; ~30% of business-user questions get "I don't know how to answer that" or hallucinated SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause.&lt;/strong&gt; User vocabulary ("MRR", "geo", "logos") doesn't match declared dimension/measure names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; Expand &lt;code&gt;synonyms&lt;/code&gt; lists on every dimension and measure; add verified queries covering common business phrases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Extend &lt;code&gt;revenue_model.yaml&lt;/code&gt; to cover the finance team's real vocabulary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Business phrase&lt;/th&gt;
&lt;th&gt;Canonical measure / dimension&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"MRR"&lt;/td&gt;
&lt;td&gt;derived measure: &lt;code&gt;SUM(monthly_recurring_revenue_cents) / 100&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"geo"&lt;/td&gt;
&lt;td&gt;dimension: &lt;code&gt;region&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"logos"&lt;/td&gt;
&lt;td&gt;dimension: &lt;code&gt;customer_id&lt;/code&gt; (as COUNT DISTINCT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"churned logos"&lt;/td&gt;
&lt;td&gt;measure: unique customers with status='churned'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"net-new logos"&lt;/td&gt;
&lt;td&gt;measure: unique customers with first order this period&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# revenue_model.yaml — extended synonyms + new measures&lt;/span&gt;
&lt;span class="na"&gt;tables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fct_orders&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TEXT&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;geo&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;geography&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;territory&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sales region&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;market&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;area&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;country&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
        &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NUMBER&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;logo&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;logos&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;account&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;accounts&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;customer&lt;/span&gt;

    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SUM(total_cents) / &lt;/span&gt;&lt;span class="m"&gt;100.0&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gross revenue in USD.&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sales&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gross revenue&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;total sales&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;top line&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bookings&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GMV&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mrr&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SUM(monthly_recurring_revenue_cents) / &lt;/span&gt;&lt;span class="m"&gt;100.0&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Monthly recurring revenue in USD.&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MRR&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;monthly recurring revenue&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;recurring revenue&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;subscription revenue&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unique_logos&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COUNT(DISTINCT customer_id)&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Distinct customers (unique logos) with at least one order.&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique logos&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique customers&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;number of logos&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;customer count&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;accounts&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;churned_logos&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COUNT(DISTINCT CASE WHEN status = 'churned' THEN customer_id END)&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Distinct customers with churned status in the period.&lt;/span&gt;
        &lt;span class="na"&gt;synonyms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;churn&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;churned&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;churned logos&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;lost customers&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;attrition&lt;/span&gt;

&lt;span class="na"&gt;verified_queries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mrr_by_geo_this_quarter&lt;/span&gt;
    &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;our&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MRR&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;geo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;quarter?"&lt;/span&gt;
    &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;SELECT region AS geo,&lt;/span&gt;
             &lt;span class="s"&gt;SUM(monthly_recurring_revenue_cents)/100.0 AS mrr&lt;/span&gt;
      &lt;span class="s"&gt;FROM   PROD_ANALYTICS.MART.FCT_ORDERS&lt;/span&gt;
      &lt;span class="s"&gt;WHERE  order_date &amp;gt;= DATE_TRUNC('quarter', CURRENT_DATE)&lt;/span&gt;
        &lt;span class="s"&gt;AND  order_date &amp;lt;  DATE_TRUNC('quarter', DATEADD('quarter', 1, CURRENT_DATE))&lt;/span&gt;
      &lt;span class="s"&gt;GROUP  BY region&lt;/span&gt;
      &lt;span class="s"&gt;ORDER  BY mrr DESC;&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;churned_logos_this_quarter&lt;/span&gt;
    &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;many&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;logos&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;churned&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;quarter?"&lt;/span&gt;
    &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;SELECT COUNT(DISTINCT customer_id) AS churned_logos&lt;/span&gt;
      &lt;span class="s"&gt;FROM   PROD_ANALYTICS.MART.FCT_ORDERS&lt;/span&gt;
      &lt;span class="s"&gt;WHERE  status = 'churned'&lt;/span&gt;
        &lt;span class="s"&gt;AND  order_date &amp;gt;= DATE_TRUNC('quarter', CURRENT_DATE);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every dimension and measure gets an expanded &lt;code&gt;synonyms&lt;/code&gt; list covering the business team's real vocabulary. "MRR", "GMV", "logos", "geo" are business-user language; the YAML translates them to canonical measure / dimension names. A missing synonym = a failed question; over-curating synonyms is cheap.&lt;/li&gt;
&lt;li&gt;Where the business team has a phrase that doesn't map to an existing measure ("MRR"), add a &lt;em&gt;new&lt;/em&gt; measure. Cortex Analyst can then answer MRR questions natively without any prompt gymnastics. Note the measure expression &lt;code&gt;SUM(monthly_recurring_revenue_cents) / 100.0&lt;/code&gt; — it assumes the underlying column exists on &lt;code&gt;fct_orders&lt;/code&gt;; a drift check would catch a missing column.&lt;/li&gt;
&lt;li&gt;Complex business concepts ("churned logos") become measures with &lt;code&gt;CASE WHEN&lt;/code&gt; guards inside the aggregation. This encapsulates the business rule ("churned = status = 'churned'") in one place; users don't have to remember it and can't get it wrong.&lt;/li&gt;
&lt;li&gt;Verified queries covering the top business phrases anchor the LLM's output. Every quarter, look at the last 200 questions users asked; the top 10-20 recurring phrases become verified queries. This creates a positive feedback loop — the more the tool is used, the better it gets at the common questions.&lt;/li&gt;
&lt;li&gt;Synonym curation is &lt;em&gt;never done&lt;/em&gt;. Every quarter, review Cortex Analyst's answer logs for questions that returned low-confidence SQL or errored. The vocabulary gaps become the next synonym-list additions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User phrase&lt;/th&gt;
&lt;th&gt;Before synonym&lt;/th&gt;
&lt;th&gt;After synonym&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"MRR by geo"&lt;/td&gt;
&lt;td&gt;"I can't compute MRR"&lt;/td&gt;
&lt;td&gt;correct SQL with &lt;code&gt;region AS geo&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"unique logos"&lt;/td&gt;
&lt;td&gt;wrong (used &lt;code&gt;count(*)&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COUNT(DISTINCT customer_id)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"churn this quarter"&lt;/td&gt;
&lt;td&gt;hallucinated table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUM(... status='churned' ...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"bookings by market"&lt;/td&gt;
&lt;td&gt;brittle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;revenue by region&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"top line"&lt;/td&gt;
&lt;td&gt;rare hit&lt;/td&gt;
&lt;td&gt;90%+ correct&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Synonym curation is a continuous investment, not a one-time task. Budget 30 minutes a week per semantic model for the first quarter; drop to 30 minutes a month once the vocabulary stabilises. The payoff is user trust: every "I don't know how to answer that" erodes it; every correct answer builds it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on Cortex Analyst
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design a production Cortex Analyst deployment for a finance team of 30 people asking natural-language revenue questions against a Snowflake warehouse. Include the YAML semantic model, the REST-API integration, governance (RLS + column masks), the drift check, the synonym curation loop, and the golden-question eval harness. Then show me the metric you'd track to know it's working."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a governed YAML semantic model + RLS-inherited execution + drift + golden-question harness + a user-facing "confidence surface"
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Snowflake governance — row-access policy + column mask on the fact&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ACCESS&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;finance_region_rap&lt;/span&gt;
  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="c1"&gt;-- Finance-team members see all regions; sales-region managers see only their own&lt;/span&gt;
      &lt;span class="n"&gt;IS_ROLE_IN_SESSION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FINANCE_TEAM'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_ROLE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;-- role names match region codes for sales managers&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;PROD_ANALYTICS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MART&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FCT_ORDERS&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ACCESS&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;finance_region_rap&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;MASKING&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;customer_email_mask&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;IS_ROLE_IN_SESSION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FINANCE_TEAM'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'SUPPORT_TEAM'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(.).*(@.*)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;1***&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;2'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;PROD_ANALYTICS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MART&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DIM_CUSTOMERS&lt;/span&gt;
  &lt;span class="k"&gt;MODIFY&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;MASKING&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;customer_email_mask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. Analyst app — Cortex Analyst REST + caller-role execute + confidence surface
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt;

&lt;span class="n"&gt;ACCOUNT&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abcxyz-us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;CORTEX_URL&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACCOUNT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.snowflakecomputing.com/api/v2/cortex/analyst/message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;SEMANTIC&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@PROD_ANALYTICS.SEMANTIC_MODELS/revenue_model.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;SAFETY_DENYLIST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(DROP|DELETE|TRUNCATE|ALTER|GRANT|REVOKE|CREATE)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UnsafeSQLError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask_cortex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CORTEX_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic_model_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SEMANTIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no SQL block in response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;safe_execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pd.DataFrame&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;SAFETY_DENYLIST&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;UnsafeSQLError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generated SQL contains DDL/DML; refusing to execute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;UnsafeSQLError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence too low (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;); require human review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetch_pandas_all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Cortex] confidence=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;safe_execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;UnsafeSQLError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NOT EXECUTED: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 3. Golden-question harness (from section 1, extended)
# runs in CI on every YAML change; reports exact-match + semantic-match + confidence
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 4. Ops metric — confidence distribution + refusal rate over time
# Log every response: (question, confidence, executed?, error?, user_flag?)
# Dashboards:
#   - Avg confidence by week (target &amp;gt; 0.75)
#   - Refusal rate (target &amp;lt; 5%)
#   - User "wrong answer" flag rate (target &amp;lt; 3%)
#   - Golden-question semantic-match rate (target &amp;gt; 95%)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse&lt;/td&gt;
&lt;td&gt;RLS + column masks on fct_orders + dim_customers&lt;/td&gt;
&lt;td&gt;governance boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@PROD_ANALYTICS.SEMANTIC_MODELS/revenue_model.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;grounding contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyst app&lt;/td&gt;
&lt;td&gt;Cortex REST + PAT + caller role&lt;/td&gt;
&lt;td&gt;ask + interpret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safety layer&lt;/td&gt;
&lt;td&gt;DDL/DML denylist + confidence gate&lt;/td&gt;
&lt;td&gt;pre-execute guard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eval harness&lt;/td&gt;
&lt;td&gt;golden questions + semantic-diff&lt;/td&gt;
&lt;td&gt;CI regression gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ops dashboard&lt;/td&gt;
&lt;td&gt;confidence + refusal + user-flag rates&lt;/td&gt;
&lt;td&gt;production monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync check&lt;/td&gt;
&lt;td&gt;schema-drift script&lt;/td&gt;
&lt;td&gt;prevent silent breakage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the rollout, finance-team members ask questions in English and see the generated SQL alongside the chart; the caller-role execution inherits RLS (so a west-region sales manager only sees west-region rows) and column masks (so support-team users see redacted emails); the safety gate refuses DDL/DML even in the unlikely event Cortex hallucinates one; and the ops dashboard tracks confidence + refusal + user-flag rates so drift is visible before it becomes a production incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Actual (after 30 days)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg confidence per response&lt;/td&gt;
&lt;td&gt;&amp;gt; 0.75&lt;/td&gt;
&lt;td&gt;0.82&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refusal rate (safety gate)&lt;/td&gt;
&lt;td&gt;&amp;lt; 5%&lt;/td&gt;
&lt;td&gt;1.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User "wrong answer" flag rate&lt;/td&gt;
&lt;td&gt;&amp;lt; 3%&lt;/td&gt;
&lt;td&gt;2.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Golden-question semantic-match&lt;/td&gt;
&lt;td&gt;&amp;gt; 95%&lt;/td&gt;
&lt;td&gt;96.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS enforcement (unauthorised rows in results)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column-mask enforcement&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;YAML semantic model&lt;/strong&gt;&lt;/strong&gt; — the declarative grounding contract. Measures, dimensions, synonyms, and verified queries are the boundary of what Cortex Analyst can generate. Hallucination reduction is not a fine-tuning problem; it's a grounding problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Caller-role execution&lt;/strong&gt;&lt;/strong&gt; — the generated SQL runs under the analyst's own Snowflake role. RLS, column masks, warehouse grants, network policies all bind automatically. The LLM never touches row values; the safety boundary is the same as if the analyst had written the SQL by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Safety denylist + confidence gate&lt;/strong&gt;&lt;/strong&gt; — the pre-execute guardrail. DDL/DML gets refused even if Cortex hallucinates one; low-confidence responses require human review. This is the "belt-and-braces" defense against the small failure rate the grounding alone can't catch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Confidence surface in UI&lt;/strong&gt;&lt;/strong&gt; — showing users the confidence score alongside the SQL builds trust. Analysts learn to trust &lt;code&gt;&amp;gt; 0.8&lt;/code&gt; responses and to spot-check &lt;code&gt;&amp;lt; 0.6&lt;/code&gt; responses. This creates a shared vocabulary between the tool and the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — Cortex Analyst per-message costs are Snowflake-priced (Cortex credits); a typical 30-user finance-team deployment consumes ~100-300 credits/month. The engineering cost is a one-time YAML authoring investment (~1-2 weeks) plus continuous synonym curation (~30 min/week). The eliminated cost is analyst time hand-writing SQL for repetitive questions. Net payback typically &amp;lt; 2 months.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL problems on governed text-to-SQL and semantic models&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;API Integration&lt;/span&gt;
&lt;span&gt;Topic — api-integration&lt;/span&gt;
&lt;strong&gt;API-integration problems on REST-served LLM assistants&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/api-integration" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Databricks AI/BI Genie — natural-language BI over Unity Catalog
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;databricks ai/bi genie&lt;/code&gt; grounds on Unity Catalog metadata + curated example queries — the business-user chat surface over the lakehouse
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;&lt;code&gt;databricks ai/bi genie&lt;/code&gt; is the natural-language BI surface inside Databricks that scopes each conversation to a "Genie space" (a curated bundle of Unity Catalog tables + column comments + sample values + example queries), lets end users chat with the space and get charts back, and enforces every existing Unity Catalog governance policy (row-level filters, column masks, grants) automatically — its answer quality is directly proportional to how completely you've populated the metadata and how many curated example queries the space ships with&lt;/strong&gt;. Genie is the business-user counterpart to Cortex Analyst; the grounding source is Unity Catalog metadata rather than a hand-authored YAML, and the persona is analyst-adjacent business users rather than SQL-writing developers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzqq9gxcnjc2d5ht4yox0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzqq9gxcnjc2d5ht4yox0.jpeg" alt="Iconographic Databricks AI/BI Genie diagram — a Genie space card scoped to Unity Catalog tables on the left, with a natural-language question card feeding an LLM sparkle in the middle, and a chart + SQL card on the right; a warning chip 'row-level filter enforced'." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for AI/BI Genie.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grounding.&lt;/strong&gt; Unity Catalog table comments + column comments + sample values + curated example queries + optional "AI/BI Instructions" (free-text guidance the space author writes). No YAML semantic model in the Cortex-Analyst sense — Genie leans on the metadata already in Unity Catalog + hand-written examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persona.&lt;/strong&gt; Business users — marketing analysts, sales-ops managers, finance partners — who never learned SQL. Genie ships an in-workspace chat pane and is embedded in AI/BI Dashboards as a "ask a follow-up" surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance.&lt;/strong&gt; Unity Catalog RLS (&lt;code&gt;ROW FILTER&lt;/code&gt;), column masks, grants, and audit logs all bind automatically because Genie executes the generated SQL via a Databricks SQL warehouse under the caller's identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency + review.&lt;/strong&gt; 3-8 seconds per turn. Every response shows the chart and the generated SQL side by side; users can toggle to see the SQL and flag "wrong answer" if the chart looks off. Genie ships a first-class "explain / regenerate" loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Setting up a Genie space — the four-step recipe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 1: pick the scope.&lt;/strong&gt; A Genie space is scoped to a small set of Unity Catalog tables (typically 3-15). Cross-domain spaces (marketing + finance + product) tend to be brittle; single-domain spaces (marketing only) tend to be sharp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2: populate metadata.&lt;/strong&gt; Every table gets a &lt;code&gt;COMMENT&lt;/code&gt;; every column gets a &lt;code&gt;COMMENT&lt;/code&gt;. Sparse comments = brittle grounding. Sample values (via &lt;code&gt;sample_values&lt;/code&gt; or by letting Genie profile the table) help the LLM understand distributions and units.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3: curate example queries.&lt;/strong&gt; 10-50 example (question, SQL) pairs per space. These act as few-shot exemplars. Curate iteratively: watch what users ask, add the top misses as examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4: write AI/BI instructions.&lt;/strong&gt; A free-text prompt scoped to the space — "always show revenue in millions", "when asked about churn, use the &lt;code&gt;status = 'churned'&lt;/code&gt; convention", "date filters default to the last quarter unless specified."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on Genie.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How is Genie different from Cortex Analyst?" — Genie grounds on UC metadata + examples (no YAML); persona is business users not analysts; UI is chat + chart, not REST.&lt;/li&gt;
&lt;li&gt;"How do you stop it from leaking PII?" — column masks on sensitive columns; caller-identity execution; scope the space to tables the users are already granted on.&lt;/li&gt;
&lt;li&gt;"How do you evaluate a Genie space?" — golden questions per space; user-flag rate; sample-value coverage.&lt;/li&gt;
&lt;li&gt;"When would you pick Genie over embedding a dashboard?" — when the question space is unbounded (users ask novel questions); dashboards win when the question space is fixed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — setting up a Genie space for a marketing analytics team
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The canonical Genie deployment: pick 4 marketing tables (campaigns, leads, conversions, spend), populate comments and sample values, curate 15 example queries, write a page of AI/BI Instructions, and hand the space to the marketing team. Walk through the SQL / Databricks CLI commands that ship the space.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tables.&lt;/strong&gt; &lt;code&gt;marketing.campaigns&lt;/code&gt;, &lt;code&gt;marketing.leads&lt;/code&gt;, &lt;code&gt;marketing.conversions&lt;/code&gt;, &lt;code&gt;marketing.spend&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persona.&lt;/strong&gt; Marketing ops manager + 8 marketing analysts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance.&lt;/strong&gt; Marketing team has SELECT on the marketing catalog; column mask on &lt;code&gt;leads.email&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Ship the Genie space: comments, sample-value curation, example queries, instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Table + column comments&lt;/td&gt;
&lt;td&gt;grounding metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Example queries&lt;/td&gt;
&lt;td&gt;few-shot exemplars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI/BI Instructions&lt;/td&gt;
&lt;td&gt;space-scoped prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column mask&lt;/td&gt;
&lt;td&gt;PII protection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Ensure every table + column has a comment (grounding metadata)&lt;/span&gt;
&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;campaigns&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt;
  &lt;span class="s1"&gt;'One row per marketing campaign. Grain: campaign_id. Includes launch/end dates, channel, and total budget.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;campaigns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;campaign_id&lt;/span&gt;  &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="s1"&gt;'Primary key; unique per campaign.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;campaigns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;      &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="s1"&gt;'Marketing channel; one of: email, social, paid_search, display, referral.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;campaigns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;budget_cents&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="s1"&gt;'Total campaign budget in cents. Divide by 100 for USD.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;campaigns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launch_date&lt;/span&gt;  &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="s1"&gt;'Business-effective launch date.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversions&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt;
  &lt;span class="s1"&gt;'One row per conversion event. Grain: conversion_id. Links campaign_id → lead_id → revenue.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Column mask on PII&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email_mask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;is_account_group_member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'marketing-pii-approved'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'(.).*(@.*)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$1***$2'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leads&lt;/span&gt;
  &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;MASK&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email_mask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Row filter — sales-region managers see only their region&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;region_row_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;is_account_group_member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'marketing-global'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_user_group_region&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leads&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;region_row_filter&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 4. Provision the Genie space via the Databricks SDK
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;databricks.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WorkspaceClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;databricks.sdk.service.ai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GenieSpace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GenieInstruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GenieExample&lt;/span&gt;

&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WorkspaceClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;space&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;genie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_space&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GenieSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing-analytics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chat with marketing.campaigns / leads / conversions / spend.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;table_identifiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing.campaigns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing.leads&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing.conversions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing.spend&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;warehouse_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abc123-shared-analytics-wh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instructions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GenieInstruction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        - Revenue is `SUM(conversions.revenue_cents) / 100.0` in USD.
        - Cost per lead = SUM(spend.cost_cents) / COUNT(DISTINCT leads.lead_id) / 100.
        - When users say &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;campaign performance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, show conversions and cost side-by-side.
        - Default time range is the last quarter unless the user specifies.
        - Channel values are exactly: email, social, paid_search, display, referral.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;example_queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;GenieExample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What was our best-performing campaign last quarter?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sql&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                SELECT c.campaign_id, c.channel, SUM(v.revenue_cents)/100.0 AS revenue
                FROM   marketing.campaigns    c
                JOIN   marketing.conversions  v USING (campaign_id)
                WHERE  v.event_date &amp;gt;= date_trunc(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;QUARTER&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, date_sub(current_date(), 90))
                GROUP  BY c.campaign_id, c.channel
                ORDER  BY revenue DESC
                LIMIT  10
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;GenieExample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cost per lead by channel this month&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sql&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                SELECT c.channel,
                       SUM(s.cost_cents) / COUNT(DISTINCT l.lead_id) / 100.0 AS cost_per_lead
                FROM   marketing.campaigns c
                JOIN   marketing.spend     s USING (campaign_id)
                JOIN   marketing.leads     l USING (campaign_id)
                WHERE  s.spend_date &amp;gt;= date_trunc(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MONTH&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, current_date())
                GROUP  BY c.channel
                ORDER  BY cost_per_lead ASC
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;# ... 13 more curated examples
&lt;/span&gt;    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Genie space created: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;space&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every table and every column gets a &lt;code&gt;COMMENT&lt;/code&gt;. This is Genie's primary grounding source; sparse comments produce brittle answers. The comment on &lt;code&gt;channel&lt;/code&gt; enumerates the valid values (&lt;code&gt;email, social, paid_search, display, referral&lt;/code&gt;), so Genie knows not to invent channels users might reference.&lt;/li&gt;
&lt;li&gt;Column masks and row filters are Unity Catalog features that bind automatically to any query — including Genie's generated SQL. The email-mask function redacts email addresses for anyone not in &lt;code&gt;marketing-pii-approved&lt;/code&gt;; the row filter restricts non-global users to their own region. Genie's caller-identity execution means these policies inherit for free.&lt;/li&gt;
&lt;li&gt;The Genie space is provisioned via the Databricks SDK with (a) the four table identifiers, (b) a shared warehouse for execution, (c) free-text AI/BI Instructions that give the LLM space-scoped guidance (business rules, definitions, defaults), and (d) 15 curated example queries.&lt;/li&gt;
&lt;li&gt;AI/BI Instructions do the work that verified queries do in Cortex Analyst — they encode business rules and defaults that would otherwise require the user to specify every time. "Revenue is &lt;code&gt;SUM(conversions.revenue_cents) / 100.0&lt;/code&gt; in USD" prevents the LLM from picking a different revenue definition.&lt;/li&gt;
&lt;li&gt;Example queries are the highest-leverage grounding artifact after column comments. Every business phrase you want the space to handle well ("best-performing campaign", "cost per lead", "conversion rate by channel") should have a curated example. Curate iteratively: watch the top user questions in production, add the top misses as examples.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Genie space asset&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scoped tables&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;grounding scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column comments&lt;/td&gt;
&lt;td&gt;~40 (all columns)&lt;/td&gt;
&lt;td&gt;grounding metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Example queries&lt;/td&gt;
&lt;td&gt;15 (curated)&lt;/td&gt;
&lt;td&gt;few-shot exemplars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI/BI instructions&lt;/td&gt;
&lt;td&gt;1 page&lt;/td&gt;
&lt;td&gt;space-scoped prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row filters&lt;/td&gt;
&lt;td&gt;1 (region)&lt;/td&gt;
&lt;td&gt;governance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column masks&lt;/td&gt;
&lt;td&gt;1 (email)&lt;/td&gt;
&lt;td&gt;PII protection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Every Genie space needs (a) complete column comments, (b) at least 10 curated example queries, and (c) a page of AI/BI Instructions. Skimping on any of the three produces brittle answers. Budget 1-2 days to author a space; treat it as a living artifact that improves with usage.&lt;/p&gt;

&lt;h4&gt;
  
  
  Worked example — the sparse-comments failure mode + iterative curation
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; A team ships a Genie space with only table-level comments (no column comments) and 3 example queries. Users ask questions like "what's the CTR by channel?"; Genie hallucinates a &lt;code&gt;ctr&lt;/code&gt; column that doesn't exist. The fix is a curation sprint: comment every column with its meaning and unit, add 15 example queries covering the top user phrases, and iterate weekly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom.&lt;/strong&gt; ~35% of user questions return either "I don't know" or wrong-column errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause.&lt;/strong&gt; Column meanings are inferable from column names alone; Genie hallucinates plausible column names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; Populate every column comment; add example queries covering the top misses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Ship the "comment audit" script that grades a Genie space's grounding completeness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Comment coverage&lt;/td&gt;
&lt;td&gt;100% of columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Example query count&lt;/td&gt;
&lt;td&gt;≥ 10 per space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ambiguous column names&lt;/td&gt;
&lt;td&gt;zero (or explicitly commented)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/audit_genie_grounding.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;databricks.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WorkspaceClient&lt;/span&gt;

&lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing.campaigns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing.leads&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing.conversions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing.spend&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WorkspaceClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;warehouse_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abc123-shared-analytics-wh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT column_name, comment
            FROM   &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.INFORMATION_SCHEMA.COLUMNS
            WHERE  table_name = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_array&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;commented&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commented&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;covered&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;coverage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commented&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;audit_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;commented&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  (missing: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;commented&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;11.1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The audit script queries &lt;code&gt;INFORMATION_SCHEMA.COLUMNS&lt;/code&gt; for every table in every Genie space; any column without a &lt;code&gt;comment&lt;/code&gt; is flagged. This runs weekly in CI or on-demand; the output tells the space author exactly which columns need attention.&lt;/li&gt;
&lt;li&gt;The fix is per-column &lt;code&gt;COMMENT ON COLUMN&lt;/code&gt; DDL. Include: (a) the column's meaning in one sentence, (b) the unit if numeric ("cents", "USD", "seconds"), (c) the valid values if categorical, (d) any known gotchas ("nullable; NULL means unknown, not zero").&lt;/li&gt;
&lt;li&gt;Sample values are the second-order grounding source. Genie can auto-profile tables to learn value distributions, but explicit sample values in the comment (&lt;code&gt;'Channel; one of: email, social, paid_search'&lt;/code&gt;) are more reliable than profiling.&lt;/li&gt;
&lt;li&gt;Curation sprints on example queries: every week, pull the top 20 low-confidence questions from the Genie audit log; convert the ones that should have worked into example queries; deploy. Genie answer quality improves visibly week over week under this loop.&lt;/li&gt;
&lt;li&gt;Track a "grounding completeness score" per space: (comment coverage) × (example query count / target) × (AI/BI Instructions presence). Below 0.7 = brittle; 0.7-0.9 = usable; &amp;gt; 0.9 = production. Publish the score alongside space usage metrics.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Comment coverage&lt;/th&gt;
&lt;th&gt;Example queries&lt;/th&gt;
&lt;th&gt;Answer quality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bootstrap&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;brittle (~35% wrong)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comment sprint&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;improved (~15% wrong)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Example curation&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;usable (~5% wrong)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly iteration&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;30+&lt;/td&gt;
&lt;td&gt;production (~2% wrong)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Genie space quality is a curation game, not a modelling game. Ship the audit script; publish the grounding-completeness score; run weekly curation sprints. Six weeks of curation moves a brittle space to a production-grade one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on Genie
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "You're launching Genie to a marketing team on Databricks. Design the space setup, the governance, the eval harness, and the on-call story when a business user reports 'the numbers are wrong.'"&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using scoped Genie space + UC governance + weekly curation sprint + a "wrong-answer" triage runbook
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Governance — RLS + column mask (as before)&lt;/span&gt;
&lt;span class="c1"&gt;-- 2. Comments on every column (audit script enforces)&lt;/span&gt;
&lt;span class="c1"&gt;-- 3. Genie space created via SDK with 15 example queries + AI/BI instructions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 4. Wrong-answer triage runbook (encoded as a script)
# scripts/triage_wrong_answer.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generated_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;correct_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Categorise a user-flagged wrong answer and produce a fix ticket.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;diagnosis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="c1"&gt;# (a) grounding gap
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;generated_sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;correct_sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FROM MARKETING.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JOIN MARKETING.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grounding-gap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;add column/table comment; re-audit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# (b) missing example query
&lt;/span&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;correct_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GROUP BY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;correct_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing-example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;add example query: (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;correct_sql&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# (c) business rule missing from AI/BI Instructions
&lt;/span&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;revenue_cents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;generated_sql&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;generated_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;instruction-gap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;add rule to AI/BI Instructions: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;divide revenue_cents by 100&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;

    &lt;span class="c1"&gt;# (d) LLM regression (rare — file a Genie support ticket)
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm-regression&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalate to Databricks support with the pair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;diagnosis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;diagnosis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generated_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;generated_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;correct_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unity Catalog&lt;/td&gt;
&lt;td&gt;column comments + RLS + masks&lt;/td&gt;
&lt;td&gt;grounding + governance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Genie space&lt;/td&gt;
&lt;td&gt;4 tables + 15 examples + instructions&lt;/td&gt;
&lt;td&gt;conversation scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly sprint&lt;/td&gt;
&lt;td&gt;audit + example curation&lt;/td&gt;
&lt;td&gt;continuous grounding improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong-answer triage&lt;/td&gt;
&lt;td&gt;script categorises the failure mode&lt;/td&gt;
&lt;td&gt;fast fix routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ops dashboard&lt;/td&gt;
&lt;td&gt;user-flag rate + comment coverage&lt;/td&gt;
&lt;td&gt;production monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the deployment, marketing analysts ask questions in English, see charts + generated SQL, flag surprises with one click; the weekly curation sprint absorbs the flags into either new comments, new example queries, or new AI/BI Instructions; the wrong-answer triage script routes each flag to the correct fix in under 60 seconds. Space quality improves visibly for the first 6 weeks, then stabilises.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Week 1&lt;/th&gt;
&lt;th&gt;Week 6&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User "wrong answer" flag rate&lt;/td&gt;
&lt;td&gt;~15%&lt;/td&gt;
&lt;td&gt;~2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comment coverage&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Example queries&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg time to fix a flag&lt;/td&gt;
&lt;td&gt;3 days&lt;/td&gt;
&lt;td&gt;2 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketing-team confidence&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Unity-Catalog-grounded&lt;/strong&gt;&lt;/strong&gt; — no YAML to author separately; the grounding source is the same metadata that powers governance, lineage, and search. One source of truth reduces drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Scoped Genie space&lt;/strong&gt;&lt;/strong&gt; — a 4-table space produces sharper answers than a 40-table space. Scope forces you to think about persona and use case; sprawl invites hallucination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Curation loop&lt;/strong&gt;&lt;/strong&gt; — weekly example-query additions and comment refinements move space quality from brittle to production in ~6 weeks. Space quality is a function of continuous investment, not one-time setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Wrong-answer triage&lt;/strong&gt;&lt;/strong&gt; — the runbook categorises each flag into one of four failure modes (grounding gap, missing example, instruction gap, LLM regression) so the fix is targeted, not shotgun.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — space quality is bounded by curation time (~2 hours/week per space post-bootstrap) and SQL warehouse compute per query. Compared to building custom analyst-only dashboards for every question, Genie's marginal cost per novel question is ~$0.01 in warehouse credits. Net O(1) per question; O(space size × week) for curation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Data Analysis&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — data-analysis&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Data-analysis problems on natural-language BI patterns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-analysis" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;




&lt;span&gt;Data Validation&lt;/span&gt;
&lt;span&gt;Topic — data-validation&lt;/span&gt;
&lt;strong&gt;Data-validation problems on generated-SQL correctness&lt;/strong&gt;


&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/data-validation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Picking / stacking the assistants — decision matrix + interview signals
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;llm assistants for analytics&lt;/code&gt; should be &lt;em&gt;stacked&lt;/em&gt;, not picked — dbt Copilot upstream authors what Cortex Analyst / Genie serve downstream
&lt;/h3&gt;

&lt;p&gt;The mental model in one line: &lt;strong&gt;the mature 2026 answer to "which LLM assistant should we deploy" is &lt;em&gt;stack them&lt;/em&gt;: dbt Copilot at the authoring layer builds and documents the models, dbt semantic models declare the metrics, Cortex Analyst serves the SQL-writing persona from the same semantic layer via YAML export, and Genie serves the business-user persona from the same warehouse via Unity-Catalog metadata — each assistant grounds on an artifact the upstream layer already produces, so the total investment scales as O(models) rather than O(assistants × personas)&lt;/strong&gt;. Picking one assistant only works for teams with one persona; the moment you have authors, analysts, and business users, you need the stack.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5fs0y4aw8fcdxsaaekx.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5fs0y4aw8fcdxsaaekx.jpeg" alt="Iconographic decision-matrix diagram — a 4x4 grid comparing dbt Copilot, Cortex Analyst, Genie, and generic LLM across grounding, governance, persona, and latency, plus a stacked-arrow diagram showing dbt Copilot upstream and Cortex/Genie downstream." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four axes for the stack decision.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grounding compatibility.&lt;/strong&gt; dbt Copilot grounds on the compiled project; dbt semantic models export to Cortex Analyst YAML; Unity Catalog comments (which dbt can &lt;code&gt;persist_docs&lt;/code&gt;) feed Genie. The upstream layer &lt;em&gt;feeds&lt;/em&gt; the downstream layer, so authoring effort compounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persona coverage.&lt;/strong&gt; Authors → analysts → business users. Each persona has a canonical assistant. Skipping a persona = a coverage gap; the skipped users either fall back to hand-written SQL or an ungoverned side-channel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance uniformity.&lt;/strong&gt; All three serving assistants (Cortex Analyst, Genie, dbt semantic layer) execute under caller identity, so warehouse-level RLS + column masks bind uniformly. Authoring assistants (dbt Copilot) have no runtime governance; safety comes from CI + review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation harness.&lt;/strong&gt; One golden-question suite per semantic model, run in CI on every change, catches regressions across whichever assistant consumes the semantic layer. One test suite, many assistants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The stack recipe.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layer 1: authoring.&lt;/strong&gt; dbt Cloud + dbt Copilot. Models + tests + docs + semantic models authored inline. Manifest + catalog kept fresh via pre-commit + CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 2: semantic layer.&lt;/strong&gt; dbt semantic models declare metrics once. Exporter (&lt;code&gt;dbt-cortex&lt;/code&gt;, custom script) transforms them into Cortex Analyst YAML. &lt;code&gt;persist_docs&lt;/code&gt; writes descriptions into the warehouse for Genie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 3: serving.&lt;/strong&gt; Cortex Analyst for the analyst / developer persona (REST API, YAML-grounded). Genie for the business-user persona (chat + chart, UC-grounded). Both execute under caller identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 4: evaluation.&lt;/strong&gt; Golden-question suite per semantic model. CI runs it on every YAML / model change; alerts on regression.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common interview probes on stacking.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Do you pick one assistant or stack them?" — required answer: stack; each persona has a canonical assistant.&lt;/li&gt;
&lt;li&gt;"What's the single source of truth for the semantic layer?" — required answer: dbt semantic models; export to Cortex YAML, &lt;code&gt;persist_docs&lt;/code&gt; to Unity Catalog.&lt;/li&gt;
&lt;li&gt;"How do you keep them consistent?" — CI export + drift check + golden-question harness.&lt;/li&gt;
&lt;li&gt;"What if you're on Snowflake but not Databricks?" — dbt Copilot + Cortex Analyst covers authors + analysts + business users (via a Streamlit / internal-app wrapper on Cortex).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Worked example — the stacked-assistant reference architecture
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Detailed explanation.&lt;/strong&gt; The reference architecture: dbt Cloud authors semantic models; a CI job exports them to Cortex Analyst YAML + persists comments to Unity Catalog; Cortex Analyst serves the analyst REST API; Genie serves business-user chat; a shared golden-question suite runs on every semantic-model change and gates deploys.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo layout.&lt;/strong&gt; &lt;code&gt;dbt_project/&lt;/code&gt; (models + semantic models), &lt;code&gt;semantic_models/&lt;/code&gt; (exported YAML), &lt;code&gt;eval/&lt;/code&gt; (golden questions), &lt;code&gt;.github/workflows/&lt;/code&gt; (CI).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI pipeline.&lt;/strong&gt; On PR: compile dbt → export semantic models to Cortex YAML → upload YAML to Snowflake stage → run golden-question harness → gate merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance.&lt;/strong&gt; RLS + column masks on warehouse tables; Cortex Analyst + Genie inherit automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question.&lt;/strong&gt; Show the CI pipeline that keeps the stack consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dbt compile&lt;/td&gt;
&lt;td&gt;manifest freshness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic-model export&lt;/td&gt;
&lt;td&gt;dbt → Cortex YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML upload&lt;/td&gt;
&lt;td&gt;to Snowflake stage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UC comment sync&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;persist_docs&lt;/code&gt; for Genie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Golden-question run&lt;/td&gt;
&lt;td&gt;regression gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/stacked-assistants.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stacked-assistants&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dbt_project/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;semantic_models/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eval/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install dbt-snowflake==1.9.0 pyyaml requests snowflake-connector-python&lt;/span&gt;

      &lt;span class="c1"&gt;# Layer 1 — compile dbt (Copilot grounding freshness)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd dbt_project &amp;amp;&amp;amp; dbt deps &amp;amp;&amp;amp; dbt compile --profiles-dir ci_profiles&lt;/span&gt;

      &lt;span class="c1"&gt;# Layer 2 — export semantic models to Cortex YAML&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/export_dbt_to_cortex.py \&lt;/span&gt;
                 &lt;span class="s"&gt;--manifest dbt_project/target/manifest.json \&lt;/span&gt;
                 &lt;span class="s"&gt;--out semantic_models/revenue_model.yaml&lt;/span&gt;

      &lt;span class="c1"&gt;# Layer 2b — persist docs (writes UC comments for Genie)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd dbt_project &amp;amp;&amp;amp; dbt docs generate&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd dbt_project &amp;amp;&amp;amp; dbt run-operation persist_all_docs&lt;/span&gt;

      &lt;span class="c1"&gt;# Layer 3 — upload YAML to Snowflake stage&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/upload_yaml_to_stage.py semantic_models/revenue_model.yaml&lt;/span&gt;

      &lt;span class="c1"&gt;# Layer 4 — golden-question regression gate&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python eval/run_golden_questions.py&lt;/span&gt;

      &lt;span class="c1"&gt;# Layer 4b — schema-drift check&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/check_cortex_yaml_drift.py&lt;/span&gt;

      &lt;span class="c1"&gt;# Layer 4c — Copilot drift check&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/check_copilot_drift.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/export_dbt_to_cortex.py — one exporter, one semantic layer, three assistants
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dbt_metric_to_cortex_measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Translate a dbt metric into a Cortex Analyst measure block.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type_params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NUMBER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;synonyms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;synonyms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dbt_dimension_to_cortex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEXT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;synonyms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;synonyms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--manifest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;sms&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic_models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;model_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node_relation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node_relation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node_relation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node_relation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dbt_dimension_to_cortex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                           &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categorical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time_dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dbt_dimension_to_cortex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;measures&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dbt_metric_to_cortex_measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;cortex_yaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;revenue_model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Auto-exported from dbt semantic models.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cortex_yaml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exported &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tables to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The CI job runs on any PR touching dbt models, semantic models, or the eval harness. Each layer of the stack has its own step; a failure at any layer blocks the merge, so drift never reaches production.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dbt compile&lt;/code&gt; refreshes the manifest — the grounding source for dbt Copilot. This is also the source-of-truth artifact for the exporter script.&lt;/li&gt;
&lt;li&gt;The exporter translates dbt semantic models into Cortex Analyst YAML. Metrics become measures; categorical dimensions stay as dimensions; time-typed dimensions become time-dimensions. The synonym list travels from dbt &lt;code&gt;meta.synonyms&lt;/code&gt; to Cortex &lt;code&gt;synonyms&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;persist_docs&lt;/code&gt; writes dbt column descriptions into the warehouse as native &lt;code&gt;COMMENT ON COLUMN&lt;/code&gt; statements. This is what feeds Genie's grounding — one authoring workflow (dbt) produces grounding metadata for both Cortex (YAML) and Genie (warehouse comments).&lt;/li&gt;
&lt;li&gt;The golden-question harness (from section 1) runs against the deployed YAML and gates the merge. Schema-drift check + Copilot-drift check are additional safety rails. Merged PRs cannot break any assistant in the stack.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Artifact produced&lt;/th&gt;
&lt;th&gt;Consumed by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dbt models&lt;/td&gt;
&lt;td&gt;fct_&lt;em&gt;, dim_&lt;/em&gt; tables&lt;/td&gt;
&lt;td&gt;warehouse queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt semantic models&lt;/td&gt;
&lt;td&gt;sm_orders (metrics + dims)&lt;/td&gt;
&lt;td&gt;exporter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exporter&lt;/td&gt;
&lt;td&gt;revenue_model.yaml&lt;/td&gt;
&lt;td&gt;Cortex Analyst&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;persist_docs&lt;/td&gt;
&lt;td&gt;UC column comments&lt;/td&gt;
&lt;td&gt;Genie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;golden-question harness&lt;/td&gt;
&lt;td&gt;regression score&lt;/td&gt;
&lt;td&gt;CI gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Author the semantic layer once, in dbt; export it to every consuming assistant (Cortex YAML, UC comments); run one golden-question suite that catches regressions across the stack. The upfront authoring investment pays off across every persona.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior interview question on stacking
&lt;/h3&gt;

&lt;p&gt;A senior interviewer might ask: "Design the LLM-assistant strategy for a company on Snowflake + dbt + Databricks (they have both warehouses). Cover the authoring persona, analyst persona, and business-user persona; explain how the semantic layer stays consistent across three assistants and two warehouses; specify the eval harness that gates every change."&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Using a dbt-first semantic layer, exporters to Cortex + UC, and a unified golden-question harness
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. dbt semantic models — the single source of truth&lt;/span&gt;
&lt;span class="c1"&gt;# (as in section 2's worked example)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. Two exporters — one per warehouse
# scripts/export_dbt_to_cortex.py   (as above; produces Cortex YAML)
# scripts/export_dbt_to_uc.py       (produces UC COMMENT ON COLUMN DDL)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 3. Unified golden-question harness — runs against BOTH assistants
# eval/run_golden_questions.py extended with --backend cortex|genie
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cortex_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ask_cortex&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;genie_client&lt;/span&gt;  &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ask_genie&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cortex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ask_cortex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;genie&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ask_genie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;space_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;genie_space_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;semantic_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step-by-step trace.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Assistant fed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dbt semantic models&lt;/td&gt;
&lt;td&gt;metrics + dims&lt;/td&gt;
&lt;td&gt;authoring source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cortex exporter&lt;/td&gt;
&lt;td&gt;revenue_model.yaml&lt;/td&gt;
&lt;td&gt;Snowflake Cortex Analyst&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UC exporter&lt;/td&gt;
&lt;td&gt;COMMENT ON DDL&lt;/td&gt;
&lt;td&gt;Databricks Genie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Golden questions&lt;/td&gt;
&lt;td&gt;shared suite (backend switch)&lt;/td&gt;
&lt;td&gt;both regressions caught&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI&lt;/td&gt;
&lt;td&gt;5-stage pipeline&lt;/td&gt;
&lt;td&gt;gates every merge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the rollout, one dbt PR simultaneously updates dbt Copilot's grounding (via manifest recompile), Cortex Analyst's grounding (via YAML export), and Genie's grounding (via UC comment persist). The shared golden-question harness catches regressions on either backend. The company runs three assistants in production but maintains one semantic layer — the authoring investment scales linearly, not quadratically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Single-assistant deployment&lt;/th&gt;
&lt;th&gt;Stacked deployment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Semantic layer sources&lt;/td&gt;
&lt;td&gt;1-3 (fragmented)&lt;/td&gt;
&lt;td&gt;1 (dbt)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authoring cost per new metric&lt;/td&gt;
&lt;td&gt;3 (one per assistant)&lt;/td&gt;
&lt;td&gt;1 (dbt only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI regression coverage&lt;/td&gt;
&lt;td&gt;per-assistant&lt;/td&gt;
&lt;td&gt;unified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persona coverage&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance uniformity&lt;/td&gt;
&lt;td&gt;ad-hoc&lt;/td&gt;
&lt;td&gt;inherited (caller role)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-to-add a new metric&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt; — concept by concept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Stack, don't pick&lt;/strong&gt;&lt;/strong&gt; — assistants complement, they don't compete. Authoring assistants (dbt Copilot) sit above serving assistants (Cortex, Genie); serving assistants sit above governance (warehouse RLS). Skipping a layer creates a coverage gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;One semantic layer, many exporters&lt;/strong&gt;&lt;/strong&gt; — dbt semantic models are the source of truth. Cortex YAML and UC comments are derived artifacts, regenerated by CI on every model change. Drift is impossible when the derivation is automated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Persona-matched serving&lt;/strong&gt;&lt;/strong&gt; — Cortex Analyst for analysts (REST + SQL surfaced), Genie for business users (chat + chart). Each persona gets the UX that fits; neither is asked to use the other's UX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Unified golden-question harness&lt;/strong&gt;&lt;/strong&gt; — one suite, two backends. Both assistants are held to the same accuracy bar; a regression on either fails CI. Evaluation cost scales as O(questions), not O(questions × assistants).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/strong&gt; — one-time exporter development (~1 sprint), continuous dbt authoring (weeks), zero incremental per-assistant maintenance. Compared to maintaining three independent semantic layers, this is a 3× reduction in authoring cost and a 3× reduction in drift risk. Net O(dbt project size) authoring; O(1) per-assistant marginal cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;span&gt;Design&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — design&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;Design problems on multi-assistant analytics stacks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/design" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;


&lt;p&gt;&lt;span&gt;SQL Generation&lt;/span&gt;&lt;br&gt;
&lt;span&gt;Topic — sql-generation&lt;/span&gt;&lt;br&gt;
&lt;strong&gt;SQL-generation problems on stacked semantic-layer patterns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;Practice →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;





&lt;h2&gt;
  
  
  Cheat sheet — LLM assistant recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Which assistant when.&lt;/strong&gt; dbt Copilot is the 2026 default for the authoring persona whenever your team uses dbt Cloud (or a dbt-Copilot-compatible plugin). Cortex Analyst is the default text-to-SQL layer for analyst-persona serving on Snowflake, grounded on a hand-authored YAML semantic model. Databricks AI/BI Genie is the default business-user chat surface on Databricks, grounded on Unity Catalog comments + curated example queries. Generic LLM wrappers (Cursor / Claude / GPT-4o) belong in the engineer's ad-hoc toolbox, not in a governed production workload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dbt Copilot enablement checklist.&lt;/strong&gt; Enable Copilot in dbt Cloud project settings; enforce &lt;code&gt;dbt compile&lt;/code&gt; on pre-commit for anyone authoring outside dbt Cloud IDE; enforce &lt;code&gt;dbt docs generate&lt;/code&gt; after any column-shape change; set &lt;code&gt;persist_docs: {relation: true, columns: true}&lt;/code&gt; in &lt;code&gt;dbt_project.yml&lt;/code&gt; so YAML descriptions land in the warehouse for downstream Genie / Cortex consumption; run a CI drift-check script that fails PRs referencing unknown &lt;code&gt;ref()&lt;/code&gt; targets; adopt a Copilot-code-review rubric (ref hallucination, column hallucination, doc mismatch, semantic-model entity types).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cortex Analyst semantic-model YAML template.&lt;/strong&gt; &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;tables[]&lt;/code&gt; (each with &lt;code&gt;base_table.{database,schema,table}&lt;/code&gt;, &lt;code&gt;primary_key.columns&lt;/code&gt;, &lt;code&gt;dimensions[]&lt;/code&gt;, &lt;code&gt;time_dimensions[]&lt;/code&gt;, &lt;code&gt;measures[]&lt;/code&gt; with &lt;code&gt;expr&lt;/code&gt; + &lt;code&gt;synonyms&lt;/code&gt; + &lt;code&gt;description&lt;/code&gt;), &lt;code&gt;relationships[]&lt;/code&gt; (declared FKs; joins refused without them), &lt;code&gt;verified_queries[]&lt;/code&gt; (10-50 few-shot exemplars). Upload to a Snowflake stage; reference via &lt;code&gt;@DB.SCHEMA.STAGE/model.yaml&lt;/code&gt; in the REST call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cortex Analyst REST call.&lt;/strong&gt; &lt;code&gt;POST https://&amp;lt;account&amp;gt;.snowflakecomputing.com/api/v2/cortex/analyst/message&lt;/code&gt; with &lt;code&gt;{"messages": [{"role":"user","content":[{"type":"text","text":"&amp;lt;question&amp;gt;"}]}], "semantic_model_file": "@..."}&lt;/code&gt;; Bearer PAT. Response &lt;code&gt;message.content&lt;/code&gt; has &lt;code&gt;text&lt;/code&gt; interpretation blocks, &lt;code&gt;sql&lt;/code&gt; blocks (&lt;code&gt;statement&lt;/code&gt; + &lt;code&gt;confidence&lt;/code&gt;), and optional &lt;code&gt;suggestion&lt;/code&gt; blocks. Execute the SQL under the caller's role — never Cortex's — so RLS + column masks bind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databricks Genie space setup steps.&lt;/strong&gt; (1) Scope the space to 3-15 Unity Catalog tables in one domain; (2) ensure every column has a &lt;code&gt;COMMENT&lt;/code&gt; (audit script enforces); (3) apply column masks and row filters on sensitive columns; (4) curate 10+ example queries per space; (5) write a page of AI/BI Instructions covering business rules, defaults, and unit conventions; (6) provision via Databricks SDK (&lt;code&gt;w.genie.create_space(...)&lt;/code&gt;) or the workspace UI; (7) ship the wrong-answer triage runbook so flags convert to fixes in hours, not weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golden-question evaluation harness skeleton.&lt;/strong&gt; &lt;code&gt;eval/golden_questions.json&lt;/code&gt; — 100-500 &lt;code&gt;(question, expected_sql)&lt;/code&gt; pairs per semantic model. &lt;code&gt;eval/run_golden_questions.py&lt;/code&gt; — POSTs each question to the assistant, extracts generated SQL, canonicalises via &lt;code&gt;sql_metadata&lt;/code&gt; / &lt;code&gt;sqlglot&lt;/code&gt;, computes semantic-match score. CI gate: block merge on exact-match &amp;lt; 90% OR semantic-match &amp;lt; 95%. Track drift week-over-week on a dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema-drift check.&lt;/strong&gt; For Cortex: parse the YAML, extract declared columns per table, query Snowflake &lt;code&gt;INFORMATION_SCHEMA.COLUMNS&lt;/code&gt;, fail on any YAML column not in the warehouse. For dbt Copilot: parse &lt;code&gt;ref()&lt;/code&gt; calls, verify each target exists in &lt;code&gt;manifest.json&lt;/code&gt;. For Genie: audit &lt;code&gt;INFORMATION_SCHEMA.COLUMNS&lt;/code&gt; for comment coverage (target 100%). Run all three in CI on every PR touching models or YAML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance uniformity contract.&lt;/strong&gt; All three serving assistants (Cortex Analyst REST, Genie chat, dbt semantic layer) execute the generated SQL under the &lt;em&gt;caller's&lt;/em&gt; warehouse role. Row-access policies, column masks, warehouse grants, and audit logs bind automatically. Authoring assistants (dbt Copilot) have no runtime governance; the safety comes from human review + CI. This is the key argument against generic LLM wrappers — they run under a service account with wide grants, so PII leaks are structural, not incidental.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synonym / vocabulary curation loop.&lt;/strong&gt; For Cortex Analyst: expand &lt;code&gt;synonyms&lt;/code&gt; on every dimension and measure to cover business-user phrasing ("MRR", "GMV", "logos", "geo"); add verified queries for the top misses. For Genie: refine table + column comments, add example queries, tweak AI/BI Instructions. Run a weekly 30-minute curation sprint per semantic model / Genie space; the vocabulary stabilises after 6-8 weeks then requires only monthly refreshes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong-answer triage runbook.&lt;/strong&gt; When a user flags a wrong answer, categorise it in under 60 seconds: (a) grounding gap → add column comment + re-audit; (b) missing example → add verified query / Genie example; (c) instruction gap → add rule to AI/BI Instructions; (d) LLM regression → escalate to vendor. Log every flag; publish weekly flag-rate + fix-time dashboards; treat &amp;gt; 5% flag rate as a P2 incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stacking pattern.&lt;/strong&gt; dbt Copilot (authoring) → dbt semantic models (single source of truth) → exporters (dbt → Cortex YAML; dbt persist_docs → UC comments) → Cortex Analyst (analyst persona) + Genie (business-user persona) → unified golden-question harness (regression gate). One semantic layer, many consuming assistants; O(models) authoring cost, O(1) per-assistant marginal cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence surface + safety gate.&lt;/strong&gt; For every generated SQL, surface the model's confidence score to the user (Cortex returns it in the &lt;code&gt;sql&lt;/code&gt; block; Genie surfaces it in the UI). Pre-execute safety gate: refuse DDL/DML via a regex denylist; refuse &lt;code&gt;confidence &amp;lt; 0.6&lt;/code&gt; without human review; cap &lt;code&gt;LIMIT&lt;/code&gt; on unbounded queries; require &lt;code&gt;WHERE&lt;/code&gt; on partitioned columns for cost control. This is the belt-and-braces defense between grounded generation and warehouse execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost management.&lt;/strong&gt; Cortex Analyst credits ~$0.01-0.05 per message depending on model + question complexity; Genie warehouse compute scales with SQL execution cost (typically $0.001-0.01 per turn on a serverless warehouse). Budget alerts + per-user rate limits are non-optional; unbounded usage can spike credit consumption. dbt Copilot is priced as a dbt Cloud seat add-on; per-user cost is fixed and cheap relative to authoring productivity gain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration cost between assistants.&lt;/strong&gt; Adding Cortex Analyst on top of an existing dbt project: ~1 sprint per semantic model (YAML authoring + REST integration + golden questions). Adding Genie on top of a Databricks project with dense UC comments: ~2 days per space (scope + examples + instructions). Migrating off a generic-LLM wrapper to a grounded assistant: ~2-4 weeks (semantic-layer authoring + integration switch + user retraining). The stacked deployment is worth the upfront cost by month 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an LLM assistant for analytics in one sentence?
&lt;/h3&gt;

&lt;p&gt;An &lt;code&gt;llm data assistant&lt;/code&gt; for analytics is a natural-language interface — inline completion in an IDE, REST API, or embedded chat pane — that grounds an LLM's generation on a declarative artifact (dbt project graph, YAML semantic model, Unity Catalog metadata) so the assistant can produce SQL, docs, semantic-layer metrics, or charts that reference the team's real tables, columns, and metrics without hallucinating. The three canonical vendor assistants — &lt;code&gt;dbt copilot&lt;/code&gt; (project-graph-grounded authoring in dbt Cloud), &lt;code&gt;snowflake cortex analyst&lt;/code&gt; (YAML-semantic-model-grounded text-to-SQL REST API), and &lt;code&gt;databricks ai/bi genie&lt;/code&gt; (Unity-Catalog-grounded chat-with-your-data over the lakehouse) — differ in grounding source, target persona, governance boundary, and latency, and the choice binds every downstream question, dashboard, and trained business user for years. Every senior analytics-engineering interview probes LLM assistants because they're the load-bearing UX pattern for the modern warehouse + semantic-layer stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  dbt Copilot vs Snowflake Cortex Analyst — when do I pick each?
&lt;/h3&gt;

&lt;p&gt;Pick &lt;strong&gt;dbt Copilot&lt;/strong&gt; when the user is an &lt;em&gt;analytics engineer&lt;/em&gt; authoring dbt models, tests, YAML docs, or semantic-layer metrics — Copilot is embedded in the dbt Cloud IDE, grounds on the compiled &lt;code&gt;manifest.json&lt;/code&gt; + &lt;code&gt;catalog.json&lt;/code&gt;, and drafts SQL / YAML / semantic-model blocks that reference your real refs, sources, and columns. Pick &lt;strong&gt;Snowflake Cortex Analyst&lt;/strong&gt; when the user is an &lt;em&gt;analyst or app developer&lt;/em&gt; on Snowflake who needs &lt;code&gt;natural language to sql&lt;/code&gt; grounded on a declarative semantic model — it's a REST API that consumes a &lt;code&gt;semantic_model.yaml&lt;/code&gt; you author, returns generated SQL + confidence + interpretation, and executes under the caller's Snowflake role so RLS and column masks bind automatically. The mature answer is &lt;em&gt;both&lt;/em&gt;: dbt Copilot at the authoring layer, Cortex Analyst at the serving layer, with dbt semantic models as the single source of truth exported to Cortex YAML via CI. Never treat the two as substitutes; they serve different personas at different points in the analytics workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a semantic layer and why does it matter for LLM assistants?
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;semantic layer&lt;/code&gt; is a declarative artifact that names your business measures (&lt;code&gt;revenue&lt;/code&gt;, &lt;code&gt;mrr&lt;/code&gt;, &lt;code&gt;churn&lt;/code&gt;) and dimensions (&lt;code&gt;region&lt;/code&gt;, &lt;code&gt;customer_segment&lt;/code&gt;, &lt;code&gt;channel&lt;/code&gt;) once, ties each to its underlying SQL expression against warehouse tables, enumerates synonyms for user-facing vocabulary, declares foreign-key relationships between tables, and (for the mature implementations) ships verified example queries as few-shot exemplars. It matters for LLM assistants because the semantic layer is the &lt;em&gt;grounding contract&lt;/em&gt; — the LLM can only generate what's declared, so hallucination is bounded by design rather than fought after the fact. Cortex Analyst grounds on a YAML semantic model; Genie grounds on Unity Catalog metadata + example queries (a lightweight semantic layer); dbt Copilot grounds on &lt;code&gt;semantic_models.yml&lt;/code&gt; blocks in the project. Without a semantic layer, every assistant collapses to "guess from table names", which is the failure mode users experience as "the numbers are wrong."&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use these assistants without exposing raw table access to end users?
&lt;/h3&gt;

&lt;p&gt;Yes — and you &lt;em&gt;should&lt;/em&gt;. The three vendor assistants all execute their generated SQL under the &lt;em&gt;caller's&lt;/em&gt; warehouse identity, not a shared service account, so every row-access policy, column mask, warehouse grant, and audit log applies exactly as if the user had written the SQL by hand. Cortex Analyst returns the generated SQL to your application; your application runs it via the Snowflake connector as the authenticated user. Genie runs it via a Databricks SQL warehouse under the caller's Unity Catalog identity. dbt Copilot only &lt;em&gt;drafts&lt;/em&gt; SQL; a human commits and dbt Cloud runs it under the project credentials with normal RBAC + code review. The pattern to avoid is a generic LLM wrapper with its own service account — that pattern leaks PII structurally, because the LLM has grants the end user doesn't. Grounded vendor assistants + caller-identity execution + column masks + row-level filters is the correct 2026 answer for regulated data.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I evaluate an LLM analytics assistant?
&lt;/h3&gt;

&lt;p&gt;Ship a &lt;strong&gt;golden-question harness&lt;/strong&gt; on day one: 100-500 &lt;code&gt;(question, expected_sql)&lt;/code&gt; pairs per semantic model, maintained in a &lt;code&gt;eval/golden_questions.json&lt;/code&gt; file under version control. On every semantic-model change (dbt PR, YAML edit, new Genie example), run the harness in CI: POST each question to the assistant, extract the generated SQL, canonicalise it (via &lt;code&gt;sqlglot&lt;/code&gt; or &lt;code&gt;sql_metadata&lt;/code&gt; — extract tables, join graph, filter set, measure set, GROUP BY), compute a semantic-match score against expected, and gate the merge on &lt;code&gt;exact_match_rate &amp;lt; 90%&lt;/code&gt; OR &lt;code&gt;semantic_match_rate &amp;lt; 95%&lt;/code&gt;. Track weekly: avg confidence, refusal rate (safety gate), user-flag rate (wrong-answer button clicks), and golden-question regression rate. Publish the dashboards. The professional signal is "we run the golden-question suite in CI and block merges below threshold" — candidates who name this pattern get graded senior; candidates who don't get graded mid.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are these assistants safe for regulated data?
&lt;/h3&gt;

&lt;p&gt;Yes, with the same discipline any production analytics workload requires. The safety story rests on three legs: (1) &lt;strong&gt;grounded generation&lt;/strong&gt; bounds the hypothesis space — the LLM cannot generate SQL against tables or measures not declared in the YAML / manifest / UC metadata; (2) &lt;strong&gt;caller-identity execution&lt;/strong&gt; means every row-access policy, column mask, and grant on the underlying warehouse tables binds automatically to the generated SQL; (3) &lt;strong&gt;safety gates&lt;/strong&gt; in the application layer refuse DDL/DML, low-confidence responses, and unbounded queries before execution. For HIPAA, PCI-DSS, SOC2, and GDPR workloads: put column masks on every PII column, define row-access policies for tenant / region isolation, scope Genie spaces / Cortex semantic models to the minimum tables required for the use case, log every request + response for audit, and run the golden-question harness on every change. The pattern to reject is a generic LLM wrapper with a shared service account — it fails the caller-identity leg by construction and cannot be retrofitted safely. Grounded vendor assistants + warehouse governance + application-layer safety gates is the auditable 2026 architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice on PipeCode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drill the &lt;a href="https://pipecode.ai/explore/practice/topic/sql" rel="noopener noreferrer"&gt;SQL practice library →&lt;/a&gt; for the semantic-layer, text-to-SQL, and grounded-generation problems senior interviewers love.&lt;/li&gt;
&lt;li&gt;Rehearse the &lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;SQL-generation practice library →&lt;/a&gt; for natural-language-to-SQL, semantic-diff scoring, and golden-question harness patterns.&lt;/li&gt;
&lt;li&gt;Sharpen the analytics axis with the &lt;a href="https://pipecode.ai/explore/practice/topic/data-analysis" rel="noopener noreferrer"&gt;data-analysis practice library →&lt;/a&gt; for business-user question decomposition and metric-definition scenarios.&lt;/li&gt;
&lt;li&gt;Stack the prerequisites against PipeCode's broader 450+ data-engineering catalogue to anchor the three-vendor decision matrix against real graded inputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Lock in LLM-assistant muscle memory&lt;/h3&gt;

&lt;p&gt;Docs explain features. PipeCode drills explain the decision — when dbt Copilot's manifest goes stale, when Cortex Analyst's YAML drifts from the warehouse, when Genie's answer quality is limited by column-comment coverage, when a golden-question harness earns its place in CI. &lt;a href="https://pipecode.ai/" rel="noopener noreferrer"&gt;Pipecode.ai&lt;/a&gt; is Leetcode for Data Engineering — pattern-first practice tuned for the production trade-offs senior analytics and data engineers actually face when picking, stacking, and evaluating LLM assistants.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pipecode.ai/explore/practice/topic/sql-generation" rel="noopener noreferrer"&gt;Practice SQL-generation problems →&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pipecode.ai/explore/practice/topic/data-analysis" rel="noopener noreferrer"&gt;Practice data-analysis problems →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>interview</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
