Skip to content

feat: value-safe professionalize-my-sheet makeover - #60

Open
Ocean82 wants to merge 11 commits into
mainfrom
feat/sheet-makeover
Open

Ocean82 wants to merge 11 commits into
mainfrom
feat/sheet-makeover

Conversation

@Ocean82

@Ocean82 Ocean82 commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds a value-safe "professionalize my sheet" makeover: a chat request like "make this look professional" / "clean up the formatting" / "format this like a pro" produces a whole-sheet styling pass that writes zero cell data.

Value-safe split

  • Default makeover = format-only execute_macro pass. The plan builder emits ONLY allowlist tools (format_cells, set_column_width, set_row_height, auto_fit, rename_header ΓÇö the AUTO_APPLY_LOW_RISK_TOOLS set). It emits NO set_cell / set_range / style_recipe / apply_formula step, so every emitted step is format-only (preview changes carry newValue: null / newFormula: undefined).
  • execute_macro is preview-first. It is intentionally not in toolRegistry, so the agent-parser stage special-cases it into an Apply/Reject preview (handleMacroPreview) instead of running it inline. shouldAutoApply('execute_macro', 'auto') is false, so the whole-sheet makeover always previews ΓÇö it is never auto-applied.
  • Totals are a separate, opt-in, preview-only step. The data-writing totals path (style_recipe: total_row, which writes a Total label + SUM formulas) is not part of the makeover macro. It is surfaced only as a distinct follow-up suggestion in the response message and is triggered by its own phrase ("add a totals row"). It stays off the auto-apply allowlist, remains in highImpactTools, and keeps its persistence caveat ΓÇö no makeover code path can reach it automatically.

Role → number-format mapping

A new pure function roleToNumberFormat(role, purpose) over existing inferRole / detectPurpose outputs (no inference changes):

ColumnRole detectedPurpose numberFormat
date any date
percentage any percent
quantity any number-int
amount budget / invoice / inventory / sales currency
amount generic number
id / category / label / description / unknown any (left as text ΓÇö no step)

An already-styled-sheet guard (spec §3) skips emitting a format_cells step when a cell already carries the relevant format, keeping the makeover "match and extend" and re-runs near-idempotent.

Testing

  • npm run typecheck ΓÇö Γ£à clean (tsc --noEmit)
  • npm run lint ΓÇö Γ£à clean (eslint .)
  • npm run test (vitest) ΓÇö Γ£à 174 files, 2252 tests passed

New/extended tests lock the value-safe invariants: every emitted makeover step is on the low-risk allowlist (no set_cell/set_range/apply_formula/style_recipe), the execute_macro preview yields zero changes with a non-null value/formula, and both execute_macro and style_recipe are shouldAutoApply(..., 'auto') === false.

Doc/code discrepancies recorded in plan.md

  1. Header freeze deferred — no freeze-pane tool in the allowlist. Spec §2 allows "freeze the header row if supported," but there is no freeze-pane tool in toolRegistry / AUTO_APPLY_LOW_RISK_TOOLS. Freeze is skipped as out of value-safe allowlist scope and noted as deferred.
  2. Semantic router intentionally not expanded for kind: 'recipe'. The professionalize_sheet catalog entry (spec §4) is added for discoverability, but semanticCapabilityRouter.ts still returns null for kind: 'recipe'. The makeover resolves via the Tier-1 regex (which runs before Tier-2), so a high-scoring paraphrase defers to Tier-1/LLM rather than emitting a steps-less execute_macro. The router was deliberately left unexpanded (mature-codebase stability); the embedded-but-unrouted entry is a subtle invariant to preserve if the router is ever expanded.

Task artifacts under .agents/tasks/ are intentionally gitignored and not part of this PR.

Summary by Sourcery

Add a preview-first, value-safe professionalization workflow that formats an entire sheet without changing its data.

New Features:

  • Add a value-safe whole-sheet professionalization flow that applies formatting and layout changes without modifying cell values or formulas.
  • Support professionalization requests through dedicated phrase detection and preview-first macro actions, with totals offered separately as an explicit opt-in.
  • Apply role- and sheet-purpose-based number formats while preserving existing compatible formatting.

Bug Fixes:

  • Prevent makeover previews from appearing to delete data by preserving current values and formulas.
  • Ensure layout-only and large or overlapping makeover plans produce bounded, non-empty previews.
  • Keep macro formatting, rollback, and undo history scoped to the sheet targeted when Apply begins.
  • Correct formatting for sheets with detected headers below the first row and partially styled headers.
  • Reject negated or non-command professionalization requests.

Enhancements:

  • Keep narrow formatting, table, and totals requests on their existing routes while deferring profile-dependent makeover planning when required context is unavailable.

Tests:

  • Add coverage for value safety, preview behavior, routing, number-format inference, idempotent styling, sheet scoping, rollback, and auto-apply restrictions.

Review-fix pass: value-safe "professionalize my sheet" makeover

Addresses the 11 review findings against the value-safe sheet-makeover feature on feat/sheet-makeover. Each finding was verified against current code; valid issues were fixed and test-only findings were hardened. The value-safe contract is preserved throughout: the default makeover writes zero cell data, totals stay opt-in / preview-only, execute_macro never auto-applies, and one macro = one undo entry using the PR#57 undo-id toast-guard pattern.

Fixes per cluster

Preview correctness — findings #1 / #4 / #6 (src/lib/previewBuilders.ts)

  • naming fix #4 Format preview looked like data deletion. previewExecuteMacro no longer emits newValue: null; it preserves each cell's existing value and formula so a format-only makeover renders as a format change, not a blanking of data, in the Apply/Reject diff.
  • feat: streaming AI upgrade with cloud-first provider failover #1 Layout-only plans applied unseen. Layout steps (auto_fit / set_column_width / set_row_height) now emit a value-preserving anchor change, so a layout-only plan still produces a non-empty preview and cannot be applied without being shown.
  • feat: upgrade default LLM to Groq Llama-3.3-70B-Versatile #6 Large/overlapping ranges stalled the preview. Cells are deduplicated across overlapping ranges and capped at MAX_PREVIEW_CELLS = 200 with a single value-preserving summary row, bounding memory and UI cost.

Macro execution scope — findings #2 / #3 (src/store/aiExecution.ts)

Plan building — findings #5 / #8 (src/lib/sheetMakeover.ts, src/ai/sheetProfile.ts)

Phrase gating — finding #9 (shared/spreadsheetPhrases.ts)

  • refactor: unified tool registry and agent executor #9 Negated requests triggered the makeover. Negated / non-command phrasing ("don't professionalize this") is rejected before an execute_macro makeover action is constructed, so Apply/Reject is no longer presented for a request that says not to apply it.

Trigger ordering & invariants — findings #7 / #10 / #11 (test hardening)

Preserved value-safe invariants

  • Default makeover writes zero cell data (value-safety assert on the plan unchanged).
  • Totals recipe remains opt-in and preview-only.
  • execute_macro never auto-applies and stays off the tool registry / out of DESTRUCTIVE_TOOLS.
  • One macro = one undo entry, scoped to the target sheet, using the PR#57 undo-id toast guard.

Verification

  • npm run typecheck — green
  • npm run lint — green
  • npm run test — 176 files / 2267 tests passing

Commits FEAT-001..FEAT-005: f6abcff, e3c1180, cc1c38a, 9d5b0b0, eeb4911.

Add src/lib/sheetMakeover.ts exporting roleToNumberFormat and
buildMakeoverPlan, which emits only format-only/layout steps from the
AUTO_APPLY_LOW_RISK_TOOLS allowlist (header styling, per-column
numberFormat with the spec section 3 already-styled guard, auto_fit, and
one optional negative-amount emphasis). An in-function assert enforces
value-safety. Add an execute_macro branch to buildActionPreview that
flattens format_cells steps into format-only CellChange[] (newValue null,
newFormula undefined). Covered by new vitest suites.
Add parseProfessionalizePhrase and route general 'make it professional' phrases to a single execute_macro built from buildMakeoverPlan, placed before the narrow format_as_table/style_recipe recipes so specific phrases still defer. Surface the totals row as a separate opt-in data-writing follow-up; the macro itself stays format-only. Veto question-framed requests via execute_macro in DESTRUCTIVE_TOOLS, and defer the local actTemplates fallback (no live profile) to the pipeline path.
…otals test

Add the spec section 4 professionalize_sheet capability (kind recipe, tool execute_macro) to shared/capabilities.js for catalog discoverability; it is resolved by the Tier-1 regex parser, not the semantic router, which is left unchanged. Extend applyPosture.test.ts to prove style_recipe and execute_macro stay preview-only and off the auto-apply allowlist.
Copilot AI balanced review requested due to automatic review settings October 16, 2026 20:54

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fdb4fb29-7ab4-4718-a55a-b4c88472fcad
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Introduces a preview-first, value-safe “professionalize my sheet” workflow that builds whole-sheet formatting-only macros from live sheet profiles, preserves existing command routing, keeps totals explicitly opt-in, and verifies zero cell-data writes across unit and integration coverage.

Sequence diagram for the value-safe sheet makeover preview

sequenceDiagram
    actor User
    participant AgentParser as AgentParser
    participant Parser as parseMessage
    participant Makeover as buildMakeoverPlan
    participant Preview as handleMacroPreview
    participant Response as buildActionPreview
    participant Store as executeMacroAction

    User->>AgentParser: make this look professional
    AgentParser->>Parser: parseMessage(message, sheetContext)
    Parser->>Makeover: buildMakeoverPlan(sheet, profile, getComputedValue)
    Makeover-->>Parser: format-only steps
    Parser-->>AgentParser: execute_macro(steps)
    AgentParser->>Preview: handleMacroPreview(call, explanation)
    Preview->>Response: buildActionPreview(execute_macro, steps)
    Response-->>User: format-only preview with Apply or Reject
    alt User chooses Apply
        User->>Store: executeMacroAction(steps)
        Store-->>User: apply one undo group
    else User chooses Reject
        User-->>AgentParser: reject preview
    end
Loading

Flow diagram for professionalize request routing and safety boundaries

flowchart TD
    A[Professionalize request] --> B{parseProfessionalizePhrase}
    B -->|General whole-sheet phrase| C{Live sheet, profile, and value accessor?}
    C -->|No| D[Defer to LLM]
    C -->|Yes| E[buildMakeoverPlan]
    E --> F[execute_macro with format-only steps]
    F --> G[Preview first]
    G --> H{User confirms?}
    H -->|Apply| I[Formatting changes only]
    H -->|Reject| J[No changes]
    B -->|Narrow recipe phrase| K[Existing format_as_table or style_recipe routing]
    K --> L[Totals remain separate and preview-only]
Loading

Flow diagram for role-based number formatting

flowchart LR
    A[ColumnRole plus detectedPurpose] --> B{roleToNumberFormat}
    B -->|date| C[date]
    B -->|percentage| D[percent]
    B -->|quantity| E[number-int]
    B -->|amount and budget/invoice/inventory/sales| F[currency]
    B -->|amount and generic| G[number]
    B -->|text roles| H[No format step]
Loading

File-Level Changes

Change Details Files
Adds a value-safe whole-sheet professionalization flow that generates only formatting and layout macro steps.
  • Recognizes broad professionalization phrases while preserving routing for table-formatting and style-recipe phrases.
  • Builds plans for header styling, role- and purpose-based number formats, auto-fit layout, conditional negative-value emphasis, and already-styled guards.
  • Enforces that generated steps belong to the low-risk formatting allowlist and excludes totals/data-writing operations.
src/lib/sheetMakeover.ts
src/lib/sheetMakeover.test.ts
shared/spreadsheetPhrases.ts
shared/spreadsheetPhrases.test.ts
shared/actTemplates.ts
src/agent/parser.ts
src/agent/parser.makeover.test.ts
Routes the makeover through a preview-first execute_macro action without inline execution or automatic application.
  • Adds live sheet, profile, and computed-value context to agent parsing.
  • Special-cases execute_macro into an Apply/Reject preview because it is absent from toolRegistry.
  • Keeps execute_macro and style_recipe out of automatic application and leaves the totals-row recipe as an explicit follow-up suggestion.
  • Defers unsupported minimal-context and question-framed requests instead of guessing or creating empty macros.
src/ai/pipeline/stages/agentParser.ts
src/ai/pipeline/__tests__/agentParser.makeover.integration.test.ts
src/lib/applyPosture.test.ts
shared/capabilities.js
src/ai/pipeline/stages/semanticCapabilityRouter.ts
src/ai/pipeline/__tests__/semanticCapabilityRouter.test.ts
Adds format-only preview generation for macro actions, guaranteeing that previews do not propose cell value or formula changes.
  • Flattens format_cells macro steps into CellChange entries while ignoring layout-only steps.
  • Represents preview changes with null newValue and undefined newFormula.
  • Returns no preview for empty macros or macros containing only layout operations.
src/lib/previewBuilders.ts
src/lib/previewBuilders.test.ts
Expands regression coverage for routing, value safety, formatting semantics, preview behavior, and end-to-end pipeline handling.
  • Tests phrase matching and protection of existing narrow routes.
  • Verifies role-to-number-format mappings and currency versus generic numeric formatting.
  • Verifies allowlist-only plans, idempotent formatting guards, non-empty format-only previews, and no inline execution.
src/lib/sheetMakeover.test.ts
src/agent/parser.makeover.test.ts
src/ai/pipeline/__tests__/agentParser.makeover.integration.test.ts
src/lib/previewBuilders.test.ts
shared/spreadsheetPhrases.test.ts
src/lib/applyPosture.test.ts
src/ai/pipeline/__tests__/semanticCapabilityRouter.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

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

