Skip to content

Integrate desktop authoring line into core TMCP#474

Draft
mattcfilbert wants to merge 21 commits into
mainfrom
feature/desktop
Draft

Integrate desktop authoring line into core TMCP#474
mattcfilbert wants to merge 21 commits into
mainfrom
feature/desktop

Conversation

@mattcfilbert

Copy link
Copy Markdown
Contributor

Description

AI Assisted Authoring via MCP is coming to Tableau Desktop.

  • Conflicts resolved keeping main's build infra (tsconfig.providers step, toolName registry, feature-gate layout) and folding in desktop additions (desktop asset staging, desktop agent-check checks, README sections)
  • All 45 desktop tools carry the Required contract; hints chosen per tool semantics (execute-tableau-command destructive, cache mutators non-idempotent, readers read-only/idempotent)
  • Serialized desktop tools/list surface fits the 46k deferral-cliff budget (45,572 measured) via schema-description compression only; tool descriptions and expertise:// steering pointers unchanged from authoring
  • CI: desktop variant build step added; workflow also triggers on PRs to feature/desktop

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Other (please describe):

How Has This Been Tested?

  • Validation: tsc clean, 255 test files / 3660 passed, lint clean, web + desktop builds green

Checklist

  • I have updated the version in the package.json file by using npm run version. For example,
    use npm run version:patch for a patch version bump.
  • I have made any necessary changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have documented any breaking changes in the PR description. For example, renaming a config
    environment variable or changing its default value.

Contributor Agreement

By submitting this pull request, I confirm that:

…ng at 1e81b39)

Squash-and-diff of the desktop/authoring tool line (45 tools, knowledge corpus,
binder/template system, validation rules) onto main v2.25.0.

- Conflicts resolved keeping main's build infra (tsconfig.providers step,
  toolName registry, feature-gate layout) and folding in desktop additions
  (desktop asset staging, desktop agent-check checks, README sections)
- All 45 desktop tools carry the Required<ToolAnnotations> contract; hints
  chosen per tool semantics (execute-tableau-command destructive, cache
  mutators non-idempotent, readers read-only/idempotent)
- Serialized desktop tools/list surface fits the 46k deferral-cliff budget
  (45,572 measured) via schema-description compression only; tool
  descriptions and expertise:// steering pointers unchanged from authoring
- CI: desktop variant build step added; workflow also triggers on PRs to
  feature/desktop
- Validation: tsc clean, 255 test files / 3660 passed, lint clean, web +
  desktop builds green
@mattcfilbert
mattcfilbert marked this pull request as draft July 9, 2026 15:19

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

Early review — desktop authoring integration

Solid, well-tested port with a clean validation/binder architecture. Two confirmed correctness bugs in the field-metadata apply path should block; a handful of important robustness risks in the network/apply paths are worth confirming before this leaves draft. Scope: I reviewed the ~12.6k lines of desktop-authoring TypeScript logic (skipped knowledge markdown, XML fixtures, generated data, tests).

🔴 Blocking

  • fields.ts encoding-add verification checks the wrong ref — throws on real success (calc-field aggregation case)
  • fields.ts ensureColumnInstanceInDependencies returns the pre-correction name — emits a dangling field binding

🟠 Important

  • externalApiClient per-request timeout is dead in the production path
  • interpretLoadOutcome payload-sniffing can convert real successes into errors
  • classify geo-slot widening can silently double-bind one field (fail-open)
  • field-resolver count-distinct emits an invalid derivation

🟡 Nits — duplicated aggregation logic (root cause of the two blockers), a comment that misdescribes the function below it, stale // src/binder/... header paths, WHAT-narration comments + leftover [DEBUG] logs, and a couple of fail-closed allowlists that hard-error on unlisted-but-valid content.

Great test coverage and the fail-closed security funnel (pack verification, SEA asset integrity, apply mutex) all checked out clean.

🤖 Posted by KylerGPT — an AI reviewer trained on Kyler's review history. Kyler reviewed and approved this before posting.

const verifyPanes = normalizeArray(worksheet.table.panes.pane);
const verifyPane = verifyPanes[0];
const verifyEncodings = normalizeArray(verifyPane?.encodings?.[encodingType]);
const encodingAdded = verifyEncodings.some((enc: any) => enc['@_column'] === columnRef);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Verification checks the wrong ref — throws on real success. The encoding was inserted with correctedColumnRef (line 145), but this verifies against the original columnRef. For any calc field whose formula already aggregates (the exact case the correction exists for), correctedColumnRef !== columnRef, so encodingAdded is false and this throws Failed to add encoding... on a successful add. Verify against correctedColumnRef.

dependencies.length === 1 ? dependencies[0] : dependencies;

// Return the corrected instance name so caller can use it
return correctedInstanceName;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Returns the pre-correction name — dangling field binding. When the base column isn't yet in datasource-dependencies (only in the workbook), the first correction block (~654) is skipped and the second block (~814) creates the instance as actualColumnInstanceName ([usr:...]) — but this returns correctedInstanceName ([sum:...]). The caller writes the returned name onto the shelf/encoding, so it references a column-instance that doesn't exist. Return actualColumnInstanceName (and ideally collapse the two correction blocks into one path).

headers['content-type'] = options.contentType;
}

const signal = options.signal ?? AbortSignal.timeout(this.timeoutMs);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Per-request timeout is dead in the production path. options.signal ?? AbortSignal.timeout(...) means the 60s floor only applies when no signal is passed — but ExternalApiToolExecutor.callEndpoint always threads a signal, so the timeout never engages. If the caller signal carries no timeout, a hung loopback socket leaves fetch outstanding indefinitely. Combine them: AbortSignal.any([options.signal, AbortSignal.timeout(this.timeoutMs)]) so a caller signal augments rather than replaces the floor.

* so a normal success (empty `result`, no `error`) is never turned into a false
* negative.
*/
export function interpretLoadOutcome(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Payload-sniffing can convert real successes into errors. This now gates the agent-API load success paths. Branch 1 (top-level status.error) is safe, but branches 2-4 key off generic field names (status, error, errors, success/ok/loaded) inside an arbitrary result payload the code doesn't control. A successful load that happens to carry one of those fields with unrelated semantics gets relayed as load-rejected. Worth confirming against real Desktop payloads before trusting the sniffing branches.

}

const chosen = [...picks.values()];
if (new Set(chosen).size !== chosen.length) return null; // two slots, one field

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Geo-slot widening can silently double-bind one field (fail-open). This distinctness check compares geo picks only among themselves, never against fields already consumed by non-geo slots in used. The W60 zero-candidate widening (~887) resolves a geo slot over the full schemaDims. For a fast-path template with ≥2 geo slots plus another dimension slot, an ask naming both can bind the same field to a geo slot AND a non-geo slot, and validation passes — a silent wrong bind on a path whose contract is "fail closed." Fix: exclude used fields from the widened pool, or fold geo picks into a distinctness check that also includes the non-geo bound fields.

// If it's a calculated field with aggregation, we need to use usr prefix
if (existingBaseColumn?.calculation?.['@_formula']) {
const formula = existingBaseColumn.calculation['@_formula'];
const aggFunctions = [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Duplicated algorithm (root cause of the two blockers above). This 13-element aggFunctions array + hasAggregation scan + usr: correction is copy-pasted verbatim again at ~817, and the quantitative-derivation list is duplicated twice more in this function (plus a third aggFunctions copy in field-builder.ts:387). Changing the recognized aggregation set means editing all copies in lockstep. Extract formulaHasAggregation(formula) and a shared QUANTITATIVE_DERIVATIONS set.

import { rewriteFieldReferences } from './fieldReferenceRewriter.js';
import { injectTemplate, InsertPosition, SheetType } from './injectTemplate.js';

/** Escape the five XML metacharacters (identical to the inject-template tool). */

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Comment misdescribes the function below it. This doc (Escape the five XML metacharacters...) sits directly above ensureUserNamespace; the function it actually describes (escapeXml) is further down with no doc. The two ported comments got interleaved — move it (or drop it per the NO-COMMENTS rule).

@@ -0,0 +1,1260 @@
// src/binder/classify.ts

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Stale header path. Reads // src/binder/classify.ts but the tree moved under desktop/. Same stale first-line comment is on all 11 src/desktop/binder/*.ts files — per the delete-stale-comments rule, drop them.

});
const doc = parser.parseFromString(workbookXml.trim(), 'text/xml');

