Repository navigation
feat: end-of-task column impact, an offline report with a column graph, and /lineage-report for any column - #21
ralphstodomingo wants to merge 22 commits into
Conversation
A task that changes a dbt model ends with one band: each changed column's downstream reach, read from a manifest copied at the task's start. Open report or /lineage-report opens the report. Edits no longer toast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNQjkaiykbC1WtASw6Yegs
…s it Name-matched edges carry "name" as a fourth element and show as such. Snapshots are not traced. Engine flags show on models. The band keeps Open lineage beside Open report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNQjkaiykbC1WtASw6Yegs
The Cleared group names the models altimate-code clears, and keeps the count alone when an answer has no list. Links matched by name draw dimmer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNQjkaiykbC1WtASw6Yegs
… one line On WSL, explorer.exe is tried by name, then at /mnt/c/Windows, then xdg-open; a WSL kernel is known from uname. The log line joins the summary with " · ". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNQjkaiykbC1WtASw6Yegs
…logs only to debug With a column answer, Claude's note has the reach, may-carry, cleared and model-level counts the summary shows; older builds keep the column reach. Per-edit lines leave the transcript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNQjkaiykbC1WtASw6Yegs
Reads, edits and answers still move what it shows, so `/lineage` or Open lineage opens it on the latest model. A session that never asks sees no pane at any width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNQjkaiykbC1WtASw6Yegs
Invalid JSON with exit 0 counted as ok with no metadata, so the graph lookup cached "no model" for the session and the edit's impact note silently went missing. It now reports a failure and asks again next time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNQjkaiykbC1WtASw6Yegs
…mes it The task end traces downstream for the band only. Open report or `/lineage-report` traces upstream and writes the page from what the task froze. It opens in plain words, lists dbt exposures or says none are declared, and keeps the levels and paths below. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNQjkaiykbC1WtASw6Yegs
When ExitPlanMode goes up, a line above the dialog gives each named model's downstream models, tests and exposures, and the band repeats it after. It holds nothing back and spends no model tokens. It goes by names in the plan's text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNQjkaiykbC1WtASw6Yegs
"Nothing is hidden" sat beside a bar counting columns on hidden levels, and the note opened with "from the exposures" when there are none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNQjkaiykbC1WtASw6Yegs
Kept from main: the answer-object check, auto-open with the narrow-terminal toast, stale-SQL notes, folders, the saved-file reach, failure toasts. Kept here: no per-edit band; the impact note goes to Claude and the debug log, the person gets the end-of-task summary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
- The report draws the column lineage as a graph: upstream left, downstream right, opened to fit, with zoom and pan. - A column click lights its path; "more levels" adds one; a level over 12 models folds into one box. - The page allows only its own inline script and style. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
- An edit's `impact_analysis` runs through the CLI, not MCP: no permission prompt in accept-edits mode, and a large answer arrives whole. - The pane's column reach still tries MCP first; its saved-answer and hang tests move there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
…etry - The summary shown, Open report, /lineage-report, and built, failed or no browser each send one `tool report-event` call: the step and the session id only. - PRIVACY.md describes the report and its events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
The report's graph labels an edge only when the engine says how the value changes: same, renamed, calculated or aggregated. An unknown edge stays dashed with no label, and a note under the graph says what that means. The summary's numbers don't change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
With an argument, the command traces that column both ways in the project's manifest and opens a report that leads with where the column comes from. It doesn't depend on the last task and never replaces its report. An unknown model or column gets a plain error. Without an argument, nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
A press and release on the same column, or on a folded level, within 12 px is a click. A press that moved more, or started on the canvas, still pans. Before, any move of 5 px or more while pressed made a click a drag, and nothing lit up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
|
@codex review against the numbered claims and the disclosed residuals in the "Claims (for review)" section of this PR's description: report only a reproducible trace that violates a numbered claim. Instances of the disclosed residuals are not findings. A round with no claim violation ends review. |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9f62178bfa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const meta = await ask($, done, change, col.name, 'downstream') | ||
| const down = answerOf(meta) | ||
| return { model: change.model, column: col.name, down, up: null, modelLevel: modelLevelOf(meta), failed: !down, consumers: consumersOf(meta) } |
There was a problem hiding this comment.
Fall back to column_reach in the task summary and report
C1 is violated when an older CLI returns valid column_reach without column_answer: answerOf(meta) returns null, and this code discards the reach and marks the column as failed. Reproduced with metadata {direct_count:1, transitive_count:0, column_reach:{models:[{name:'rpt_orders', columns:['revenue']}], exposures:[]}} for a change to fct_orders.revenue: passing this trace through summaryOf/summaryLines prints revenue could not be traced, and reportData produces a null reach and This direction could not be traced. instead of showing the supplied downstream reader. Preserve and consume the legacy reach when no column answer exists so both outputs provide the claimed fallback.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 462251f. You were right that C1 didn't hold. Rather than build a second summary from column_reach, an answer with neither column_answer nor column_answer_error is now read as coming from an altimate-code that predates the column answer. The summary says "no column answer: update altimate-code", and the report says to update it in both directions, keeping the model-level count. It no longer says the trace failed. C1 and "What it needs" now state this. The plugin ships with or after the altimate-code release that adds the answer. New test: task-impact.test.tsx, "with an altimate-code from before the column answer…", which fails on 9f62178.
…it", not a failed trace An altimate-code from before the column answer gives neither the answer nor its error. The summary and report now keep the model-level count and say to update altimate-code, instead of "could not be traced". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
|
@codex review against the numbered claims and the disclosed residuals in the "Claims (for review)" section of this PR's description (C1 was restated for 462251f): report only a reproducible trace that violates a numbered claim. Instances of the disclosed residuals are not findings. A round with no claim violation ends review. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 462251f54a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const copied = made?.exitCode === 0 ? await $.process.run(['cp', manifest, `${dir}/${RUNNING_MANIFEST}`], { timeoutMs: 20_000 }).catch(() => null) : null | ||
| if (copied?.exitCode === 0) snapshot = `${dir}/${RUNNING_MANIFEST}` |
There was a problem hiding this comment.
Isolate manifest snapshots by session and task
C6 is violated when two Claude sessions work in the same dbt project. Reproduced by running prepareRoot for task A with manifest A, replacing the current manifest with B, running prepareRoot for task B, then calling freezeManifests and ask for A: A's successful snapshot is overwritten at the shared manifest-before.json path, so its end-of-task answer reads manifest B. Both copies succeed, so this is not residual R4. Give each session/task its own snapshot and frozen path so another task cannot replace the manifest an answer reads.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 6947714. The start-of-task copy and the end-of-task copy now carry the session's id (manifest-before.<session>.json, task-manifest.<session>.json), so another session's task can't replace the manifest this session's answer and report read. So the copies don't pile up in target/, a task's end removes copies more than a day old; that is disclosed as R5. New test: "each session's manifest copies carry its id…", which fails on 462251f.
… task can't replace them The start-of-task and end-of-task copies carry the session's id, and a task's end removes copies a day old. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
|
@codex review against the numbered claims and the disclosed residuals in the "Claims (for review)" section of this PR's description (R5 added for 6947714): report only a reproducible trace that violates a numbered claim. Instances of the disclosed residuals are not findings. A round with no claim violation ends review. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 694771492d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const copy = `${dir}/${runningManifest(await sessionTag($))}` | ||
| const copied = made?.exitCode === 0 ? await $.process.run(['cp', manifest, copy], { timeoutMs: 20_000 }).catch(() => null) : null | ||
| if (copied?.exitCode === 0) snapshot = copy |
There was a problem hiding this comment.
Give overlapping tasks distinct manifest snapshot paths
C6 still fails when consecutive tasks overlap in the same session: taskTurnComplete schedules finishTask without awaiting it, so the next taskTurnStart can overwrite this snapshot before the previous task freezes it. Reproduced using the actual prepareRoot and freezeManifests functions with real filesystem copies: prepare task A with manifest A, replace the project's manifest with B, prepare task B using the same session ID, then freeze A; A's frozen manifest contains B. Both copies succeed and neither expires, so neither R4 nor R5 applies. The fresh evidence after 6947714 is same-session task overlap, rather than the previously reported cross-session overwrite. Include a task-specific identifier in both running and frozen snapshot paths so background answers retain their own inputs.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed: finishTask runs a few git calls before it moves the copy, so a prompt queued during the task can start the next turn first. That only changes the answer if Claude recompiled during the earlier task. This is round 3, the review cap, and the finding follows the shape of the last fix, so it is recorded under "Held for Ralph" in the description instead of fixed here. The fix would be a per-task part in both copy names.
| .filter(one => one.column !== null) | ||
| .map(one => { | ||
| const reach = one.down ? reachOf(one.down) : null | ||
| for (const entry of one.down?.unresolved ?? []) unresolved.add(one.down!.nodes[entry.model]?.id ?? one.down!.nodes[entry.model]?.name ?? String(entry.model)) |
There was a problem hiding this comment.
WARNING: The band can assert every downstream model was read while some were skipped
unresolved is filled only from the answer.unresolved array, while reachOf (line 145) takes its count from counts.unresolved_models, using that array only as a fallback; the column-answer contract allows any field to be absent. An answer with counts.unresolved_models: 2 and no unresolved array therefore yields unresolved: 0 here, so summaryLines prints ✓ Every downstream model was read (and omits the lower-bound warning) for the very trace that impactWords and the report page describe as "2 models couldn't be read". Seed this count from the computed reach.unresolved too, so the band and the report cannot contradict each other.
| /** The last task that changed a model: this session's, else the one a project's saved file names. */ | ||
| async function lastTask($: EngineInterface): Promise<Finished | null> { | ||
| if (finished) return finished | ||
| const own = await projectRoot($, await $.session.root().catch(() => '.'), false).catch(() => null) |
There was a problem hiding this comment.
WARNING: The saved task is written under one project root but looked up under another, so /lineage-report loses it after a restart in a monorepo
finishTask writes last-task.json (and removes report.html) under changes[0].root (lines 261, 280-285), but this lookup resolves the dbt project by walking up from the session root only. From a monorepo root (the README advertises running /lineage there), a turn that edits /mono/analytics/models/.../fct_orders.sql saves the record under /mono/analytics/target/.altimate-lineage/, while projectRoot('/mono') returns null, so after the next session start (which resets finished) /lineage-report answers "No column impact report yet" although the report exists one level down. The same mismatch makes a task whose changes first land in a second project save under that project while this lookup reads the first one, rebuilding an older task. Resolve the same root on both sides (e.g. record the project root in the saved record and find the newest last-task.json under the projects the pane already discovers via projectsBelow).
| task = t | ||
| await update($, summaryAtom, () => null) | ||
| await update($, planAtom, () => null) | ||
| const own = await projectRoot($, await $.session.root().catch(() => '.'), false).catch(() => null) |
There was a problem hiding this comment.
WARNING: Model changes made by a shell command are missed when the session root is not itself the dbt project
Only the upward-resolved session project is prepared here; collectChanges discovers shell-made changes solely by iterating t.roots (line 151), which is populated by prepareRoot from this call and from taskBeforeTool (edits only). From a monorepo root, a task that changes analytics/models/.../fct_orders.sql with sed -i or a script and never calls Edit/Write leaves t.roots empty, so collectChanges returns [] and finishTask returns at line 267: no band, no last-task.json, no report, although the README promises changes made "by an edit or a shell command git sees". At task end, resolve the project for each git-changed .sql file (or the projects below the session root) before collecting.
| unsettled, | ||
| maybeReaders: maybe, | ||
| readers: reach ? reach.models.length : carried.length ? new Set(carried.flatMap(one => one.carriers.map(model => model.name))).size : null, | ||
| unsettled: unsettled + carried.reduce((n, one) => n + one.reach.unresolved + one.reach.mayCarry, 0), |
There was a problem hiding this comment.
SUGGESTION: The whole view payload is now dead weight
After the per-edit lineage band was removed, nothing consumes ImpactNote.view except view.model and view.column in the debug log at line 1400; severity, direct, transitive, tests, exposures, readers, unsettled, maybeReaders, maybeExposures, dashboardsUnsettled and truncated are computed for every edit and discarded. Returning just { model: node.name, column } from impactFirst (and narrowing ImpactNote.view) would delete this block; ImpactView remains needed only for SEVERITY_COLOR.
| p.appendChild(bar); | ||
| var svg=sv('svg',{'class':'g-svg',tabindex:0,role:'group','aria-label':'Column graph: drag or use the arrow keys to move, the wheel or + and - to zoom, 0 to fit'}); | ||
| var vp=sv('g',{'class':'g-vp'}),gE=sv('g',{'class':'g-edges'}),gB=sv('g',{'class':'g-boxes'});vp.appendChild(gE);vp.appendChild(gB);svg.appendChild(vp); | ||
| lay.edges.forEach(function(e){var g=sv('g',{'class':'g-edge '+(GLYPH[e.glyph]||'unk'),'data-keys':e.keys.map(function(k){return k[0]+'>'+k[1]}).join(' ')}); |
There was a problem hiding this comment.
SUGGESTION: A space or > in a column name silently breaks edge highlighting
Keys are opaque modelId|lower(column) strings (keyOf, impact-report.ts line 350) with no sanitisation, and here they are packed as from>to joined by spaces; applyHighlight parses them back with split(' ') then split('>') (impact-report.ts line 753). dbt allows quoted identifiers, so a column such as "net revenue" produces data-keys="model.a|net revenue>model.b|net revenue", which splits into fragments that match no key and lights no edge (the pathbar's split('|') at line 755 has the same ambiguity). Encode each key (e.g. encodeURIComponent or JSON) instead of relying on separators that can appear in ids.
| svg.addEventListener('wheel',function(ev){ev.preventDefault();var r=svg.getBoundingClientRect();zoomAt(v.k*Math.exp(-ev.deltaY*0.0015),ev.clientX-r.left,ev.clientY-r.top)},{passive:false}); | ||
| var ptrs={},start=null; | ||
| function targetOf(n){var r=n&&n.closest?n.closest('.g-row'):null;if(r)return 'row:'+r.getAttribute('data-key');var b=n&&n.closest?n.closest('.g-box.collapsed'):null;return b?'box:'+b.getAttribute('data-box'):null} | ||
| svg.addEventListener('pointerdown',function(ev){ptrs[ev.pointerId]={x:ev.clientX,y:ev.clientY};moved=false;press=Object.keys(ptrs).length===1?{x:ev.clientX,y:ev.clientY,target:targetOf(ev.target)}:null;start={x:ev.clientX,y:ev.clientY,vx:v.x,vy:v.y};var ids=Object.keys(ptrs);if(ids.length===2){var a=ptrs[ids[0]],b=ptrs[ids[1]];start.d=Math.hypot(a.x-b.x,a.y-b.y);start.k=v.k}}); |
There was a problem hiding this comment.
SUGGESTION: Any mouse button arms the click detector
press is recorded for every pointer button here, and up() (line 176) turns a short same-target press/release into a click without checking ev.button. Pointer events are delivered for secondary buttons too, so a right-click on a .g-row toggles its highlight and a right-click on a collapsed box expands it while the context menu opens. Record a press only for ev.button === 0 (or ev.isPrimary && ev.button === 0).
Code Review SummaryStatus: 7 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (6 files)
Reviewed increment Fix these issues in Kilo Cloud Previous Review Summary (commit 6947714)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 6947714)Status: 6 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (13 files)
Reviewed by deepseek-v4.1-flash-1017 · Input: 108.9K · Output: 35K · Cached: 1.4M |
… down A click on a column showed the path bar in the page's flow, which moved everything under the pointer, so the tooltip then named the row above. The bar is now fixed at the top, lets the pointer through, and keeps its Clear button clickable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
… keyboard The key names every mark (exposures, engine notes, end, the changed column) and which side is upstream; each count says what it counts on hover. A narrow panel opens on the changed column at a readable zoom. Badges no longer cover names, and Tab goes left to right, bringing each column into view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb
| g.appendChild(sv('title',{},b.title)); | ||
| g.appendChild(sv('rect',{width:b.w,height:b.h,rx:8,'class':'g-rect'})); | ||
| g.appendChild(sv('text',{x:10,y:19,'class':'g-name'},b.name)); | ||
| if(b.badge)g.appendChild(sv('text',{x:b.w-10,y:19,'class':'g-badge','text-anchor':'end'},b.badge)); |
There was a problem hiding this comment.
SUGGESTION: box.contract, box.exposures and box.flags are written but never read
Now that the badge is built in graphLayout and this line draws it from b.badge, nothing consumes the per-box contract, exposures and flags (and the already-unused tests) set at line 76: their only reader was the inline badge computation this line replaces. Dropping them keeps the layout output to what the page actually draws.
What changes for users
mrt_query_usage · 39 downstream models · query_cost reaches 24 columns in 11 models (11 ends) · 13 may carry it · 15 cleared · ⚠ Lower bound: 6 models could not be read./lineage-reportopens one HTML file: what changed, what to expect downstream, who consumes it (dbt exposures), and a column graph, upstream left and downstream right, where a click lights up a column's path./lineage-report <model>.<column>opens the same page for any column, leading with where it comes from: the "this number looks wrong" question. It reads the manifest as it is now and never replaces the task's report; an unknown model or column gets a plain error.How it works
target/manifest.jsonaside. At the end it asksimpact_analysisabout each changed column through the altimate CLI, so the answer describes the SQL as it was before the task.end, the changed column), each count says what it counts, a narrow window opens on the changed column, and Tab moves left to right. The path bar floats over the page, so a click never moves what is under the pointer.tool report-event): the action and the session id only, off under the telemetry opt-out. See PRIVACY.md.What it needs
impact_analysis, with one column resolver fordbt_lineagealtimate-code#1710 (the column answer and its shared column resolver; issue [FEATURE]: column-level impact answer from impact_analysis, with the parent-column fixes it needs altimate-code#1709). Release this plugin with it or after it. With an older altimate-code, the summary and report keep the model-level count and say to update it; the pane and the per-edit note keep today's column reach.tool report-event. It comes in a separate altimate-code PR, not open yet. Until it ships, that call exits 1 and nothing is sent.Claims (for review)
impact_analysisgives neithercolumn_answernorcolumn_answer_error, the summary and report keep the model-level count and say to update altimate-code, never that the trace failed; nothing throws.default-src 'none'with inline script and style only, and it names no external URL./lineage-reportwith no argument opens the last task's report as before./lineage-report <model>.<column>writescolumn-report.htmland never overwrites the task's report; an unknown model or column, or a malformed argument, gives a plain error and writes nothing.=,~,ƒandΣ; any other value is dashed and unlabelled.Residuals:
tool report-event(a separate PR); until then the call exits 1 and nothing is sent.Held for Ralph
How it was checked
claude plugin test: 284 pass. They include the graph layout, the unlabelled edges, a click with a few pixels of drift against a drag, and/lineage-report <model>.<column>with an unknown model, an unknown column and a malformed argument.tscandclaude plugin validatepass./lineage-report, with the same numbers each time.🤖 Generated with Claude Code
https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb