Skip to content

feat: end-of-task column impact, an offline report with a column graph, and /lineage-report for any column - #21

Open
ralphstodomingo wants to merge 22 commits into
mainfrom
feat/column-impact-report
Open

ralphstodomingo wants to merge 22 commits into
mainfrom
feat/column-impact-report

Conversation

@ralphstodomingo

@ralphstodomingo ralphstodomingo commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

What changes for users

  • What a change reaches, at the end of the task. When Claude finishes a task that changed a dbt model, a summary above the prompt says how far each changed column's value goes downstream. For example: 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.
  • An offline report with a column graph. Open report or /lineage-report opens 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.
  • Any column, without a task. /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.
  • Plans. When a plan goes up for approval, one line names the models it touches, with their downstream counts.

How it works

  • At the start of a task the plugin copies target/manifest.json aside. At the end it asks impact_analysis about each changed column through the altimate CLI, so the answer describes the SQL as it was before the task.
  • The page is one file and makes no requests; its CSP allows only its own inline script and style.
  • A graph edge carries a label only when the engine says how the value changes (same, renamed, calculated, aggregated). Edges it can't classify are dashed with no label, and a note under the graph says what that means. A click on a column counts even if the pointer drifts up to 12 px; a longer drag pans.
  • The graph reads on first view: its key names every mark it draws (exposures, engine notes, 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.
  • The per-edit column lookup goes through the CLI instead of MCP, so it raises no permission prompt in accept-edits mode.
  • Usage events for the report go through altimate-code's telemetry (tool report-event): the action and the session id only, off under the telemetry opt-out. See PRIVACY.md.

What it needs

Claims (for review)

  • C1. With an altimate-code whose impact_analysis gives neither column_answer nor column_answer_error, the summary and report keep the model-level count and say to update altimate-code, never that the trace failed; nothing throws.
  • C2. The report page makes no request: its CSP is default-src 'none' with inline script and style only, and it names no external URL.
  • C3. /lineage-report with no argument opens the last task's report as before. /lineage-report <model>.<column> writes column-report.html and never overwrites the task's report; an unknown model or column, or a malformed argument, gives a plain error and writes nothing.
  • C4. A graph edge is labelled only for =, ~, ƒ and Σ; any other value is dashed and unlabelled.
  • C5. A press and release on the same column within 12 px selects it once, never twice; a longer drag pans and selects nothing; keyboard activation still selects.
  • C6. When the copy at the task's start succeeded, the end-of-task answer reads that copy, not the manifest the task left behind.
  • C7. A usage event carries the action and the session id only.
  • C8. The per-edit column note never calls MCP; it runs the altimate CLI.

Residuals:

  • R1. Usage events need altimate-code's tool report-event (a separate PR); until then the call exits 1 and nothing is sent.
  • R2. A level of more than 12 models folds into one box; two levels each way by default.
  • R3. A model with about 100 or more downstream models gets a model-level note instead of column detail.
  • R4. When that copy failed, the answer reads the current manifest.
  • R5. Each session's copies are its own and are removed a day after they were made; a report asked for after that reads the project's current manifest and says so.

Held for Ralph

  • Codex round 3 (the cap) on 6947714: two tasks in one session can overlap when a queued prompt starts the next turn before the previous task's end step has moved its manifest copy. If Claude recompiled during the earlier task, that task's answer then reads the later copy. The fix is a per-task part in the copy names; it is held at the round cap, not made.

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. tsc and claude plugin validate pass.
  • Live at 64d63e4, in an isolated Claude Code 2.1.322 on a 233-model dbt project with altimate-code at the PR above: three runs of edit, summary, report, column click, more levels, Fit and /lineage-report, with the same numbers each time.
  • In headless Chromium with real pointer events, clicks with 0, 4 and 10 px of drift light the path and 20 px drags pan. The page before the last commit drops the 10 px click.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VEmKQwEiHMwQ7BdwgPo9sb

ralphstodomingo and others added 18 commits November 5, 2026 20:58
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
- 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
@ralphstodomingo ralphstodomingo self-assigned this Nov 11, 2026
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

@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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T05:13:30.763598Z 6947714 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +209 to +211
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) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

@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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +122 to +123
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}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

@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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +128 to +130
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@ralphstodomingo
ralphstodomingo marked this pull request as ready for review November 12, 2026 05:19
.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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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(' ')});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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}});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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).

@kilo-code-bot

kilo-code-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 7 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 4
Issue Details (click to expand)

WARNING

File Line Issue
plugins/altimate-code/hooks/impact-report.ts 235 Band can print "Every downstream model was read" when counts.unresolved_models is set without an unresolved array, contradicting the report's lower-bound text.
plugins/altimate-code/hooks/task-impact.tsx 314 lastTask resolves the project by walking up from the session root while finishTask saves under changes[0].root; from a monorepo root /lineage-report loses the saved task after a restart (and can rebuild an older task).
plugins/altimate-code/hooks/task-impact.tsx 575 Only the upward-resolved session project is prepared at turn start, so shell-made model changes in a subproject produce no changes, no band and no saved task, contrary to the README.

SUGGESTION

File Line Issue
plugins/altimate-code/hooks/report-graph.ts 162 New in 1cf43a2: the badge now comes from graphLayout, so box.contract, box.exposures and box.flags are written but never read.
plugins/altimate-code/hooks/lineage.tsx 1116 The whole ImpactNote.view payload is computed per edit but unused after the band removal (only model/column are logged).
plugins/altimate-code/hooks/report-graph.ts 155 Edge keys are packed with >/space and parsed by splitting, so a quoted column name containing either silently defeats path highlighting.
plugins/altimate-code/hooks/report-graph.ts 194 Presses are recorded for every pointer button, so right-click toggles a column highlight or expands a folded box.
Files Reviewed (6 files)
  • plugins/altimate-code/hooks/impact-report.ts - 0 new (1 carried)
  • plugins/altimate-code/hooks/impact-report.test.ts - 0 issues
  • plugins/altimate-code/hooks/report-graph.ts - 1 new (2 carried)
  • plugins/altimate-code/hooks/report-graph.test.ts - 0 issues
  • plugins/altimate-code/hooks/task-impact.tsx - targeted check only (2 carried, unchanged)
  • plugins/altimate-code/hooks/lineage.tsx - targeted check only (1 carried, unchanged)

Reviewed increment 6947714..1cf43a2 (path bar moved out of flow, graph legend/key, badge-aware name cutting, reading-order boxes, graphView first view, Tab-to-focus panning, mobile graph height). No new correctness or security defect found in the incremental changes; one redundancy noted, and the six earlier findings re-verified against current HEAD.

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

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
plugins/altimate-code/hooks/impact-report.ts 235 Band can print "Every downstream model was read" when counts.unresolved_models is set without an unresolved array, contradicting the report's lower-bound text.
plugins/altimate-code/hooks/task-impact.tsx 314 lastTask resolves the project by walking up from the session root while finishTask saves under changes[0].root; from a monorepo root /lineage-report loses the saved task after a restart (and can rebuild an older task).
plugins/altimate-code/hooks/task-impact.tsx 575 Only the upward-resolved session project is prepared at turn start, so shell-made model changes in a subproject produce no changes, no band and no saved task, contrary to the README.

SUGGESTION

File Line Issue
plugins/altimate-code/hooks/lineage.tsx 1116 The whole ImpactNote.view payload is computed per edit but unused after the band removal (only model/column are logged).
plugins/altimate-code/hooks/report-graph.ts 129 Edge keys are packed with >/space and parsed by splitting, so a quoted column name containing either silently defeats path highlighting.
plugins/altimate-code/hooks/report-graph.ts 168 Presses are recorded for every pointer button, so right-click toggles a column highlight or expands a folded box.
Files Reviewed (13 files)
  • PRIVACY.md - 0 issues
  • plugins/altimate-code/CHANGELOG.md - 0 issues
  • plugins/altimate-code/README.md - 0 issues
  • plugins/altimate-code/hooks/impact-report.test.ts - 0 issues
  • plugins/altimate-code/hooks/impact-report.ts - 1 issue
  • plugins/altimate-code/hooks/integrations.tsx - 0 issues
  • plugins/altimate-code/hooks/lineage.test.tsx - 0 issues
  • plugins/altimate-code/hooks/lineage.tsx - 1 issue
  • plugins/altimate-code/hooks/report-graph.test.ts - 0 issues
  • plugins/altimate-code/hooks/report-graph.ts - 2 issues
  • plugins/altimate-code/hooks/task-impact.test.tsx - 0 issues
  • plugins/altimate-code/hooks/task-impact.tsx - 2 issues
  • plugins/altimate-code/types/index.d.ts - 0 issues

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash-1017 · Input: 108.9K · Output: 35K · Cached: 1.4M

ralphstodomingo and others added 2 commits November 12, 2026 14:24
… 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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant