Integrate desktop authoring line into core TMCP#474
Conversation
…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
tableaukyler
left a comment
There was a problem hiding this comment.
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.tsencoding-add verification checks the wrong ref — throws on real success (calc-field aggregation case)fields.tsensureColumnInstanceInDependenciesreturns the pre-correction name — emits a dangling field binding
🟠 Important
externalApiClientper-request timeout is dead in the production pathinterpretLoadOutcomepayload-sniffing can convert real successes into errorsclassifygeo-slot widening can silently double-bind one field (fail-open)field-resolvercount-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); |
There was a problem hiding this comment.
🔴 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; |
There was a problem hiding this comment.
🔴 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); |
There was a problem hiding this comment.
🟠 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( |
There was a problem hiding this comment.
🟠 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 |
There was a problem hiding this comment.
🟠 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 = [ |
There was a problem hiding this comment.
🟡 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). */ |
There was a problem hiding this comment.
🟡 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 | |||
There was a problem hiding this comment.
🟡 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 |
There was a problem hiding this comment.
🟡 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>([ |
There was a problem hiding this comment.
🟡 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.
…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 (1e81b39 → b537e4d, 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 (1e81b39 → b537e4d, 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>
Description
AI Assisted Authoring via MCP is coming to Tableau Desktop.
Type of Change
How Has This Been Tested?
Checklist
npm run version. For example,use
npm run version:patchfor a patch version bump.environment variable or changing its default value.
Contributor Agreement
By submitting this pull request, I confirm that:
its Contribution Checklist.