// Find the <window class="dashboard" name="<dashboardName>"> element

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 WHAT-narration comments. These (// Find the <window..., // Remove any existing <viewpoints>, // Build new <viewpoints>) restate what the next line does — flagged under the near-absolute NO-COMMENTS rule. The function's leading docblock explaining why viewpoints matter is the allowed kind and should stay. (Same applies to leftover [DEBUG] console.error/console.warn in the fields.ts apply path and the Lane M6 milestone... PR-status headers across the intelligence files.)

* `column-instance` derivation outside this closed set silently rewrites to None on
* load. Exported so other code can share the single allowlist.
*/
export const CANONICAL_DERIVATIONS = new Set<string>([

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Fail-closed allowlist that hard-ERRORs on unlisted-but-valid content. Any derivation outside CANONICAL_DERIVATIONS becomes a blocking error — e.g. spatial Collect (MakePoint/MakeLine) isn't listed, so a legit live-readback workbook containing it would be hard-rejected as "invalid derivation." An allowlist that errors is fragile to omissions; consider warn-not-error for the "unknown but not obviously invalid" case. Same pattern in connectionsNotAuthorable.ts (terminal reject on any non-federated connection). Both are documented as intentional — flagging the fragility, not demanding a change.

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

(duplicate of the review directly above — double-posted by mistake; see the other review for the full findings)

mattcfilbert and others added 20 commits July 15, 2026 00:45
…play, post-#501) (#504)

* Resync feature/authoring → feature/desktop (content-level replay of 1e81b39..b537e4d)

The af82a71 squash broke ancestry, so this is the planned one-time
content-level resync, not a merge: git apply -3 of the authoring delta
since the squash point (1e81b39b537e4d, post-#501) onto
feature/desktop. 361 files: the full W60-W64 authoring evolution — P0
dogfood fixes (spatial-intent guard, bindExplicitTemplate manifest
enforcement, tooltip-dimension-requires-attr + 25-rule validation suite),
consolidated add-field/remove-field tools, knowledge corpus refresh,
render-stamp manifests, behavioral test suites.

Desktop-side work is preserved, adjudicated file-by-file against the
squash base:
- The 6 per-shelf field tools desktop edited in #431 are deleted (they
  were consolidated into add-field/remove-field on the authoring line);
  #431's Required destructiveHint/idempotentHint annotations are grafted
  onto the 8 authoring tools that lacked them, using desktop's reviewed
  hint values per tool.
- 24 region conflicts resolved to the authoring side after verifying no
  conflict block contained desktop-only annotation content.
- package.json keeps desktop's version (2.25.0); lockfile regenerated.
- Byte-ratchet reconciled: plan-dashboard-creation and inject-template
  descriptions trimmed to fit their grandfathered caps with the new
  annotation hints; stale pins ratcheted down (bind-template 2131→2110,
  validate-proposal 2035→2014, plan-dashboard-creation 2043→2040,
  inject-template 1404→1382).
- eslint now ignores the two lockstep byte-identity files (classify.ts,
  fieldReferenceRewriter.ts) whose deliberate prettier drift would
  otherwise fail desktop CI's lint gate; the other 9 pre-existing lint
  errors from the authoring line are fixed. scripts/check-lockstep.mjs
  still passes (byte identity intact).

Validation: full combined suite 4201 passed / 1 skipped (289 files),
tsc --noEmit clean, npm run lint clean, lockstep gate OK, desktop
tools/list surface tests green.

Note: tmcp PR #503 (detail→lod normalization) is open against
feature/authoring and is NOT in this snapshot — merge it there and
re-apply the 3-file delta here, or retarget it to feature/desktop after
this lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(web): pin the clock in stale-content report test (UTC-midnight flake)

The 'runs a single Site Content VDS query' test computed daysSinceLastUse
against the real clock while wb-recent's fixture date is fixed
(2026-04-15) — the moment UTC passed 2026-07-15 the row aged past the
90-day threshold and the test started failing everywhere. Pin Date (only
Date, so awaited promises still run) to the same 2026-05-20 anchor the
file's unit tests already use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(binder): 30s timeout on the memo speedup benchmark (CI-runner flake)

The warm-vs-cold benchmark (300 cold binds over a 302-field workbook)
blew the 5s default on the shared CI runner under the combined suite's
load (both node jobs, 2026-07-15 run). The assertion is a load-immune
ratio (warm < cold), so a generous timeout is safe.

Port note: bump a2td's equivalent memo benchmark the same way if it
starts flaking there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): isolate seaAssets from the parallel e2e pool (build/ race killed 12 suites)

Root cause of the 12 e2e suites dying with "MCP error -32000: Connection
closed" at ~70ms in CI: seaAssets.test.ts (new to this branch from the
authoring line) runs buildVariant('desktop') — an in-place rebuild of
./build — concurrently with the rest of the e2e pool, whose suites spawn
`node build/index.js` children. A child that boots during the write
window dies pre-handshake; which 12 suites lose is scheduling luck.
Evidence: an unrelated branch without this file ran the same pipeline
green 15 minutes before the red run; the failing suites' code and the
web runtime are byte-identical to feature/desktop/main.

Fix: exclude seaAssets from vitest.config.e2e.ts and run it alone via
vitest.config.e2e.sea.ts (npm run test:e2e:sea) as a follow-up step in
the run-e2e-tests composite action. mergeConfig CONCATENATES include
arrays, so the sea config hard-overrides test.include after merging.

Verified: sea-only config runs exactly seaAssets (2 tests green,
desktop-variant build included); main e2e config lists 0 seaAssets
tests; eslint + tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* TEMP: e2e child-stderr diagnostic (redacted) — revert before merge

* Revert "TEMP: e2e child-stderr diagnostic (redacted) — revert before merge"

This reverts commit 34fd0b1.

* Revert "test(e2e): isolate seaAssets from the parallel e2e pool (build/ race killed 12 suites)"

This reverts commit c403eaf.

* fix(ci): build:desktop must not clean away build/index.js before E2E

Root cause of the 11-12 e2e suites failing with "MCP error -32000:
Connection closed" at ~70ms on this branch: the desktop-branch ci.yml
runs "Build desktop variant" AFTER "Build", and plain build:desktop
CLEANS ./build (src/scripts/build.ts rm's it when --dirty is absent) —
deleting the build/index.js every e2e suite's spawned child needs. Suites
scheduled before server.test.ts incidentally rebuilds it (buildVariant
--dirty) died instantly; later suites passed — which is why the failing
set looked arbitrary but was deterministic per run.

Receipts: locally, `npm run build && npm run build:desktop` leaves NO
build/index.js; with --dirty both artifacts coexist. A CI diagnostic
(temporarily committed, now reverted) showed an identically-env'd child
booting cleanly — env and runtime were never the problem. The earlier
seaAssets-isolation hypothesis is reverted as refuted.

Fix: new build:desktop:dirty script; ci.yml uses it. The clean behavior
of plain build:desktop is preserved for standalone use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): use build:desktop:dirty in the workflow (companion to 416ecc2 — the Edit was hook-blocked so the yml change landed separately)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…tools under the 46k cliff (#505)

* Resync feature/authoring → feature/desktop (content-level replay of 1e81b39..b537e4d)

The af82a71 squash broke ancestry, so this is the planned one-time
content-level resync, not a merge: git apply -3 of the authoring delta
since the squash point (1e81b39b537e4d, post-#501) onto
feature/desktop. 361 files: the full W60-W64 authoring evolution — P0
dogfood fixes (spatial-intent guard, bindExplicitTemplate manifest
enforcement, tooltip-dimension-requires-attr + 25-rule validation suite),
consolidated add-field/remove-field tools, knowledge corpus refresh,
render-stamp manifests, behavioral test suites.

Desktop-side work is preserved, adjudicated file-by-file against the
squash base:
- The 6 per-shelf field tools desktop edited in #431 are deleted (they
  were consolidated into add-field/remove-field on the authoring line);
  #431's Required destructiveHint/idempotentHint annotations are grafted
  onto the 8 authoring tools that lacked them, using desktop's reviewed
  hint values per tool.
- 24 region conflicts resolved to the authoring side after verifying no
  conflict block contained desktop-only annotation content.
- package.json keeps desktop's version (2.25.0); lockfile regenerated.
- Byte-ratchet reconciled: plan-dashboard-creation and inject-template
  descriptions trimmed to fit their grandfathered caps with the new
  annotation hints; stale pins ratcheted down (bind-template 2131→2110,
  validate-proposal 2035→2014, plan-dashboard-creation 2043→2040,
  inject-template 1404→1382).
- eslint now ignores the two lockstep byte-identity files (classify.ts,
  fieldReferenceRewriter.ts) whose deliberate prettier drift would
  otherwise fail desktop CI's lint gate; the other 9 pre-existing lint
  errors from the authoring line are fixed. scripts/check-lockstep.mjs
  still passes (byte identity intact).

Validation: full combined suite 4201 passed / 1 skipped (289 files),
tsc --noEmit clean, npm run lint clean, lockstep gate OK, desktop
tools/list surface tests green.

Note: tmcp PR #503 (detail→lod normalization) is open against
feature/authoring and is NOT in this snapshot — merge it there and
re-apply the 3-file delta here, or retarget it to feature/desktop after
this lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(web): pin the clock in stale-content report test (UTC-midnight flake)

The 'runs a single Site Content VDS query' test computed daysSinceLastUse
against the real clock while wb-recent's fixture date is fixed
(2026-04-15) — the moment UTC passed 2026-07-15 the row aged past the
90-day threshold and the test started failing everywhere. Pin Date (only
Date, so awaited promises still run) to the same 2026-05-20 anchor the
file's unit tests already use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(combined): TOOL_PROFILE=combined-lean — full desktop surface + lazy web tools under the 46k cliff

The combined desktop+web build serializes ~140.9k bytes of tools/list —
3x past the ~46,000-byte ToolSearch auto-deferral cliff — and even
dropping the whole pulse group (36.4k) leaves it far over, so no eager
subset fits. combined-lean keeps the full desktop authoring surface
eager (44,015 bytes, unchanged) and collapses the entire web surface
into ONE ~600-byte loader tool.

- TOOL_PROFILE moves from desktop config to BaseConfig (shared across
  web/desktop/combined; same trim+lowercase normalization). Desktop
  treats 'combined-lean' as 'full' — the lean half is the web side.
- New web tool `load-web-tools {group}` (Tony's lazy-load): calling it
  registers the requested web tool group's real tools on the live server
  via the same filtered pipeline as eager startup (disabled +
  INCLUDE_TOOLS/EXCLUDE_TOOLS still apply); the SDK emits
  notifications/tools/list_changed per registration so clients pick up
  e.g. the 7 pulse tools on demand. Idempotent per group; 'all' loads
  everything. Unset/'full' TOOL_PROFILE keeps today's eager behavior
  byte-identical.
- registerTools callback wiring extracted to _registerWebTool (no
  behavior change on the eager path).
- New suite server.combined-lean.test.ts: combined-lean registers
  desktop-full + exactly the loader; total serialized surface asserted
  <= 46,000 (same serialization as the desktop budget test); loader
  entry capped at 650 bytes; pulse group loads on demand and is
  idempotent; EXCLUDE_TOOLS respected on hydration; unset profile
  unchanged.

Caveat: lazy loading assumes the client honors tools/list_changed (or
re-lists after calling the loader) — true for the stdio/combined embed
path this profile targets; stateless HTTP keeps eager registration.

Validation: full suite 4207 passed / 1 skipped (290 files), tsc clean,
lint clean, lockstep gate OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(combined-lean): stateless-HTTP eager fallback, serialized loads, honest budget accounting

Three findings from adversarial review of the combined-lean feature:

1. Stateless HTTP (TRANSPORT=http + DISABLE_SESSION_MANAGEMENT) discards
   the per-request server as the response closes, so lazily hydrated
   tools would register on a corpse. combined-lean now falls back to
   eager registration there; the lean surface applies only where the
   server outlives the call (stdio / sessionful HTTP).

2. Overlapping load-web-tools calls could both pass the loaded-set check
   and the second registerTool would throw on the duplicate name.
   Loads are now serialized through a never-rejecting promise chain;
   concurrent same-group calls resolve as loaded + already-loaded with
   exactly one registration set.

3. The 46k budget test summed per-entry bytes, undercounting the actual
   {"tools":[...]} payload envelope (~54 bytes) — real bytes sat at
   exactly 46,000 with a passing assert. The test now measures the
   payload shape itself, and the loader description is trimmed to buy
   real margin.

Suite: 4209 passed / 1 skipped, tsc + lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(binder): 30s timeout on the memo speedup benchmark (CI-runner flake)

The warm-vs-cold benchmark (300 cold binds over a 302-field workbook)
blew the 5s default on the shared CI runner under the combined suite's
load (both node jobs, 2026-07-15 run). The assertion is a load-immune
ratio (warm < cold), so a generous timeout is safe.

Port note: bump a2td's equivalent memo benchmark the same way if it
starts flaking there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): isolate seaAssets from the parallel e2e pool (build/ race killed 12 suites)

Root cause of the 12 e2e suites dying with "MCP error -32000: Connection
closed" at ~70ms in CI: seaAssets.test.ts (new to this branch from the
authoring line) runs buildVariant('desktop') — an in-place rebuild of
./build — concurrently with the rest of the e2e pool, whose suites spawn
`node build/index.js` children. A child that boots during the write
window dies pre-handshake; which 12 suites lose is scheduling luck.
Evidence: an unrelated branch without this file ran the same pipeline
green 15 minutes before the red run; the failing suites' code and the
web runtime are byte-identical to feature/desktop/main.

Fix: exclude seaAssets from vitest.config.e2e.ts and run it alone via
vitest.config.e2e.sea.ts (npm run test:e2e:sea) as a follow-up step in
the run-e2e-tests composite action. mergeConfig CONCATENATES include
arrays, so the sea config hard-overrides test.include after merging.

Verified: sea-only config runs exactly seaAssets (2 tests green,
desktop-variant build included); main e2e config lists 0 seaAssets
tests; eslint + tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* TEMP: e2e child-stderr diagnostic (redacted) — revert before merge

* Revert "TEMP: e2e child-stderr diagnostic (redacted) — revert before merge"

This reverts commit 169eb9f.

* Revert "test(e2e): isolate seaAssets from the parallel e2e pool (build/ race killed 12 suites)"

This reverts commit 4e5b6ff.

* fix(ci): build:desktop must not clean away build/index.js before E2E

Root cause of the 11-12 e2e suites failing with "MCP error -32000:
Connection closed" at ~70ms on this branch: the desktop-branch ci.yml
runs "Build desktop variant" AFTER "Build", and plain build:desktop
CLEANS ./build (src/scripts/build.ts rm's it when --dirty is absent) —
deleting the build/index.js every e2e suite's spawned child needs. Suites
scheduled before server.test.ts incidentally rebuilds it (buildVariant
--dirty) died instantly; later suites passed — which is why the failing
set looked arbitrary but was deterministic per run.

Receipts: locally, `npm run build && npm run build:desktop` leaves NO
build/index.js; with --dirty both artifacts coexist. A CI diagnostic
(temporarily committed, now reverted) showed an identically-env'd child
booting cleanly — env and runtime were never the problem. The earlier
seaAssets-isolation hypothesis is reverted as refuted.

Fix: new build:desktop:dirty script; ci.yml uses it. The clean behavior
of plain build:desktop is preserved for standalone use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): use build:desktop:dirty in the workflow (companion to 416ecc2 — the Edit was hook-blocked so the yml change landed separately)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(lint): sort server.web.ts imports (simple-import-sort) — combined-lean import block

CI lint (24.x) flagged the WebToolGroupName/webToolGroupNames import ordering
in the combined-lean addition; local lint had a stale eslint cache. Autofix
only — reorders the type/value imports from ./tools/web/toolName.js.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ield-tool title scrub (desktop twin of #503) (#510)

fix(metadata): normalize detail→lod encoding + SQL string-fn knowledge + field-tool title scrub (desktop twin of #503)

Port of #503 (feature/authoring) onto feature/desktop, needed because the
#504 content resync snapshot predates #503:
- canonicalEncodingType() maps the "detail" alias → round-trip-stable <lod>
  at addFieldToEncoding/removeFieldFromEncoding/moveFieldInEncoding entry
  (W-23447710 follow-up: Tableau silently strips <detail> on apply — the
  Location pill vanished and the symbol map collapsed to one AVG centroid)
- SQL string-functions translation section in tactics/data/sql-translation.md
- add-field/remove-field title + description tableau-speak scrub

Excluded from #503: package.json 2.12.1 bump (desktop line is 2.25.x →
bumped 2.25.1 instead); getStaleContentReport.test.ts fake-timers pin
(already present on this line).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…lockstep carrier) (#511)

* feat(classify): semantic-role-first geo slot binding (lockstep carrier for a2td W2)

Geo slot resolution prefers Tableau's semantic-role tag over name tokens: a
unique concept match ([State].[Name] → state slot) wins outright; 2+ matches
fail closed; no match falls back to the existing name-token unique-max logic,
except a field whose semantic role names a DIFFERENT geo concept is excluded
from name fallback. Adds city/zip concepts + the GEO_SEMANTIC_ROLE_CONCEPT
table, and an optional semanticRole on the classifier's inlined SchemaField.

BEHAVIOR IN THIS REPO TODAY: inert — desktop's schema summary does not yet
populate semanticRole, so every field falls through to the unchanged name-token
path (optional field, structural typing; compiles and binds identically).
The follow-up plumbing (listAvailableFields → SchemaField semanticRole, mirror
tables in the ask-router equivalent, tests) is the a2td→tmcp port ledger item;
a2td carries the full implementation and the behavioral test suite.

This file is the canonical lockstep-core copy: a2td's src/lockstep-core/
classify.ts is byte-synced FROM this file (sync-lockstep-core.mjs) — landing
this keeps the byte-identity invariant real rather than hash-regen-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(classify): masking hyphen↔space lockstep + masked propose-shortlist scoring (red-team CLS-001/002)

- maskFieldNames now applies the same hyphen↔space transform as
  phraseIndexInAsk, so a hyphenated field name matched via its spaced form
  ("Waterfall-Chart" ↔ "waterfall chart") is fully blanked — a FIELD NAME can
  no longer survive masking and select its chart family.
- buildLlmInput keyword-scores (and Fuse-searches) the MASKED ask, closing
  the same field-name leak on the propose leg (a field literally named "Pie"
  no longer collapses the shortlist to the pie family).

Canonical lockstep-core edit; a2td synced + hash-regenerated the same night
with pinning tests (portability-lane masking-hardening describe block).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…→ feature/desktop (#512)

* feat(desktop): W2 semanticRole plumbing + W4 post-apply readback verification

W2 — plumb Tableau @_semantic-role from the metadata read path into
SchemaField.semanticRole (types.ts, field-builder.ts, schema-summary.ts) so
classify.ts's already-present geo semantic-role binder activates. Territory-class
fields ([State].[Name]) now bind spatial-choropleth-map end-to-end; verified no
wrong-bind against portability/bind-behavior-matrix/carrier-uniqueness suites.

W4 — port readback-verify.ts (verbatim from a2td) + wire into loadWorksheetXml:
after a successful apply, re-read the sheet and diff intent-bearing structures.
ERROR-severity drops (silently-dropped-pill) fail the apply via a new
readback-failed variant; WARNING-severity findings ride along on Ok and surface
in the apply-worksheet message. Readback runs before focus (never navigate to a
rejected sheet). Re-read failure skips verification, never masks a real apply.

Note: tmcp has no interaction/episode event system, so a2td's readback_verification
interaction event has no home here (logged via structured logger instead).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(desktop): W9 session reliability — stale-session gate + cross-instance cache-bleed guard

Freshness gate: SessionManager.getExecutor now re-verifies a cached local session's
Desktop instance against the live manifest (port + start_time) before handing the
executor back. A restarted/quit Desktop evicts the cached session and throws
SessionStaleError with agent-actionable recovery. DesktopDiscoverer injectable for test.

Cache-bleed guard: new cacheFingerprint.ts writes a <file>.meta.json sidecar recording
the producing Desktop instance (pid/port/start_time) when batch-create-and-cache-sheets
caches workbook/worksheet/dashboard XML; build-and-apply-worksheet and
build-and-apply-dashboard refuse a cache whose fingerprint mismatches the current
session (new CacheSessionMismatchError, 409) and tell the agent to re-read. No override
flag by design. Missing/unreadable sidecars and unresolvable fingerprints proceed
(pre-sidecar caches stay valid, never block blind).

Adapted to tmcp's session model: fingerprints resolve via DesktopDiscoverer (injectable
resolver) rather than a2td's ServerContext/episodeStore, which tmcp does not have.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(desktop): W6 sheet-name did-you-mean on a worksheet miss

getWorksheetXml's no-worksheet-found branches now append a best-effort suggestion:
list the live sheet names (via the list-worksheets command), surface close matches
(case-insensitive substring either direction) first, and tell the agent to ask the
user rather than guess when nothing clearly matches. Never throws; '' when the list is
unavailable (zero cost on the success path). This is the SHEET-name twin of tmcp's
existing FIELD did-you-mean (field-resolver.ts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(desktop): W1 + W3 portability-lane behavioral evidence

Adds src/desktop/binder/portability-lane.test.ts — offline bind assertions over three
non-Superstore fixtures (SaaS ops / healthcare claims / finance planning) that pin the
overnight behaviors tmcp's existing portability.invariant.test.ts does not cover:
  • W2 — Territory tagged [State].[Name] binds spatial-choropleth-map by semantic role,
    end-to-end, used_llm=false, non-Superstore datasource (regression lock for the metadata
    plumbing that activates classify.ts's geo binder).
  • W3 — the histogram refuses without live bin-width stats: distribution-histogram stays
    fast_path_eligible=false with the DATASET_SPECIFIC_FORMULA blocker, Call-1 proposes,
    an explicit proposal escalates, and even forced-eligible a bind never emits the old
    Superstore-derived 500 (tmcp never carried a2td's width adapter — this locks that absence).
  • W1 — non-Superstore field_mapping never leaks "Superstore"; each fixture binds under
    its own datasource name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(lockstep): regenerate classify.ts hash to match the committed file (pre-existing base drift)

The lockstep hash gate was RED on origin/feature/desktop before this wave: PR #511
landed the semantic-role classify.ts (hashing 8f0dc7d…) but never regenerated
lockstep.hashes.json, which still recorded the pre-#511 hash 2fdf604…. classify.ts
itself is UNCHANGED by this port. The regenerated hash equals a2td's manifest entry
for src/lockstep-core/classify.ts exactly, restoring the cross-repo byte-lock invariant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(desktop): lazy-construct DesktopDiscoverer in SessionManager (preserve explicit-session invariant)

The W9 freshness gate added a discoverer to SessionManager, but constructing it eagerly
in the ctor made `new DesktopMcpServer()` consult discovery machinery even for an
explicit-session tool call that never needs it — tripping the "discovery is never
consulted" invariant in bindTemplate/dashboardAutoApply tests. Build it lazily on first
real use (session create or freshness gate); an injected discoverer still wins for tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(desktop): eslint --fix formatting on the ported wave (prettier/quotes/import-sort)

Autofix-only: single-quote strings, prettier wrapping, and import sort on the W1/W2/W4/W6/W9
files. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(desktop): wire W9 cache-bleed guard into the primary get→apply flow

The overnight port wired the cross-instance cache-fingerprint guard (W9) only
into the coordination/batch build paths (batchCreateAndCacheSheets,
buildAndApply{Worksheet,Dashboard}). The primary interactive flow — get-*-xml →
edit → apply-* — was unguarded on BOTH ends: the get tools wrote no sidecar and
the apply tools ran no check. a2td guards all three artifact types on this flow
(worksheet.ts:229/299, workbook.ts:294/369, dashboard.ts:159/227); this closes
the port-fidelity gap.

Producers now writeSidecar after caching: get-worksheet-xml, get-workbook-xml,
get-dashboard-xml. Consumers now checkSidecar in the file-mode branch before
applying (returning CacheSessionMismatchError on mismatch): apply-worksheet,
apply-workbook, apply-dashboard. Guard is file-mode only — inline content
carries no fingerprint. Added explicit session-mismatch tests for apply-workbook
and apply-dashboard (the guard was previously only covered on the fail-open path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(desktop): W4 readback tolerates an Automatic mark resolved to a concrete class

Dual review (P1) flagged a false-positive: an authored <mark class="Automatic">
— the single most common authored mark — is resolved by Tableau to a concrete
class (Bar/Circle/Shape/…) on readback. The exact-match check treated that as a
changed/dropped mark at ERROR severity, which fails a legitimate apply and tells
the agent to "fix" correct XML. Now an intended Automatic mark is satisfied by
any concrete class in the same pane; only a truly absent mark (no candidate) is
still a real drop. Strictly a false-positive reducer — it never masks a genuine
drop (negative-control test pins that).

NOTE: the same exact-match logic exists in a2td's readback-verify.ts:298 — owed
as a cross-repo twin fix (port-debt ledger).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…esty (desktop twin of a2td #207) (#514)

* fix(desktop): stamp fingerprint sidecars on all cache-mutator writes (SCG-001/002 twin)

write-cached-xml, add-field, remove-field, and inject-template wrote cache
files without fingerprint sidecars; a missing sidecar deliberately fails
open at apply, so files written by these tools permanently bypassed the
cross-instance cache-bleed guard. Stamp writeSidecar after every mutator
write and require session on their schemas (desktop twin of a2td #207).

inject-template's describe trims land it at 1356 bytes — pin ratcheted
down from 1382.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(desktop): geo concept check on the validate leg + semanticRole in LLM input (GEO-02/03 twin)

Gate 3b: a geo slot must not bind a field whose Tableau semantic role
names a DIFFERENT geo concept — the deterministic path (pickGeoField)
already respected semantic roles, but the LLM-propose path validated on
name affinity alone. Mirrors the private geo tables from hash-gated
classify.ts (kept token-identical, pinned by a source-level parity test)
and enriches buildLlmInput with semanticRole so the model sees the same
evidence the binder does. Desktop twin of a2td #207; lockstep-core bytes
untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(desktop): honest response text when post-apply readback is skipped (RB-01/SCG-004 twin)

verifyPostApplyWorksheetReadback now returns {ok,status,message} instead
of a bare findings array, so a skipped readback (worksheet re-read failed
or threw) is distinguishable from a passed one. apply-worksheet and
build-and-apply-worksheet no longer claim verified success when the
readback never ran — the response carries the skip status and reason.
Desktop twin of a2td #207.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… dimension without slot capacity (#515)

fix(binder): temporal completion must not swallow an ask-named dimension with no slot capacity (CLS-003)

Unique-date temporal completion ("trend of Actual Amount by Fiscal Period" with
one schema date) silently dropped the ask-named non-temporal dimension when the
template had no slot to consume it, charting a date axis the user never asked
for. roleGreedyBind's final-bind completion now fails closed (escalate to
propose/ask) when an ask-named non-temporal dimension exceeds remaining slot
capacity. Capacity counts remaining active categorical/geo slots PLUS one armed
optional facet slot — facetBinding appends a spare named categorical after the
required slots bind, so explicit facet asks ("trend of Sales per Region") still
complete. Guard runs only when temporalCompletion is present, so
selectWithinFamily's slot-fit probes are byte-unchanged.

Lockstep twin: a2td src/lockstep-core/classify.ts synced from this file in the
paired a2td PR.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…/506) (#520)

* feat(validation): malformed-top-n-filter rule + worksheets.md Top-N correction (W-23447507 port)

Port of a2td #209's W-23447507 slice to feature/desktop. A Top-N filter
authored as a flat <groupfilter function="filter"> with count/direction
attributes is silently stripped by Tableau on apply — viz shows unfiltered
data while apply reports success. New preflight rule blocks the flat shape
in workbook + worksheet contexts; worksheets.md now teaches the confirmed
nested function="end" recipe alongside the table-calc alternative.

Deliberately NOT ported: a2td's lossy-apply-detect droppedFilters tracking —
COVERED in tmcp: readback-verify already compares intended-vs-readback
filters at error severity (stricter than a2td's warning) on the worksheet
apply path. Overlap with malformed-set-groupfilter pinned impossible by
test: that rule fires only inside <group> set definitions, this one selects
<filter> nodes with not(ancestor::group).

6/6 new tests green, knowledge suite green, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(fields): optional session on list-available-fields re-snapshots the live workbook (W-23447478 port)

Port of a2td #209's W-23447478 slice to feature/desktop — the datasource-
recognition P0: an agent listing fields from a stale cache never sees
datasources the user added after the snapshot. With session, the tool now
fetches live workbook XML (same executor path as get-workbook-xml),
rewrites the cache file and stamps its fingerprint sidecar before listing;
without session, cache-only behavior is unchanged. Refresh failure is an
explicit error with a retry-without-session hint — never a silent stale
listing.

Mapped, not copied: tmcp uses 'session' (not a2td's _session) per sibling
tool convention, and readOnlyHint flips to false since the tool now writes
the cache when refreshing (same reasoning as get-workbook-xml). Surface
delta +108 bytes; both surface-budget pins green.

8/8 tool tests, 18 server.desktop + 7 combined-lean pins green, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(server): host-computed verification receipt on apply responses (W-23447506 port)

Port of a2td #209's promise-check seam to feature/desktop. Every apply-style
success response now ends with one HOST VERIFICATION line the model cannot
fill: worksheet applies derive verified/unverified/failed from preflight
warnings + post-apply readback (the receipt subsumes the old skipped-readback
status sentence — one host-truth line, not two); whole-workbook, dashboard,
and batch applies honestly state their intent is NOT structurally re-verified.

Command layer now returns preflight warnings with the load result
(LoadWorksheetXmlOk.validationWarnings; loadWorkbookXml/loadDashboardXml
Ok payloads) so receipts never re-run validation. Dashboard receipt is a
deliberate extension beyond a2td (message-shaped, no readback — same honest
wording). Key-set-pinned tools (dashboard-auto-apply, bind-template) left
untouched; a2td's lossy-scan receipt clause dropped — tmcp has no
lossy-node detector, and claiming a clean scan that never ran would be the
exact dishonesty this seam exists to prevent.

Receipt suite 6/6; 10 touched-suite files 107 green; server.desktop pins
18 green; tsc + eslint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(server): classify apply failures into actionable agent guidance (W-23447473 p1 port)

Port of a2td #209's apply-failure classifier to feature/desktop, mapped onto
tmcp's layering: the classifier (pattern table -> failure_class + evidence +
FIX guidance, GENERIC_WRAPPER stripping, undeclared auto-calc detection)
lands beside interpretLoadOutcome in the command layer, and every
load-rejected error message is now formatApplyFailureForAgent output instead
of Desktop's raw wrapper text. McpToolError's three XML-load subclasses pass
a string message through instead of JSON.stringify-wrapping it — which also
un-JSONs the readback-failed fix recipes from W4.

Deliberately not wired (tmcp has no such paths): a2td's datasource-collapse
and connection/ECONN patterns; ExecuteCommandError dispatch failures keep
their existing funnel. 7-case classifier unit suite; command-layer test
proves a real rejected load returns 'Apply failed: ... FIX: ...'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(routing): edit-in-place for current-sheet asks + fix/repair verbs (W-23447473 p2 port)

Port of a2td #209's edit-in-place routing to feature/desktop. route-spec
gains CURRENT_SHEET_RE ('current/this/that sheet|worksheet' is edit context),
fix/repair join the edit-verb regex, and a narrow NON_SHEET_FIX_RE guard
keeps 'fix the data source' from classifying as a sheet edit (tmcp has no
sheet-edit class to absorb it — deviation from a2td, justified in-code).
DESKTOP_ROUTE_TABLE gains the edit-in-place route so generated instructions
tell skill-less clients to resolve the target and never create a new sheet
unless asked.

Surface truth: the draft's byte math was based on the stale 44,015 comment;
real combined-lean base was 45,677, so the instruction prose is tightened to
fit — measured desktop total 45,336, combined-lean 45,976 (24 bytes under
the 46,000 cliff; next growth must trim, not raise the cap). Route tests pin
verified classifyAskRoute behavior, not guessed shapes.

Full suite 4,300 green (296 files), tsc + eslint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: prettier autofix on 4 ported test files (unblock #520 CI)

The receipt / validationWarnings port left prettier drift on four
desktop test files (trailing comma in Ok({ validationWarnings: [] }),
.spyOn chain wrapping). eslint --fix only; no logic touched. Clears the
7 prettier/prettier errors failing the build (>=22.7.5 <23) and (24.x)
jobs on PR #520.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…y rewiring (W-23447497) (#521)

* feat(interaction): ask-user clarification tool + fail-closed ambiguity rewiring (W-23447497)

The clarification loop's tmcp half: a2td's tableau-ask-user ported as
'ask-user' (question + blocking|soft urgency + numbered options; pure
formatter, no session — it never touches Desktop; stop-and-wait is in the
tool contract, which even a2td's version lacked). The fail-closed outcomes
that already compute candidates now route them to the human instead of
dead-ending in tool loops: resolve-field ambiguity, bind-template
ambiguous/missing-field guidance, plan-dashboard-creation blocked response,
and DESKTOP_INSTRUCTIONS (edit-in-place 'ask via ask-user' + one blocking
clarification rule). TAS's circuit breaker referenced this exact tool name
before it existed (fixed in tab-agent-south !42) — this makes that recovery
path real.

Surface arithmetic (the real constraint): describe-trims on
validate-proposal / dashboard-auto-apply / dashboard-health-check freed more
than the 1,032-byte tool costs — desktop total 45,336 -> 45,202,
combined-lean 45,976 -> 45,843 (157 headroom); caps ratcheted DOWN. Demo
profile untouched.

Full suite 4,305 green (297 files), tsc + eslint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refine(desktop): restore ask/title noun-role on dashboard-auto-apply (Andy #521 review)

Andy-lens review flagged the surface trims that funded ask-user cut too
far on the confusable ask/title param pair ('Viz.'/'Sheet.' -> the LLM
can't tell which is the request vs the sheet name). Restore to
'Viz ask'/'Sheet title' and lead the tool description with the
all-or-nothing batch contract (its core semantic). The description reword
is byte-NEGATIVE vs the pinned baseline, funding all but +5 of the param
restoration; that +5 is a deliberate per-tool cap raise (1295->1300) with
inline justification. validate-proposal trims left as-is (no param-swap
risk). combined-lean unaffected; full suite 4305 green, tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…e 'Sheet 1' (focus-follows harden) (#522)

fix(desktop): canonical-name gate on worksheet/dashboard apply — goto-sheet can never target a stale name (W65 night wave, focus-follows harden)

Parse the <worksheet|dashboard name> from the XML fragment, fail before apply on mismatch (NFC-normalized compare, recovery-oriented FIX message, workbook-wrapped payloads rejected with 'single fragment required'), and thread the canonical name into load, readback, and focusAppliedSheet on both the Agent API and external-API paths.

Root cause twin of the a2td blank-Sheet-1 focus regression seen live 2026-07-16: the helper trusted caller args, so a stale 'Sheet 1' could be applied AND focused.

Validation: 46 targeted tests (4 new red-proofed) + full desktop sweep 2325 passed; tsc + eslint clean.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
port(a2td #214): refine sort_direction inserts computed-sort on unsorted simple bars

Twin of a2td #214 (exec dogfood 'couldn't sort a simple bar chart'). planSortInsertion on the exact safe shape only; shared pseudo-field classifier exclusion (reaches planTopN); template anchor verified against src/desktop/data/templates/magnitude-simple-bar.xml:24. Full desktop sweep 2342 green, surface cliff held (zero growth).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ling focus) (#525)

port(a2td #215): suppress per-sheet focus during plan-driven parallel dashboard builds

Gap CONFIRMED here: plan-dashboard-creation instructs parallel subagent builds (>=5 sheets) and each build-and-apply-worksheet fired goto-sheet — last completer steals focus. Internal planBuildFocus seam (session-scoped marks, zero tool-surface growth); standalone builds keep focusing; final dashboard owns focus. Full desktop sweep 2341 green, surface suites green.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Twin of a2td #213 adapted to tmcp conventions: shared refreshWorkbookCache module (list-available-fields refactored onto it), session-gated refresh-once + retry on not_found, graceful degrade for every failure shape (never a transport tool_error), concurrent dedup per workbookFile. readOnlyHint flipped to false (honest: session refresh writes cache+sidecar). Surface +113B (680 headroom), full desktop sweep 2343 green.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…d guidance rules) (#527)

knowledge: failure-recovery-honesty module — stale-cache re-read + receipt honesty (on-demand twin of a2td SKILL rules 8/9)

tmcp ships no bundled skill and DESKTOP_INSTRUCTIONS is surface-pinned, so the two agent-guidance rules land as an on-demand knowledge module at tactics/workflow/failure-recovery-honesty.md (auto-discovered by listKnowledgeSlugs; _index.md routed; real-corpus appearance pinned by resources.test.ts). NOT surface-counted — combined-lean cliff test green.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ild (#534)

* feat(desktop): route-table gates for authoring skill + plan-before-build

Add two prose entries to DESKTOP_ROUTE_TABLE:
- authoring-skill: load tableau-desktop-authoring before building; on
  repeated unresolved failures load the debugging skill instead of
  brute-forcing manual XML.
- plan-before-build: for multi-viz/dashboard asks, map requirement ->
  encoding -> rule and classify each as MAGNITUDE vs MEMBERSHIP, encoding
  membership with a discrete bucketing dimension, not a raw-measure gradient.

Pin the generated instruction text in tests.

* fix(desktop): recovery line names tableau-agent-debug by exact slug

Recovery path was prose ('the debugging skill'); switch to the exact
registered slug (tableau-agent-debug) so the agent doesn't guess when a
build is already failing. Pin the slug in a test.

* trim tools/list + instructions under the 46k auto-deferral cliff

The two new route entries plus existing tool prose put the combined-lean
measured payload at 46,626 vs the 46,000 cliff. Prose-only trims across
routeTable, bind-template, proposalSchema, plan-dashboard-creation, and
apply-dashboard bring it to ~45.7k with headroom; grandfathered per-tool
caps ratcheted down (2110->1898, 2040->1797, 1601->1557) and the
instructions snapshot re-pinned. Contract lines kept: auto_apply never
applies Call-2 proposals, exact-field-name rule, Call-1->Call-2
provenance, plan-then-build step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: prettier rewrap after byte-trim describe() changes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Matt Filbert <mattcfilbert@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…in (#537)

Byte-sync of lockstep classify.ts from a2td claude/w2c-propose-identity
(8181dd5): LlmProposeInput.fields gains optional datasource/column_ref,
emitted only when the summary has >1 datasource or duplicate display
names; single-ds payloads byte-identical. Manifest regenerated via
check-lockstep --update; both repos' manifests now carry identical
hash 87193686 for the classify twin (CLS-003 procedure).

INERT until plumbed on this side (same pattern as the #511 semanticRole
landing): tmcp's propose-instruction/prewarm surfaces don't yet emit the
qualified identities; that plumbing rides the #535 fields-tool
coordination per the port-debt ledger.

Validation firsthand: full suite 4,346 green (298 files), tsc clean,
check-lockstep 6/6.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ipt (a2td #216 port) (#538)

Port-audit-verified gap: promise-check.ts mapped readback status
'warning' to outcome 'verified', while readback classifies dropped
computed-sort/shelf-sort-v2 nodes as warning severity — so an apply that
silently lost a promised sort still stamped HOST VERIFICATION verified
(the false-PASS class #216 closed in a2td). Receipt input gains optional
readbackFindings (threaded from LoadWorksheetXmlOk.readbackWarnings,
which already existed — verifier shape unchanged); a promised-sort-loss
warning escalates verified -> failed. All other warnings keep today's
behavior; tmcp's stricter error-severity path untouched.

Cursor-minion port (gpt-5.5-high), orchestrator-adjudicated: full suite
4,351 green firsthand, tsc clean, lockstep 6/6.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#217+#222 port, seam-1 packet A) (#539)

* feat(desktop): exact column_ref first-class + shared ref helpers (a2td #217+#222 port, seam-1 packet A)

Port-wave packet A from the content audit. #222 half: exported
COLUMN_REF_REGEX + parseDatasourceQualifiedColumnRef /
parseColumnInstanceRef / parseCanonicalColumnRef /
formatCanonicalColumnRef in field-resolver (same API as a2td), private
regexes in fields.ts / explicit-bind.ts replaced; preserved divergence
(explicit-bind accepts bare [deriv:Base:sfx], fields require qualified);
colon-tolerant instance parsing (fixes [Profit:Ratio] -> [Profit]
mis-parse, caught by minion red run). #217 half: resolveField and
proposal-binding resolveInSummary recognize exact canonical column_ref
FIRST — hit resolves exact, ref-SHAPED miss fails closed (never falls to
fuzzy); datasource selector matches internal name or UNIQUE caption
(ambiguous caption refuses with guidance, Parameters excluded). Gate 5b
closure untouched; listAvailableFields untouched (#535's file).

Version 2.25.4 assumes #538 (2.25.3) merges first — resolve upward on
conflict.

Cursor-minion port (gpt-5.5-high), orchestrator-adjudicated: full suite
4,362 green firsthand, all 3 wrong-bind invariant suites green (210),
tsc clean, lockstep 6/6, eslint clean (4 fixable errors fixed at
adjudication).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: prettier fix on port test files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ource selection (a2td #220+#219 port, seam-1 packet B) (#540)

* feat(desktop): exact column_ref first-class + shared ref helpers (a2td #217+#222 port, seam-1 packet A)

Port-wave packet A from the content audit. #222 half: exported
COLUMN_REF_REGEX + parseDatasourceQualifiedColumnRef /
parseColumnInstanceRef / parseCanonicalColumnRef /
formatCanonicalColumnRef in field-resolver (same API as a2td), private
regexes in fields.ts / explicit-bind.ts replaced; preserved divergence
(explicit-bind accepts bare [deriv:Base:sfx], fields require qualified);
colon-tolerant instance parsing (fixes [Profit:Ratio] -> [Profit]
mis-parse, caught by minion red run). #217 half: resolveField and
proposal-binding resolveInSummary recognize exact canonical column_ref
FIRST — hit resolves exact, ref-SHAPED miss fails closed (never falls to
fuzzy); datasource selector matches internal name or UNIQUE caption
(ambiguous caption refuses with guidance, Parameters excluded). Gate 5b
closure untouched; listAvailableFields untouched (#535's file).

Version 2.25.4 assumes #538 (2.25.3) merges first — resolve upward on
conflict.

Cursor-minion port (gpt-5.5-high), orchestrator-adjudicated: full suite
4,362 green firsthand, all 3 wrong-bind invariant suites green (210),
tsc clean, lockstep 6/6, eslint clean (4 fixable errors fixed at
adjudication).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(desktop): planner datasource-qualified fields + apply-side datasource selection (a2td #220+#219 port, seam-1 packet B)

Port-wave packet B, stacked on packet A (#539). #220 half: planner
fields accept exact column_ref OR {query, datasource?}; resolution
cache keyed on (query, selector); task specs carry the resolved common
datasource; ambiguous AND not_found both block the plan (closes the
audit-verified hole where not_found-only still planned with a warning).
#219 half: buildAndApplyWorksheet derives the rewrite datasource from
explicitBind.datasource — the caption/first-workbook-datasource regex
grab is DELETED; no-manifest passthrough infers a single datasource from
the provided refs via the shared parser and FAILS CLOSED on mixed refs
with a prescriptive block; injectTemplate blocks caller DATASOURCE that
disagrees with the resolved mapping datasource. Two characterization
pins deliberately re-baselined (caption-first -> common-ref datasource),
each with a comment naming the new invariant. Zero contact with the
receipt-formatting area (#538's file overlap — verified no conflict).

Cursor-minion port (gpt-5.5-high), orchestrator-adjudicated: full suite
4,368 green firsthand, invariant suites 210 green, tsc clean, lockstep
6/6, eslint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

3 participants