Hey - I've found 12 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/lib/previewBuilders.ts" line_range="166" />
<code_context>
+export function previewExecuteMacro(sheet: SheetData, steps: ActionStep[]): CellChange[] {
+  const changes: CellChange[] = []
+  for (const step of steps) {
+    if (step.tool !== 'format_cells') continue // layout steps have no cell changes
+    const range = step.params?.range
+    if (typeof range !== 'string' || !range.trim()) continue
</code_context>
<issue_to_address>
**Changes apply without a preview**

When an `execute_macro` contains non-formatting steps or only layout steps, `previewExecuteMacro` skips non-formatting steps, while `executeMacroAction` executes them. Layout-only plans also produce no preview, yet `handleMacroPreview` still returns an applicable action. The user can therefore apply changes that the preview does not show, or apply a layout change with no preview.

Make the preview cover every executable step, or prevent the action from being applied when its effects cannot be previewed.

Also at `src/ai/pipeline/stages/agentParser.ts:236-237`.
</issue_to_address>

### Comment 2
<location path="src/ai/pipeline/stages/agentParser.ts" line_range="113-115" />
<code_context>
+      // from buildActionPreview. It never auto-applies and is never executed
+      // inline here; the store's executeMacroAction runs it as one undo group on
+      // Apply.
+      if (parsed.calls.length === 1 && parsed.calls[0].tool === 'execute_macro') {
+        return handleMacroPreview(parsed.calls[0], parsed.explanation)
+      }
+
       // ─── Ambiguity clarification (understood but no calls) ──────────────
</code_context>
<issue_to_address>
**Concurrent edits are lost**

When the workbook is edited while a macro runs and a later step fails or the user undoes a successful run, `executeMacroAction` snapshots the whole workbook, so failure restores the old snapshot and discards the concurrent edit; a successful run can include that edit in its undo diff, so undoing the macro also erases it.

Prevent concurrent edits from being included in the macro snapshot, rollback, or undo diff.
</issue_to_address>

### Comment 3
<location path="src/ai/pipeline/stages/agentParser.ts" line_range="113-115" />
<code_context>
+      // from buildActionPreview. It never auto-applies and is never executed
+      // inline here; the store's executeMacroAction runs it as one undo group on
+      // Apply.
+      if (parsed.calls.length === 1 && parsed.calls[0].tool === 'execute_macro') {
+        return handleMacroPreview(parsed.calls[0], parsed.explanation)
+      }
+
       // ─── Ambiguity clarification (understood but no calls) ──────────────
</code_context>
<issue_to_address>
**Mid-run sheet switch redirects formatting**

When the user switches the active sheet between awaited makeover steps, `executeMacroAction` creates a step executor that rebuilds its execution context before every step, and the formatting setters target the currently active sheet. If the user switches sheets while the multi-step makeover is running, later steps apply the original sheet’s ranges to the newly active sheet, so the wrong sheet is formatted.

Capture the target sheet ID when Apply begins and make every macro step operate on that sheet, or prevent sheet switching until the macro completes.
</issue_to_address>

### Comment 4
<location path="src/lib/previewBuilders.ts" line_range="175" />
<code_context>
+      const current = sheet.cells[cellId]
+      changes.push({
+        cell: cellId,
+        oldValue: current?.value ?? null,
+        newValue: null, // format-only: value is untouched
+        oldFormula: current?.formula,
</code_context>
<issue_to_address>
**Format preview looks like data deletion**

When a pending makeover formats populated cells, `previewExecuteMacro` sets `newValue` to `null` without representing the requested format change. `GridCell` renders that pending value as empty, and the Apply/Reject diff shows existing values as blank, making a format-only change look like data deletion.

Represent the formatting change in the preview without replacing the existing value or formula.

Also at `src/lib/previewBuilders.ts:176-179`.
</issue_to_address>

### Comment 5
<location path="src/lib/sheetMakeover.ts" line_range="93" />
<code_context>
+  }
+
+  const { headerRow, startCol, endCol, endRow } = range
+  const purpose = profile.detectedPurpose
+  const theme = themeForPurpose(purpose)
+  const dataStartRow = headerRow + 1
</code_context>
<issue_to_address>
**Formats use the wrong table columns**

When the detected table header is below row 0, `buildMakeoverPlan` uses `detectTableRange` for the header row but takes `profile.columns` and `profile.detectedPurpose` from `buildSheetProfile`, which assumes headers are on row 0. The resulting roles and purpose do not describe the detected table, so its data columns get missing or incorrect number formats.

Infer the column roles and purpose from the detected header row and use them for the makeover's format decisions.

Also at `src/lib/sheetMakeover.ts:94`, `src/lib/sheetMakeover.ts:126-132`.
</issue_to_address>

### Comment 6
<location path="src/lib/sheetMakeover.ts" line_range="157-161" />
<code_context>
+
+  if (!headerFullyStyled) {
+    const headerRange = `${colToLetter(startCol)}${headerRow + 1}:${colToLetter(endCol)}${headerRow + 1}`
+    steps.push({
+      tool: 'format_cells',
+      params: {
+        range: headerRange,
+        bold: true,
+        bgColor: theme.headerBg,
+        fontColor: theme.headerFontColor,
+        borders: { bottom: `2px solid ${theme.accentColor}` },
+      },
+      description: `Style the header row (${headerRange}) bold with a ${theme.name.toLowerCase()} fill`,
+    })
+  }
+
</code_context>
<issue_to_address>
**No-op steps roll back formatting**

When no rows need auto-fitting or an amount column has no negative values, `buildMakeoverPlan` still adds the `auto_fit` or negative-emphasis step. `handleAutoFit` and `applyFormatCells` report these no-ops as failures, and `executeMacro` rolls back the earlier formatting, so the makeover fails without leaving its styling in place.

Omit inapplicable steps or treat these expected no-op results as successful.

Also at `src/lib/sheetMakeover.ts:165-180`.
</issue_to_address>

### Comment 7
<location path="src/lib/previewBuilders.ts" line_range="171-173" />
<code_context>
+    if (typeof range !== 'string' || !range.trim()) continue
+    const cellIds = expandRange(range, sheet)
+    if (!cellIds) continue
+    for (const cellId of cellIds) {
+      const current = sheet.cells[cellId]
+      changes.push({
+        cell: cellId,
+        oldValue: current?.value ?? null,
</code_context>
<issue_to_address>
**Preview highlights the wrong cells**

When the negative-emphasis range contains positive or zero amounts, `previewExecuteMacro` expands the whole conditional-format range without evaluating its condition, so the preview marks non-negative cells as highlighted even though Apply formats only matching negative cells.

Evaluate the conditional-format condition when building the preview and include only cells that match.
</issue_to_address>

### Comment 8
<location path="src/lib/previewBuilders.ts" line_range="169-173" />
<code_context>
+    if (step.tool !== 'format_cells') continue // layout steps have no cell changes
+    const range = step.params?.range
+    if (typeof range !== 'string' || !range.trim()) continue
+    const cellIds = expandRange(range, sheet)
+    if (!cellIds) continue
+    for (const cellId of cellIds) {
+      const current = sheet.cells[cellId]
+      changes.push({
+        cell: cellId,
+        oldValue: current?.value ?? null,
</code_context>
<issue_to_address>
**Large ranges stall the preview**

When a makeover formatting range covers a very large or sparse sheet extent, `previewExecuteMacro` expands the range and allocates a `CellChange` for every cell, consuming excessive memory and slowing or stalling the response UI; overlapping ranges also duplicate preview entries.

Bound or compact the preview changes and deduplicate cells across overlapping ranges.
</issue_to_address>

### Comment 9
<location path="src/ai/pipeline/stages/agentParser.ts" line_range="107" />
<code_context>
+      // ─── execute_macro preview/confirm flow ─────────────────────────────
+      // execute_macro (the value-safe sheet makeover) is intentionally NOT in
+      // toolRegistry, so executeToolCalls would see category=undefined, treat it
+      // as a read, and run it inline — where executeTool returns
+      // "Unknown tool: execute_macro". It must instead be returned as an
+      // Apply/Reject action so responseBuilder attaches the format-only preview
</code_context>
<issue_to_address>
**Makeover gets a destructive warning**

When a professionalize request produces an `execute_macro` action, the parser checks `DESTRUCTIVE_TOOLS` before its macro-preview branch, so `handleDestructivePreview` handles the format-only makeover and tells the user it will remove all sheet data.

Keep `execute_macro` out of `DESTRUCTIVE_TOOLS` or route it to the macro-preview branch before destructive-tool handling.

Also at `src/agent/parser.ts:82`.
</issue_to_address>

### Comment 10
<location path="src/lib/sheetMakeover.ts" line_range="63-66" />
<code_context>
+}
+
+/** Does the header-row cell already carry every key the makeover would set? */
+function headerAlreadyStyled(fmt: CellFormat | undefined, theme: (typeof TABLE_THEMES)[string]): boolean {
+  if (!fmt) return false
+  return Boolean(fmt.bold) && fmt.bgColor === theme.headerBg && Boolean(fmt.borders?.bottom)
+}
+
+/**
</code_context>
<issue_to_address>
**Partly styled headers are left unchanged**

When header cells match the target bold and fill but have a different font color or bottom-border style, `headerAlreadyStyled` accepts any bottom border and does not check the requested font color or border styling. `buildMakeoverPlan` therefore skips the header-format step and leaves the headers inconsistently styled.

Have `headerAlreadyStyled` compare every header-format property the makeover sets.
</issue_to_address>

### Comment 11
<location path="shared/spreadsheetPhrases.ts" line_range="338" />
<code_context>
+  }
+
+  // "professionalize (this|my sheet)"
+  if (/\bprofessionali[sz]e\b/.test(lower)) return true
+
+  // "clean up the formatting" / "clean up this sheet"
</code_context>
<issue_to_address>
**Negated requests trigger makeover**

When the message contains “professionalize” in a negated instruction, the bare `professionali[sz]e` match accepts negated or descriptive text such as “don’t professionalize this,” so `parseMessage` builds an `execute_macro` action and presents an Apply/Reject makeover for a request that explicitly says not to apply it.

Reject negated/non-command phrasing before constructing the makeover action.
</issue_to_address>

### Comment 12
<location path="src/lib/sheetMakeover.test.ts" line_range="9" />
<code_context>
+import { buildSheetProfile } from '@/ai/sheetProfile'
+
+// Tools that write cell values/formulas — the makeover macro must emit none.
+const DATA_WRITING_TOOLS = ['set_cell', 'set_range', 'apply_formula', 'style_recipe', 'clear_sheet', 'modify_column']
+
+function makeSheet(cells: Record<string, CellData>): SheetData {
</code_context>
<issue_to_address>
**Header rewrites pass value-safety checks**

When a plan emits `rename_header`, a makeover step using `rename_header` passes the allowlist assertion and is absent from `DATA_WRITING_TOOLS`, so these tests accept a plan that changes a header value. `handleRenameHeader` calls `setCellValue`, making the claimed zero-data-write guarantee untested for this tool.

Add `rename_header` to the data-writing denylist and assert it is never emitted.

Also at `src/agent/parser.makeover.test.ts:17`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 12 findings to address first, and if the makeover chooses inappropriate number formats or styling, applying it changes the sheet’s saved formatting; reverting the code would not restore sheets already changed. The damage is limited to formatting and can be repaired, and the preview/Apply flow keeps the plan from changing cell values or formulas.

Blocking findings: src/lib/previewBuilders.ts:166, src/ai/pipeline/stages/agentParser.ts:115, src/ai/pipeline/stages/agentParser.ts:115, src/lib/previewBuilders.ts:175, src/lib/sheetMakeover.ts:93, and 7 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/lib/previewBuilders.ts Outdated
export function previewExecuteMacro(sheet: SheetData, steps: ActionStep[]): CellChange[] {
const changes: CellChange[] = []
for (const step of steps) {
if (step.tool !== 'format_cells') continue // layout steps have no cell changes

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High · Changes apply without a preview

When an execute_macro contains non-formatting steps or only layout steps, previewExecuteMacro skips non-formatting steps, while executeMacroAction executes them. Layout-only plans also produce no preview, yet handleMacroPreview still returns an applicable action. The user can therefore apply changes that the preview does not show, or apply a layout change with no preview.

Make the preview cover every executable step, or prevent the action from being applied when its effects cannot be previewed.

Also at src/ai/pipeline/stages/agentParser.ts:236-237.

Prompt for AI agents
In `src/lib/previewBuilders.ts` at line 166:

**Changes apply without a preview**

When an `execute_macro` contains non-formatting steps or only layout steps, `previewExecuteMacro` skips non-formatting steps, while `executeMacroAction` executes them. Layout-only plans also produce no preview, yet `handleMacroPreview` still returns an applicable action. The user can therefore apply changes that the preview does not show, or apply a layout change with no preview.

Make the preview cover every executable step, or prevent the action from being applied when its effects cannot be previewed.

Also at `src/ai/pipeline/stages/agentParser.ts:236-237`.

Fix in Cursor

Comment thread src/ai/pipeline/stages/agentParser.ts
Comment thread src/ai/pipeline/stages/agentParser.ts
Comment thread src/lib/previewBuilders.ts Outdated
Comment thread src/lib/sheetMakeover.ts Outdated
Comment thread src/lib/previewBuilders.ts Outdated
Comment thread src/ai/pipeline/stages/agentParser.ts
Comment thread src/lib/sheetMakeover.ts
Comment thread shared/spreadsheetPhrases.ts
Comment thread src/lib/sheetMakeover.test.ts Outdated
previewExecuteMacro now preserves each cell value+formula instead of emitting newValue:null, so a format-only makeover no longer looks like data deletion in the Apply/Reject diff (#4). Layout steps (auto_fit/set_column_width/set_row_height) emit a value-preserving anchor change so a layout-only plan still produces a non-empty preview and cannot apply unseen (#1). Cells are deduped across overlapping ranges and capped at MAX_PREVIEW_CELLS=200 with a single preserving summary row (#6). Updated the existing no-data-write tests to the preserve contract and added previewBuilders.makeover.test.ts.
Capture the active sheet id when a makeover Apply begins and thread it through buildExecutionContext so every formatting/layout step targets that sheet even if the user switches tabs mid-run (#3). Scope the macro's undo diff and failure rollback to the target sheet so a concurrent edit on another sheet is neither captured in the macro's single undo entry nor erased on rollback (#2). Preserves one-macro=one-undo-entry, the PR#57 undo-id toast guard, and execute_macro staying off the tool registry.
#8)

Export inferRole+detectPurpose from sheetProfile; buildMakeoverPlan re-infers column roles and sheet purpose from detectTableRange's header row instead of the row-0 profile. headerAlreadyStyled now compares every property the header step sets (bold, bgColor, fontColor, exact 2px bottom border). Value-safety assert unchanged.
The FEAT-004 import in shared/spreadsheetPhrases.ts omitted the .js extension, which the client bundler tolerates but the server tsc (moduleResolution nodenext) rejects with TS2835. All other non-test relative imports in shared/ already use .js; this aligns with them.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants