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>
…nalSpec verbs (330→333) (#542) feat(desktop): known-command guard on execute-tableau-command + NotionalSpec verbs in the reference Ports a2td's command-registry guard (born from the 2026-06-30 fatal-abort incident): fail-open reference load, crash-prone denylist (show-parameter- controls pair), unknown-verb refusal with up to 3 did-you-mean suggestions — wired before executor.executeCommand, server-side only (zero tools/list bytes; combined-lean ratchet untouched). Also admits the semantic-loop verbs to this repo's reference (330→333): generate-viz-from-notional-spec, generate-notional-spec-from-viz, is-analytics-assistant-available, each with a provenance_note (the generator's user-initiated classification filter drops pane-invoked commands). Full suite 4382 passed / 0 failed; tsc clean; prettier clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… + calc-authoring twin (#543) * docs(knowledge): NotionalSpec semantic loop — live-verified authoring + calc-authoring twin Port of the a2td notional-spec knowledge with tonight's live corrections: spec is authoritative replace (ClearSheet:false does not merge), poll-based readback discipline, write-blind generic route, param-error modal hazard, WorksheetId 500 avoidance. New companion doc covers the whole-document round-trip (save/load-underlying-metadata) for calc authoring — the WW-grade half that had no coverage — with the AA-gated dialog path explicitly steered away from and numerical-correctness honesty rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(routing): plain-viz route goes semantic-first — NotionalSpec before bind-template The server's own baked instructions were the deepest layer still steering every client to the XML template path first. The plain-chart route now tries execute-tableau-command + tabdoc:generate-viz-from-notional-spec first (sub-second, zero XML, live-proven for 6 chart families tonight), with bind-template kept as the fallback for out-of-vocabulary families (waterfall/KPI/funnel) and propose/escalate ambiguity. Route text trimmed to stay under the 46k tools/list deferral cliff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…instrumentation (a2td packet-#0 twin) (#541) * feat(desktop): episode-lite telemetry — lifecycle tools, tool events, structured promise outcome, route receipt (a2td packet-#0 twin for the Studio binary path) Env-gated episode JSONL for the eval loop, default OFF (EPISODE_EVENTS=on to enable; EPISODE_EVENTS_DIR overrides the file-logger directory). Zero product behavior or response-text change when off; all emission fail-open. - tableau-begin-episode / tableau-end-episode tools; begin returns episode_id in result text so the host transcript (and whole-chat LangSmith trace) carries the correlation key. - tool_start/tool_end/tool_error emitted at the DesktopTool execute choke point (duration_ms, success), field names matching the a2td stream. - classifyWorksheetPromiseOutcome extracted from the receipt formatter (text byte-identical); apply sites emit apply_succeeded/readback_verification with promise_outcome; whole-doc applies emit unverified. - route_receipt serialized from sessionRouteState at episode end — structural only, deflection prose excluded. - episode_begin pin tuple: tool_profile, system_prompt_version fallback mcp-server@<version>, TABLEAU_DESKTOP_SESSION_ID, env-sourced agent_types and langsmith_run_id (Studio-launcher sources tracked as UNKNOWN in the eval-on-Studio addendum). - FileLogger: appendJsonLine extracted so the episode writer reuses the per-file mutex machinery; log() semantics unchanged. Validation (re-run by orchestrator): npx tsc --noEmit clean, npm run lint clean, vitest 4383 passed | 1 skipped (301 files). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): tools/list expectations exclude default-off episode-lite tools The episode lifecycle tools register only when EPISODE_EVENTS=on (config.episodeEventsEnabled); the e2e expectation was asserting the full desktopToolNames set and failed on both variants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…face (fold-in from experiment) (#544) * experiment(desktop): TOOL_PROFILE=spec-loop — the ruthless 5-tool spec-loop-first surface The experiment: is Tableau's native semantic loop enough on its own, with NO XML tools, NO templates, NO bind-template? SPEC_LOOP_TOOL_PROFILE = execute-tableau-command + list-instances/available-fields/worksheets/dashboards. Everything a chart/calc/ dashboard ask needs routes through execute-tableau-command on the /v0 External API (generate-viz-from-notional-spec for charts; whole-document GET/POST for calcs); list-* is discovery/readback since the generic /v0 route is write-blind. The #542 known-command guard makes the single command tool safe against hallucinated verbs. Proven by hand 2026-07-19 on live Desktop: a full analytics workbook (12 authored calcs, 10 charts, dashboards) in 4.32s, zero XML surgery. This branch is the product home for that — additive (new profile only), off by default, for Kyler's review. Full suite 4383 green, combined-lean 46k ratchet intact, tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(desktop): eslint --fix import sort on the profile fold-in Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…dashboard XML (#546) fix(desktop): carry ancestor xmlns declarations onto extracted sheet/dashboard XML — untouched get→apply round-trips validate clean Live incident (first Sonnet 5 audition, 2026-07-19): get-worksheet-xml on a sheet whose top-N groupfilter carries user:-prefixed attributes produced a fragment missing the workbook root's xmlns:user declaration; the unmodified apply-worksheet preflight then failed well-formed-xml (NamespaceError: prefix is non-null and namespace is null) and the model hand-repaired the namespace to proceed. Extraction now copies xmlns/xmlns:* from the workbook root onto the extracted element (shared carryNamespaceDeclarations helper); dashboard extraction had the identical bug shape and uses the same helper. Validator strictness unchanged. Failing-first regression tests reproduce the live error verbatim; metadata tree 101 green; tsc clean. Authored by a Sonnet 5 builder subagent; adjudicated, re-verified, and committed by the session orchestrator. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…crete sort/top-N verification anchors (#547) * docs(knowledge): wrong-fork redirects from XML-era calc docs to the semantic calc twin + explicit sort/top-N readback blind spot Live audition receipts (first Sonnet 5 through TAS, 2026-07-19 morning): on a running-total ask the model consulted calc-fields/table-calcs and descended into workbook-XML surgery — the XML-era docs out-rank the semantic twin in knowledge search and carried no live-Desktop redirect. Also lands the blind- spot line the first Sonnet audition itself filed: nothing in-loop verifies a sort/top-N landed; state intent, not data outcomes, unless read back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(knowledge): NotionalSpec failure-recovery module — the crash cart for the semantic loop Ghost gen-9 Move 2, filled with the singing session's live forensics: error-class calibration (404 name / 400 envelope / 500 spec-contract), unknown-error-on-writes triage (modal vs dead app), full-spec-restore as the undo, re-inventory-after-interruption (the stale whole-document clobber), refine-readback race, and the say-the-mechanism-change rule. Cross-linked from notional-spec-authoring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…-first, not straight to XML (#548) fix(desktop): search-commands empty-result recommendation steers semantic-first, not straight to XML The reference's recommendation_when_no_invocable_match (and the code fallback) told every no-match search 'Use workbook JSON editing instead' — observed live steering the first Sonnet 5 audition toward XML surgery while it hunted for sheet commands mid-chart-build. The recommendation now names the NotionalSpec loop for viz asks and the workbook-document round-trip for calcs, with XML editing demoted to last resort — matching the #543/#544 semantic-first routing everywhere else in the server. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… (fabricated specs die at the tool, not as product 500s) (#545) feat(desktop): pre-dispatch guard for generate-viz-from-notional-spec — fabricated specs die at the tool, not as product 500s Live incident (first Sonnet 5 audition through tab-agent-south, 2026-07-19): the model invoked the command with an invented schema (mark/columns/rows/ title + a worksheetName param); the product returned a raw 500 and the fast path was lost to fallback. This guard validates the v0.2 contract from the notional-spec-authoring knowledge module before dispatch and answers with FIX lines carrying the minimal valid example and the expertise URI. Twin of a2td #232's param-contract guard, extended to the spec payload. 13 new tests; desktop tree 2423 green; tsc + eslint clean. Authored by a Sonnet 5 builder subagent; adjudicated, re-verified, and committed by the session orchestrator. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…-apply race) (#549) fix(desktop): refine-worksheet polls its confirmation readback — no more false refined:false on the async-apply race Confirmed live (2026-07-19, minimal repro in audition receipts): top_n refine applied, the single post-apply readback raced the async settle and reported refined:false ('readback did not contain the expected Top-N filter'); the immediate retry then refused with 'a Top-N filter already exists'. The readback now polls up to 8×250ms (the knowledge doc's documented working interval), confirming on first match; exhaustion keeps the honest refusal and names the async-settle possibility. Fetch and apply remain exactly-once. Race-simulation tests (fake timers): settle on poll 3, boundary poll 8, never-lands still refuses. 228 tests green across the touched tree; tsc clean. Sibling single-read race flagged, NOT fixed here (own ticket): verifyPostApplyWorksheetReadback (loadWorksheetXml.ts:83) — the host- verification seam behind every worksheet apply. Authored by a Sonnet 5 builder subagent; adjudicated, re-verified, and committed by the session orchestrator. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… overrides (modal-killer) (#550) feat(desktop): param-contract guard for every command — wrong param shapes die at the tool, not as modals on the user's screen Port of a2td #232, generalized: execute-tableau-command now validates args against the bundled reference's per-command parameters[] (direction:'in', required flags — all 767 entries carry explicit booleans) after validateKnownCommand and before the NotionalSpec payload guard. Unknown keys and missing required params are rejected with FIX lines; commands flagged opens_blocking_dialog get the stricter modal warning. Fails open when the reference is unreadable or a command declares zero in-params. PLUS the day's sharpest finding, encoded as LIVE_PARAM_OVERRIDES: the reference is WRONG at /v0 runtime for tabdoc:goto-sheet — it declares WindowLocator (required:true), but live {'WindowLocator': name} = 500 + the blocking modal 47BF7751 (reproduced twice on a human's screen today), while {'Sheet': name} — absent from the reference — SUCCEEDED three times. The override table is where live-verified corrections accumulate until the reference generator learns the /v0 dialect. Flagged, not fixed (own ticket): focusAppliedSheet.ts calls goto-sheet internally with {sheet: name} via the executor directly — bypasses this guard and matches NO known contract; second potential modal source. 15 guard tests + 6 integration tests; desktop tree 179 files / 2458 green; tsc + eslint clean. Guard core authored by a Sonnet 5 builder subagent (which correctly flagged the reference-vs-runtime contradiction instead of resolving it silently); live probes, override design, #545 chain-wiring, and test corrections by the session orchestrator. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…BF7751 killer) (#551) * fix(desktop): verify the applied sheet exists before goto-sheet — focusing an unknown name throws blocking modal 47BF7751 goto-sheet at a name Desktop doesn't know throws a BLOCKING modal ('bad value: sheet') instead of returning an error, and an apply is async enough that its sheet can be missing at focus time — live-reproduced three times tonight on the eval stage (probe matrix: lowercase sheet param + valid value SUCCEEDED; capital Sheet + valid SUCCEEDED; any param + bogus value = the modal). focusAppliedSheetBestEffort now polls list-worksheets/list-dashboards (4 x 250ms) and skips focus with a warning log if the sheet never appears: a skipped focus is a log line, a modal is a wedged stage. Test mocks move from strict Nth-call sequences to dispatching executors that answer the existence poll with the canonical names from the applied XML, so the NFC/NFD canonicalization assertions still hold; new modal-killer tests pin the skip path for both worksheets and dashboards. * style: prettier on the modal-killer test mocks
…tainment guard (#552) * feat(desktop): surface the document round-trip in the command reference (discoverability) Live root cause (2026-07-19, two banked receipts): on calc-heavy asks the agent searched 'underlying-metadata' -> 0 hits -> concluded 'not available in this build' -> fell back to hand-writing workbook XML and drowned. The verbs exist (executor maps them to GET/POST /v0/workbook/document) but had zero entries in the JSON that search-commands, validateKnownCommand, and paramContractGuard all read — undiscoverable AND uninvocable through the tool. Adds honest entries for tabui:save-underlying-metadata / load-underlying-metadata (load declares required in-param 'text'), allowlists both, and names the verbs in the zero-hit recommendation. Empirically gated: her exact failing query now returns the pair at ranks 1-2; 'calculated field' puts save at rank 1; guard rejects load{} and load{filepath} with actionable steers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): containment guard for load-underlying-metadata (whole-document or nothing) Adversarial-review P1 mitigation for surfacing the round-trip verbs: a pre-dispatch guard on execute-tableau-command rejects fragment loads (non- <workbook> root, no datasources/worksheets), loads that would DROP live worksheets (steer: delete-worksheet), and gross-shrink documents (<50% of live) — with fail-open on live-read infrastructure failure. Guard built by GPT-5.5 minion, adjudicated + full suite re-run firsthand (4453 green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(desktop): readback-verify law — a completed load envelope does not prove application Live-proven 2026-07-19 (Desktop main.26.0715, three probes): document loads that ADD columns or rewrite worksheet content apply; loads that REMOVE a datasource column are silently ignored — completed envelope, no effect. Teach: after every load, re-read with save-underlying-metadata and confirm the change landed. Applied to the load entry's description and the notional-spec-calc-authoring knowledge module. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(desktop): guard false-positive — real documents carry comments before the workbook root FATAL caught by live audition probe: Desktop documents open with a build comment between the XML prolog and <workbook> ('<!-- build main.26.0715 -->'); the root regex forgot comments, so the guard rejected EVERY legitimate whole-document load as a fragment — blocking the exact path it exists to protect. Live receipt: verse 2's 'fragment' rejections were false positives. Regression fixture uses the verbatim real head. Lesson (now in the stack skill): probe the guard's ACCEPT path against a real artifact, not only its reject paths against synthetic fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): author-calc tool — semantic calc authoring, no document in the agent's hands Live evidence (two drown receipts, 2026-07-19): agents on the manual round-trip fragment the 50KB document re-emission. author-calc takes caption+formula primitives and does the round-trip internally: read → unique-name splice into the target datasource (single non-Parameters default, candidates listed on ambiguity) → guard-validate → load → READBACK-VERIFY (completed does not prove applied). Caption collisions rejected pre-write. Built by GPT-5.5 minion, adjudicated firsthand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(desktop): fund author-calc under the 46k surface cliff — describe trims, ratchets tightened 46,691 → 45,753: prose-only trims across the six fattest describes; per-tool ratchets lowered to the new sizes. Load-bearing semantics restored after adjudication where the trim cut meaning (Call-2 proposal provenance on bind-template, plan-dashboard-creation pointer and applies-to-LIVE warning on build-and-apply-worksheet). Full suite 4,460 green, tsc clean, combined-lean under cap with headroom. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(desktop): calc knowledge — author-calc first; calc-editor door is human-in-loop only open-calc-editor-with-custom-calc returns completed WITHOUT committing (opens the editor pre-filled; open editor blocks the UI thread — live-proven twice 2026-07-19). Knowledge now routes: author-calc tool first, manual round-trip as fallback, calc-editor commands never in unattended runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(desktop): author-calc — only the top-level datasources block holds real datasources Live verse receipt (2026-07-19): worksheet views clone <datasource> elements, so a Superstore workbook offered 'Sample - Superstore, Sample - Superstore' as candidates, and splicing into the clone was silently discarded by Tableau (the readback-verify caught it). Selection now scans only the first <datasources> block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(desktop): author-calc — always splice at datasource end; relation columns are a position trap Live untaught-verse receipt (4 failed author-calc calls, readback-verify caught every one): <connection>/<relation>/<columns> holds schema <column ordinal=.../> nodes; insert-after-last-column landed the calc inside the relation block and Tableau silently discarded it. End-of-datasource is the position every successful live splice used tonight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(desktop): real-document replay fixture for author-calc + active-sheet law in spec knowledge Every author-calc bug tonight was invisible to synthetic fixtures and cost a live verse to find (datasource clones; relation-column position trap). The replay test runs the tool against a real saved Desktop document (username scrubbed) offline. Knowledge: generate-viz renders on the ACTIVE sheet — goto-sheet immediately before, or it clobbers a neighbor (live receipt: the agent detected the clobber via readback, restored the sheet, and rerouted — correct behavior, avoidable trap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): author-calc resolves sibling-calc caption references to internal names Live finale receipt (2026-07-19): all six author-calc calls succeeded, but 5 of 6 layered formulas referenced sibling calcs by CAPTION ([Member Profit], [Profit Tier]) — Tableau formulas resolve internal names, and authored calcs get [Calculation_N] names the agent cannot know, so every dependent calc flagged invalid and the chart leg died. The tool now rewrites bracketed caption tokens to internal names at splice time (base fields where caption equals name pass through untouched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): author-set — param-linked Top/Bottom-N sets via document round-trip Wraps the proven set shape (Segno's golden groupfilter + CODA's live param-link proof) as an author-* verb: primitives in (caption/dimension/orderBy/count/end), golden <group><groupfilter> XML server-side, spliced at datasource-end, readback- verified. count accepts a literal N or a [Parameters].[X] token — the melody plays over the key signature. Offline replay (5/5) incl. real 49KB Superstore document. Live-mapped this session (receipts): the document round-trip is the whole dialect — calcs/sets MERGE; parameters are OPEN-only (frozen to merge, all headless create/edit commands hit dlg.DoModal). Set authoring needs no parameter mutation: it references the param by token and Tableau resolves it at runtime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): author-action — parameter-change actions via document round-trip Live-proven this session (CODA): a workbook-level <actions> block MERGES via the round-trip — spliced <edit-parameter-action> targeting [Parameters].[Parameter 1] survived readback, link intact. This refuted the map-agent's OPEN-only assumption (the same trap sets fell into) — a live probe beat the reasoning. Wraps it as a verb: primitives in (caption/sourceWorksheet/sourceField/ targetParameter/activation), golden <edit-parameter-action> XML server-side, creates the workbook <actions> block if absent (between </datasources> and <worksheets>) or appends with a fresh [ActionN] name, readback-verified. Offline replay 4/4. The interactivity layer over the key signature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): format-labels verb + surface-fund the author-* trio under the 46k cliff format-labels: turns mark labels ON/OFF for a worksheet via the document round-trip (PROVEN live 2026-07-19 — <format attr='mark-labels-show'> in a pane MERGES and survives readback). Idempotent: rewrites an existing rule or inserts one; readback- verified. Offline replay 4/4. Funding: author-set/author-action/format-labels pushed the combined tool surface past the 46k tools/list auto-deferral cliff (and the combined-lean profile's). Per the ratchet doctrine (never raise the cap — trim to fit), emptied redundant single- token .describe() stubs across the fattest tools (plan-dashboard-creation, build-and- apply-dashboard, validate-proposal, dashboard-health-check, inject-template, dashboard -auto-apply, add-field, apply-worksheet, resolve-field, build-and-apply-worksheet). Field names + enums already carry the meaning; empty describe drops the JSON key entirely (verified). Lowered the grandfather caps to lock the wins in; removed the tools that dropped under the 1200B per-tool budget. Full suite 4476 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): author-parameter — the honest modulation verb (seed at OPEN, reopen explicit) Parameters are the ONE shape frozen to live merge (PROVEN live 2026-07-19, CODA: create/add/value-edit all silently refused; every headless create/edit command is a blocking dlg.DoModal). A parameter is born ONLY at OPEN. So this verb does the document surgery — splices the <column param-domain-type> into the Parameters ds (creating that ds if absent; an empty one is dropped on load, one with a real column survives) — writes a reopen-ready stage, and returns { stagePath, reopenRequired: true }. The reopen + re-pin is the serving layer's job, surfaced explicitly, NEVER hidden inside the tool (ghost gen-18: it's a session-lifecycle event). The reopen preserving merged calcs/sets/actions/formatting is live-proven. Parameters = the key signature; everything else is the melody merged over it. Offline replay 4/4. Funds the 5th verb under both 46k cliffs by emptying redundant describe stubs (get-worksheet-xml, apply-dashboard, remove-field). Full suite 4480 green, tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(knowledge): dynamic-authoring router — teach the 5 author-* verbs + key-signature/melody law New tactics/data/notional-spec-dynamic-authoring.md is the routing map that steers the agent to author-calc/author-set/author-parameter/author-action/format-labels instead of hand-XML, with the one governing law (params=key signature born at OPEN; calcs/sets/ actions/formatting=melody merged live) and the full WW44-shape recipe in build order. Cross-linked bidirectionally with the calc-authoring doc. Every claim live-proven this session. This is the teach-Sonnet layer — the verbs are the capability, this is discovery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(knowledge): dialog-command misclassification guard — 15 commands the reference marks safe but block The command reference marks 15 *DialogCommand-sourced commands as opens_blocking_dialog= false + agent_can_invoke=true; invoking them unattended pops a modal that wedges the session (live-proven: edit-existing-parameter hung 12s while health stayed OK). Names the 15, gives the trust rule (source name > derived flag), routes to author-* verbs instead, and records the generator-fix heuristic (source =~ /Dialog/ => force blocking). Refines the ghost's 'reference omission' into the real bug: misclassification, not omission. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): dynamic-authoring TOOL_PROFILE — the 10-tool singable no-XML surface Answers 'make it shorter': instead of trimming describe stubs to squeeze verbs under the 46k tools/list cliff, define a lean profile that IS the whole authoring language. TOOL_PROFILE=dynamic-authoring = spec-loop's 5 (execute-tableau-command for the NotionalSpec chart/dashboard spine + discovery/readback) + the 5 author-* verbs (calc/set/parameter/action/format-labels). Ten semantically-named tools cover the full WW44 dynamic dialect; zero agent-visible XML/cache/validation/template tools. Measured wire surface: 9,644 bytes vs the 46,000 cliff — 79% headroom (vs the full 45-tool surface that fights the cliff). The cliff stops being a fight structurally. Full set stays reachable via TOOL_PROFILE=full. Tests: registers exactly the 10, banishes XML/template tools, asserts <30k. Full suite 4482 green, tsc clean. Ghost gen-19 Move 1 (the ruthless surface already half-existed as spec-loop; this bolts the write half on and names it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): dynamic-authoring grows ask-user + search-commands — the 12-tool singable surface CODA's joy-cut overshot: a singer who cannot ask for help or discover the execute-tableau-command dialect is not lean, she is mute. Re-measured at 11,500 wire bytes — 75% headroom under the 46k cliff. * feat(desktop): author-parameter completes the reopen in-call — the key signature no longer stops the song CODA's one seam, closed tool-side: getExecutor discovers manifests dynamically and getDesktopConfig() re-reads env per call, so the tool itself relaunches the seeded stage (open -n, live-probed: manifest 2s, API-ready 3s), readback-verifies the parameter in the reopened doc, re-pins TABLEAU_DESKTOP_SESSION_ID in-process (only when a pin already existed), then SIGTERMs the old instance (probed: dies clean, no recovery sibling). Any failure degrades to the honest reopenRequired stage-on-disk result. stagePath is now optional — untaught agents never invent filesystem paths; the default stages beside the live workbook's own file. * chore: sync package-lock version to 2.26.0 * fix(desktop): reopen launches the bundle binary directly — open -a loses the document among instances Dress rehearsal caught two live defects the offline suite could not: (1) with multiple instances of the bundle running, 'open -n -a' starts a fresh instance but LaunchServices routes the document Apple Event unpredictably — the reopened instance came up EMPTY; spawning <bundle>/Contents/MacOS/<binary> <stage> detached carries the document deterministically (and puts it in argv). (2) The default stagePath cannot derive from the old process's argv for the same reason — it now stages under ~/Documents/My Tableau Repository/Workbooks (real user path; Desktop has crashed saving from sandboxed tmp dirs). E2E-proven live: reopened 78197→78328, param verified in the new instance's readback. * docs(knowledge): author-parameter reopens in-call — the router doc catches up to the shipped verb * feat(desktop): dynamic-authoring route joins the instructions — the verbs are named where the singer reads first Finale autopsy: untaught Sonnet landed both parameters through the in-call reopen but burned 55 of 65 calls spelunking raw commands for the set and dashboard, and never reached author-set — the tool list carried the verbs but the initialize instructions never routed a dynamic ask to them. Segno's discoverability law, one level up: ink the sign in the score she reads at note one. * fix(desktop): trim the dynamic-authoring route under the combined-lean 46k cliff The prior commit pushed combined-lean to 46,116 — the full suite caught it after an ungated commit chain (the lesson is the gate order, re-learned). * feat(desktop): dialog blocklist enforced in the param guard — the boo modal dies here revert-workbook-ui fired Error 47BF7751 on Matt's screen three times in one verse: the reference says safe, CODA's probe said modal, the knowledge doc knew, but nothing ENFORCED it. Now the guard refuses all 16 live-proven dialog commands outright, each with a FIX redirecting to the sanctioned alternative (author-parameter, author-action, NotionalSpec keys, or author-forward). Every annoyance dies exactly once. * fix(desktop): author-calc resolves PARAMETER captions to qualified [Parameters].[N] refs — the empty-sheet killer Verse-3 live receipt: the singer's filter calc named two parameters by caption; resolution only covered the target datasource's columns, so the refs never bound and the sheet rendered empty on Matt's screen. Parameter captions now resolve from the Parameters datasource to the qualified cross-datasource form, set after the field map so a caption collision resolves to the parameter — which is what a dynamic-ask formula means. * feat(desktop): notionalSpecGuard validates full v0.2 vocabulary — kills the silent-empty-sheet class Live eval receipt (2026-07-20 TUTTI Blake-set wave): a spec with aggregation:'none' (not v0.2) and a rangeFilter using max: (v0.2 is start/end) passed the guard, codegen generated NOTHING, and the /v0 success envelope shipped an empty sheet as a PASS. The guard now validates per-field keys/enums (data/type/role/aggregation/encoding, 'shelf' redirect, detail/tooltip v0.3 gate), sort, rangeFilters, relativeDateFilters (plural periods, next/previous), and categoricalFilters — every rejection carries a FIX line that teaches the correct vocabulary in-context. Accept fixtures are the six live-proven TUTTI golden specs; reject fixtures are the live failing payloads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(desktop): teach the NotionalSpec dialect in the census entries for the semantic-loop pair The dynamic-authoring profile has no knowledge tools, so search-commands is the singer's only reachable teacher — expertise:// pointers are dead ends there. The generate-viz entry now carries the v0.2 cheat-sheet (chart enum, field props, filter/sort shapes), the author-calc-then- reference-by-caption idiom (aggregation 'default', never 'none'), the string-date DATE() idiom, and the write-blind law (probed raw 2026-07-20: /v0 invokeCommand returns state:SUCCEEDED with no result, no operations route — verify by readback, always). The read-spec twin's entry now states its output is NOT returned by the /v0 API (product gap) and routes readers to document readback. Both entries get real parameter metadata (was []). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: autofix lint (prettier, import-sort) + explicit return type on test helper Green firsthand: lint clean, build ok, 4531 tests pass. CI was red on lint only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(desktop): mock spawnDetached in the 3 reopenFromStage tests that omitted it CI Tests step failed (both node matrices): the timeout + 2 success-path reopen tests fell through to defaultSpawnDetached, which spawned the real macOS Tableau binary — ENOENT on the Linux runner fired as an async unhandled rejection after the test passed, exiting the process 1. Passed locally only because the app path exists on macOS. All other reopen tests already mock it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): the singer sings native by default — unset TOOL_PROFILE now selects the 12-tool dynamic-authoring surface Flips the desktop default from the full 40-tool surface (raw XML get/apply + templates + cache) to the lean native-authoring surface: semantic loop via execute-tableau-command + the five author-* verbs + discovery/readback, zero agent-visible XML. 'full' is the explicit opt-in escape hatch; demo/spec-loop/ combined-lean unchanged. Consumers that need the XML surface set TOOL_PROFILE=full. TAS + the eval kit already set the profile explicitly, so the singer is unaffected; this makes native the DEFAULT for every unset consumer (ghost gen-19 standing move). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(e2e): desktop + combined list-tools expect the lean native default surface The TOOL_PROFILE default flip (unset -> dynamic-authoring) changes what both the standalone desktop server and the combined bundle's desktop half register when no profile is set. Assert the lean 12-tool native surface (raw XML tools opt-in via TOOL_PROFILE=full). Desktop list-tools verified locally; combined needs CI web creds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(desktop): data-loss guard matches double-quoted worksheet names (was failing open) Review P1: WORKSHEET_NAME_RE only matched name='...' (single quotes), so a live worksheet with a double-quoted name was invisible to the dropped-sheet check and could be silently dropped on reload — the exact data-loss this guard exists to prevent, failing OPEN. Match both quote styles (the pattern formatLabels.ts already uses for the same attribute). Regression test with a double-quoted live sheet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(e2e): skip flaky live Admin Insights TS-Events query (CI timeout) The live query-admin-insights-ts-events E2E times out at 30s in CI while sibling admin-insights queries return in <1s; it passes locally. This is server-side latency variance on the heaviest datasource, unrelated to the document-round-trip changes on this branch. Skipped with a re-enable note; tracked in the Studio dogfood backlog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… slim surface, Call-2 auto-apply, result-carried readback (#554) * feat(desktop): surface the document round-trip in the command reference (discoverability) Live root cause (2026-07-19, two banked receipts): on calc-heavy asks the agent searched 'underlying-metadata' -> 0 hits -> concluded 'not available in this build' -> fell back to hand-writing workbook XML and drowned. The verbs exist (executor maps them to GET/POST /v0/workbook/document) but had zero entries in the JSON that search-commands, validateKnownCommand, and paramContractGuard all read — undiscoverable AND uninvocable through the tool. Adds honest entries for tabui:save-underlying-metadata / load-underlying-metadata (load declares required in-param 'text'), allowlists both, and names the verbs in the zero-hit recommendation. Empirically gated: her exact failing query now returns the pair at ranks 1-2; 'calculated field' puts save at rank 1; guard rejects load{} and load{filepath} with actionable steers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): containment guard for load-underlying-metadata (whole-document or nothing) Adversarial-review P1 mitigation for surfacing the round-trip verbs: a pre-dispatch guard on execute-tableau-command rejects fragment loads (non- <workbook> root, no datasources/worksheets), loads that would DROP live worksheets (steer: delete-worksheet), and gross-shrink documents (<50% of live) — with fail-open on live-read infrastructure failure. Guard built by GPT-5.5 minion, adjudicated + full suite re-run firsthand (4453 green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(desktop): readback-verify law — a completed load envelope does not prove application Live-proven 2026-07-19 (Desktop main.26.0715, three probes): document loads that ADD columns or rewrite worksheet content apply; loads that REMOVE a datasource column are silently ignored — completed envelope, no effect. Teach: after every load, re-read with save-underlying-metadata and confirm the change landed. Applied to the load entry's description and the notional-spec-calc-authoring knowledge module. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(desktop): guard false-positive — real documents carry comments before the workbook root FATAL caught by live audition probe: Desktop documents open with a build comment between the XML prolog and <workbook> ('<!-- build main.26.0715 -->'); the root regex forgot comments, so the guard rejected EVERY legitimate whole-document load as a fragment — blocking the exact path it exists to protect. Live receipt: verse 2's 'fragment' rejections were false positives. Regression fixture uses the verbatim real head. Lesson (now in the stack skill): probe the guard's ACCEPT path against a real artifact, not only its reject paths against synthetic fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): author-calc tool — semantic calc authoring, no document in the agent's hands Live evidence (two drown receipts, 2026-07-19): agents on the manual round-trip fragment the 50KB document re-emission. author-calc takes caption+formula primitives and does the round-trip internally: read → unique-name splice into the target datasource (single non-Parameters default, candidates listed on ambiguity) → guard-validate → load → READBACK-VERIFY (completed does not prove applied). Caption collisions rejected pre-write. Built by GPT-5.5 minion, adjudicated firsthand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(desktop): fund author-calc under the 46k surface cliff — describe trims, ratchets tightened 46,691 → 45,753: prose-only trims across the six fattest describes; per-tool ratchets lowered to the new sizes. Load-bearing semantics restored after adjudication where the trim cut meaning (Call-2 proposal provenance on bind-template, plan-dashboard-creation pointer and applies-to-LIVE warning on build-and-apply-worksheet). Full suite 4,460 green, tsc clean, combined-lean under cap with headroom. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(desktop): calc knowledge — author-calc first; calc-editor door is human-in-loop only open-calc-editor-with-custom-calc returns completed WITHOUT committing (opens the editor pre-filled; open editor blocks the UI thread — live-proven twice 2026-07-19). Knowledge now routes: author-calc tool first, manual round-trip as fallback, calc-editor commands never in unattended runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(desktop): author-calc — only the top-level datasources block holds real datasources Live verse receipt (2026-07-19): worksheet views clone <datasource> elements, so a Superstore workbook offered 'Sample - Superstore, Sample - Superstore' as candidates, and splicing into the clone was silently discarded by Tableau (the readback-verify caught it). Selection now scans only the first <datasources> block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(desktop): author-calc — always splice at datasource end; relation columns are a position trap Live untaught-verse receipt (4 failed author-calc calls, readback-verify caught every one): <connection>/<relation>/<columns> holds schema <column ordinal=.../> nodes; insert-after-last-column landed the calc inside the relation block and Tableau silently discarded it. End-of-datasource is the position every successful live splice used tonight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(desktop): real-document replay fixture for author-calc + active-sheet law in spec knowledge Every author-calc bug tonight was invisible to synthetic fixtures and cost a live verse to find (datasource clones; relation-column position trap). The replay test runs the tool against a real saved Desktop document (username scrubbed) offline. Knowledge: generate-viz renders on the ACTIVE sheet — goto-sheet immediately before, or it clobbers a neighbor (live receipt: the agent detected the clobber via readback, restored the sheet, and rerouted — correct behavior, avoidable trap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): author-calc resolves sibling-calc caption references to internal names Live finale receipt (2026-07-19): all six author-calc calls succeeded, but 5 of 6 layered formulas referenced sibling calcs by CAPTION ([Member Profit], [Profit Tier]) — Tableau formulas resolve internal names, and authored calcs get [Calculation_N] names the agent cannot know, so every dependent calc flagged invalid and the chart leg died. The tool now rewrites bracketed caption tokens to internal names at splice time (base fields where caption equals name pass through untouched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): author-set — param-linked Top/Bottom-N sets via document round-trip Wraps the proven set shape (Segno's golden groupfilter + CODA's live param-link proof) as an author-* verb: primitives in (caption/dimension/orderBy/count/end), golden <group><groupfilter> XML server-side, spliced at datasource-end, readback- verified. count accepts a literal N or a [Parameters].[X] token — the melody plays over the key signature. Offline replay (5/5) incl. real 49KB Superstore document. Live-mapped this session (receipts): the document round-trip is the whole dialect — calcs/sets MERGE; parameters are OPEN-only (frozen to merge, all headless create/edit commands hit dlg.DoModal). Set authoring needs no parameter mutation: it references the param by token and Tableau resolves it at runtime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): author-action — parameter-change actions via document round-trip Live-proven this session (CODA): a workbook-level <actions> block MERGES via the round-trip — spliced <edit-parameter-action> targeting [Parameters].[Parameter 1] survived readback, link intact. This refuted the map-agent's OPEN-only assumption (the same trap sets fell into) — a live probe beat the reasoning. Wraps it as a verb: primitives in (caption/sourceWorksheet/sourceField/ targetParameter/activation), golden <edit-parameter-action> XML server-side, creates the workbook <actions> block if absent (between </datasources> and <worksheets>) or appends with a fresh [ActionN] name, readback-verified. Offline replay 4/4. The interactivity layer over the key signature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): format-labels verb + surface-fund the author-* trio under the 46k cliff format-labels: turns mark labels ON/OFF for a worksheet via the document round-trip (PROVEN live 2026-07-19 — <format attr='mark-labels-show'> in a pane MERGES and survives readback). Idempotent: rewrites an existing rule or inserts one; readback- verified. Offline replay 4/4. Funding: author-set/author-action/format-labels pushed the combined tool surface past the 46k tools/list auto-deferral cliff (and the combined-lean profile's). Per the ratchet doctrine (never raise the cap — trim to fit), emptied redundant single- token .describe() stubs across the fattest tools (plan-dashboard-creation, build-and- apply-dashboard, validate-proposal, dashboard-health-check, inject-template, dashboard -auto-apply, add-field, apply-worksheet, resolve-field, build-and-apply-worksheet). Field names + enums already carry the meaning; empty describe drops the JSON key entirely (verified). Lowered the grandfather caps to lock the wins in; removed the tools that dropped under the 1200B per-tool budget. Full suite 4476 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): author-parameter — the honest modulation verb (seed at OPEN, reopen explicit) Parameters are the ONE shape frozen to live merge (PROVEN live 2026-07-19, CODA: create/add/value-edit all silently refused; every headless create/edit command is a blocking dlg.DoModal). A parameter is born ONLY at OPEN. So this verb does the document surgery — splices the <column param-domain-type> into the Parameters ds (creating that ds if absent; an empty one is dropped on load, one with a real column survives) — writes a reopen-ready stage, and returns { stagePath, reopenRequired: true }. The reopen + re-pin is the serving layer's job, surfaced explicitly, NEVER hidden inside the tool (ghost gen-18: it's a session-lifecycle event). The reopen preserving merged calcs/sets/actions/formatting is live-proven. Parameters = the key signature; everything else is the melody merged over it. Offline replay 4/4. Funds the 5th verb under both 46k cliffs by emptying redundant describe stubs (get-worksheet-xml, apply-dashboard, remove-field). Full suite 4480 green, tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(knowledge): dynamic-authoring router — teach the 5 author-* verbs + key-signature/melody law New tactics/data/notional-spec-dynamic-authoring.md is the routing map that steers the agent to author-calc/author-set/author-parameter/author-action/format-labels instead of hand-XML, with the one governing law (params=key signature born at OPEN; calcs/sets/ actions/formatting=melody merged live) and the full WW44-shape recipe in build order. Cross-linked bidirectionally with the calc-authoring doc. Every claim live-proven this session. This is the teach-Sonnet layer — the verbs are the capability, this is discovery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(knowledge): dialog-command misclassification guard — 15 commands the reference marks safe but block The command reference marks 15 *DialogCommand-sourced commands as opens_blocking_dialog= false + agent_can_invoke=true; invoking them unattended pops a modal that wedges the session (live-proven: edit-existing-parameter hung 12s while health stayed OK). Names the 15, gives the trust rule (source name > derived flag), routes to author-* verbs instead, and records the generator-fix heuristic (source =~ /Dialog/ => force blocking). Refines the ghost's 'reference omission' into the real bug: misclassification, not omission. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): dynamic-authoring TOOL_PROFILE — the 10-tool singable no-XML surface Answers 'make it shorter': instead of trimming describe stubs to squeeze verbs under the 46k tools/list cliff, define a lean profile that IS the whole authoring language. TOOL_PROFILE=dynamic-authoring = spec-loop's 5 (execute-tableau-command for the NotionalSpec chart/dashboard spine + discovery/readback) + the 5 author-* verbs (calc/set/parameter/action/format-labels). Ten semantically-named tools cover the full WW44 dynamic dialect; zero agent-visible XML/cache/validation/template tools. Measured wire surface: 9,644 bytes vs the 46,000 cliff — 79% headroom (vs the full 45-tool surface that fights the cliff). The cliff stops being a fight structurally. Full set stays reachable via TOOL_PROFILE=full. Tests: registers exactly the 10, banishes XML/template tools, asserts <30k. Full suite 4482 green, tsc clean. Ghost gen-19 Move 1 (the ruthless surface already half-existed as spec-loop; this bolts the write half on and names it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): dynamic-authoring grows ask-user + search-commands — the 12-tool singable surface CODA's joy-cut overshot: a singer who cannot ask for help or discover the execute-tableau-command dialect is not lean, she is mute. Re-measured at 11,500 wire bytes — 75% headroom under the 46k cliff. * feat(desktop): author-parameter completes the reopen in-call — the key signature no longer stops the song CODA's one seam, closed tool-side: getExecutor discovers manifests dynamically and getDesktopConfig() re-reads env per call, so the tool itself relaunches the seeded stage (open -n, live-probed: manifest 2s, API-ready 3s), readback-verifies the parameter in the reopened doc, re-pins TABLEAU_DESKTOP_SESSION_ID in-process (only when a pin already existed), then SIGTERMs the old instance (probed: dies clean, no recovery sibling). Any failure degrades to the honest reopenRequired stage-on-disk result. stagePath is now optional — untaught agents never invent filesystem paths; the default stages beside the live workbook's own file. * chore: sync package-lock version to 2.26.0 * fix(desktop): reopen launches the bundle binary directly — open -a loses the document among instances Dress rehearsal caught two live defects the offline suite could not: (1) with multiple instances of the bundle running, 'open -n -a' starts a fresh instance but LaunchServices routes the document Apple Event unpredictably — the reopened instance came up EMPTY; spawning <bundle>/Contents/MacOS/<binary> <stage> detached carries the document deterministically (and puts it in argv). (2) The default stagePath cannot derive from the old process's argv for the same reason — it now stages under ~/Documents/My Tableau Repository/Workbooks (real user path; Desktop has crashed saving from sandboxed tmp dirs). E2E-proven live: reopened 78197→78328, param verified in the new instance's readback. * docs(knowledge): author-parameter reopens in-call — the router doc catches up to the shipped verb * feat(desktop): dynamic-authoring route joins the instructions — the verbs are named where the singer reads first Finale autopsy: untaught Sonnet landed both parameters through the in-call reopen but burned 55 of 65 calls spelunking raw commands for the set and dashboard, and never reached author-set — the tool list carried the verbs but the initialize instructions never routed a dynamic ask to them. Segno's discoverability law, one level up: ink the sign in the score she reads at note one. * fix(desktop): trim the dynamic-authoring route under the combined-lean 46k cliff The prior commit pushed combined-lean to 46,116 — the full suite caught it after an ungated commit chain (the lesson is the gate order, re-learned). * feat(desktop): dialog blocklist enforced in the param guard — the boo modal dies here revert-workbook-ui fired Error 47BF7751 on Matt's screen three times in one verse: the reference says safe, CODA's probe said modal, the knowledge doc knew, but nothing ENFORCED it. Now the guard refuses all 16 live-proven dialog commands outright, each with a FIX redirecting to the sanctioned alternative (author-parameter, author-action, NotionalSpec keys, or author-forward). Every annoyance dies exactly once. * fix(desktop): author-calc resolves PARAMETER captions to qualified [Parameters].[N] refs — the empty-sheet killer Verse-3 live receipt: the singer's filter calc named two parameters by caption; resolution only covered the target datasource's columns, so the refs never bound and the sheet rendered empty on Matt's screen. Parameter captions now resolve from the Parameters datasource to the qualified cross-datasource form, set after the field map so a caption collision resolves to the parameter — which is what a dynamic-ask formula means. * feat(desktop): notionalSpecGuard validates full v0.2 vocabulary — kills the silent-empty-sheet class Live eval receipt (2026-07-20 TUTTI Blake-set wave): a spec with aggregation:'none' (not v0.2) and a rangeFilter using max: (v0.2 is start/end) passed the guard, codegen generated NOTHING, and the /v0 success envelope shipped an empty sheet as a PASS. The guard now validates per-field keys/enums (data/type/role/aggregation/encoding, 'shelf' redirect, detail/tooltip v0.3 gate), sort, rangeFilters, relativeDateFilters (plural periods, next/previous), and categoricalFilters — every rejection carries a FIX line that teaches the correct vocabulary in-context. Accept fixtures are the six live-proven TUTTI golden specs; reject fixtures are the live failing payloads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(desktop): teach the NotionalSpec dialect in the census entries for the semantic-loop pair The dynamic-authoring profile has no knowledge tools, so search-commands is the singer's only reachable teacher — expertise:// pointers are dead ends there. The generate-viz entry now carries the v0.2 cheat-sheet (chart enum, field props, filter/sort shapes), the author-calc-then- reference-by-caption idiom (aggregation 'default', never 'none'), the string-date DATE() idiom, and the write-blind law (probed raw 2026-07-20: /v0 invokeCommand returns state:SUCCEEDED with no result, no operations route — verify by readback, always). The read-spec twin's entry now states its output is NOT returned by the /v0 API (product gap) and routes readers to document readback. Both entries get real parameter metadata (was []). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: autofix lint (prettier, import-sort) + explicit return type on test helper Green firsthand: lint clean, build ok, 4531 tests pass. CI was red on lint only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(desktop): mock spawnDetached in the 3 reopenFromStage tests that omitted it CI Tests step failed (both node matrices): the timeout + 2 success-path reopen tests fell through to defaultSpawnDetached, which spawned the real macOS Tableau binary — ENOENT on the Linux runner fired as an async unhandled rejection after the test passed, exiting the process 1. Passed locally only because the app path exists on macOS. All other reopen tests already mock it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): the singer sings native by default — unset TOOL_PROFILE now selects the 12-tool dynamic-authoring surface Flips the desktop default from the full 40-tool surface (raw XML get/apply + templates + cache) to the lean native-authoring surface: semantic loop via execute-tableau-command + the five author-* verbs + discovery/readback, zero agent-visible XML. 'full' is the explicit opt-in escape hatch; demo/spec-loop/ combined-lean unchanged. Consumers that need the XML surface set TOOL_PROFILE=full. TAS + the eval kit already set the profile explicitly, so the singer is unaffected; this makes native the DEFAULT for every unset consumer (ghost gen-19 standing move). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(e2e): desktop + combined list-tools expect the lean native default surface The TOOL_PROFILE default flip (unset -> dynamic-authoring) changes what both the standalone desktop server and the combined bundle's desktop half register when no profile is set. Assert the lean 12-tool native surface (raw XML tools opt-in via TOOL_PROFILE=full). Desktop list-tools verified locally; combined needs CI web creds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(desktop): data-loss guard matches double-quoted worksheet names (was failing open) Review P1: WORKSHEET_NAME_RE only matched name='...' (single quotes), so a live worksheet with a double-quoted name was invisible to the dropped-sheet check and could be silently dropped on reload — the exact data-loss this guard exists to prevent, failing OPEN. Match both quote styles (the pattern formatLabels.ts already uses for the same attribute). Regression test with a double-quoted live sheet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): cut model round-trips — bind-template on the slim surface, coherent instructions, census, live-session fields Live receipts (2026-07-20 slim battery) root-caused the slow eval: ~9 model turns/song at ~5s each while every tool result returns in <0.1s. Three of those turns were manufactured by the surface itself: - instructions advertised bind-template and XML fallback tools the dynamic-authoring profile does not register (m1 receipt: two search-commands calls literally hunting for 'bind template'), so bind-template joins the slim profile (13 tools) as the deterministic ~0.3s fast-path for plain chart shapes; route prose no longer names unregistered tools; - a command census prose entry lists the load-bearing tabdoc/tabui commands so search-commands is reserved for the uncommon path (m1: 13 of 23 turns were discovery thrash); - list-available-fields required a cached workbookFile and its error pointed at cut get-*-xml tools (e1 turn 1 burned on 'File not found'); workbookFile is now optional and omitting it reads the live session workbook. Instructions stay under the 46k tools/list deferral cliff (combined-lean budget test green). Full suite 4535 green, tsc clean, lint clean. Known tradeoff: DESKTOP_INSTRUCTIONS is profile-independent, so full-profile consumers lose the dashboard-auto-apply routing prose; instructions now match the default surface. Profile-aware instructions are a follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): Call-2 auto-apply + result-carried readback for generate-viz-from-notional-spec Live re-time receipt (e1, 60s/13 calls): a Call-2 bound result stranded the slim surface (auto-apply was Call-1-only and the manual apply tools are cut), costing 4 search-commands thrash turns; then 5 more turns re-verified a write-blind spec apply. - bind-template auto_apply now applies any bound fast-path-eligible result. A Call-2 proposal is binder-validated against the live workbook and applies under the same events-anchor user-change guard; the freehand fallback the model otherwise takes has fewer guards. - tabdoc:generate-viz-from-notional-spec success results now append a compact server-side readback (sheet, Rows/Cols shelves, mark, sort) resolved from the active window; fail-open, one metadata round-trip. The command reference's verify guidance now points at the result readback. - bind-template byte cap ratcheted down to 1629 (describe trim funded the gate-comment rewrite). Full suite 4537 green, tsc clean, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): retire the notional-spec dialect from routing — bind-template + author-* carry the song, refine-worksheet joins the slim surface NotionalSpec is retired (Matt, 2026-07-20). Three live pointers still steered the model at the dead command (jam gen-21): the plain-chart/dashboard/dynamic/ edit-in-place route prose, the command census, and the success-path hint strings author-calc and author-set return on every call. All re-pointed at the post-spec dialect: author the semantic object first, then bind-template the chart naming the authored caption; refine-worksheet (primitives-only top-N/sort) joins the 14-tool dynamic-authoring profile to carry edit-in-place. The spec command itself stays registered and functional — the door closes when the replacement chain is live-proven, not before. Full suite 4537 green, tsc clean, lint clean. Instructions +13 bytes, cliff-safe (refine-worksheet was already in the full surface the combined budget measures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): inline calcs[] on bind-template — author + bind in one call jam gen-22 Move 1 (receipt: the author-calc→bind chain costs ~9-13s of model turns per calc song). bind-template accepts calcs[{caption,formula,datatype, role}]: formulas validate BEFORE any document mutation (atomic error, zero apply on a bad calc), authoring reuses author-calc's splice/load/readback via the new shared authorCalcCore, and the bind + auto-apply see the post-calc workbook. authored_calcs surfaces in every outcome. Route prose updated. Per-tool cap raised 1591→1725 for the capability (slim surface is ~11.6k of 46k; combined-lean cliff stays the hard gate, green at ~45.9k). Full suite 4540 green, tsc clean, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): refine-worksheet sort_by_field + empty session treated as absent jam gen-21/22 Move 2 (receipt: m1 waterfall — tabdoc:sort 500'd on seven wire shapes, ~150s of blind model turns, before a manual computed-sort splice landed direction/using correctly). sort_by_field produces that same computed-sort shape through refine-worksheet's readback-verified apply path, with caption resolution matching the existing ops. resolveSession now treats empty/whitespace session strings as absent (was a wasted error turn: session:'' against a pinned Desktop). Full suite 4548 green, tsc clean, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(desktop): tabdoc:sort is dialog-natured — block with redirect; teach the true sort contracts Live receipts (2026-07-20): tabdoc:sort popped blocking modals on the user's screen (47BF7751 'missing: global-field-name') after seven opaque 500s. The monolith's own sort-cmd.data settles it: SortCommand drives the sort UI dialog (updateSortDialog notification) — it was never agent-safe — and SortNested is the programmatic sort that existed all along. - execute-tableau-command now refuses tabdoc:sort pre-dispatch with a FIX line redirecting to refine-worksheet sort_by_field or tabdoc:sort-nested. - Reference corrected: Sort marked dialog-risky with the true wire contract (global-field-name, Type-required-unless-clear, computed-sort MeasureName); sort-nested pinned with its required params. - paramContractGuard enforces sort-nested's required params pre-dispatch. Full suite 4556 green, tsc clean, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(desktop): shared slot vocabulary — sort + top_n on the bind proposal (#555) * feat(desktop): shared slot vocabulary — sort and top_n on the bind proposal jam gen-23 Move 1 (receipts: m1's order-by strand; the ranking family cannot say 'top 10'). The proposal schema gains two optional concepts — sort {by, direction} and top_n — advertised in PROPOSAL_OUTPUT_SCHEMA so Call-2 models learn them from the propose payload itself, validated against the workbook schema BEFORE apply, spliced via the computed-sort/top-N helpers extracted from refine-worksheet (built earlier today — reused, not duplicated), atomic failure on splice errors. One bind call now carries ordering and limits; no refinement tail. Growth law honored: receipt-backed concepts only, binder- validated, riding the one-call path. Full suite 4567 green, tsc clean, lint clean. combined-lean 45,992/46,000 — 8 bytes headroom; next growth must trim first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(desktop): complete the slot-vocabulary commit — validate seam + describe trims These two files were part of the sort/top_n packet but missed the explicit staging list; the working-tree suite masked the gap (adjudication lesson: git-status-clean check BEFORE declaring a packet landed). Without them the pushed tree fails its own validate-proposal byte cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(desktop): literal names at every tool boundary — the ampersand bug dies Receipt (m1, 2026-07-20): a sheet named 'P&L Waterfall…' surfaced entity- escaped through list-worksheets, made refine-worksheet miss by literal name, and silently confirm-miss (refined:false) by escaped name — any &-titled sheet was unreachable. Now: names decode exactly once at output boundaries (list-worksheets/dashboards, bind-template sheet_name/guidance), name inputs resolve normalized-to-normalized with escaped-input tolerance, apply gates compare normalized, and refine uses the canonical literal name from fetched XML for apply/readback/output. Coverage for & < " in literal and escaped paths. (Includes a sliver of the prior commit's file overlap — the two commits were disentangled from one worktree; suite green across both.) Full suite 4582 green, tsc clean, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…penapi (#557) Tighten the hand-derived Athena V0 contract against the live /openapi.json (0.1.0, captured 2026-07-20) plus live-probe evidence: - Operation envelope: operationId -> id, add kind, id/kind/state required, warnings[] added; error is now a distinct OperationError {code, message} instead of the conflated Problem shape. result stays optional (absent from the spec; kept until output-param behavior is confirmed with the API owner). - Problem: RFC-9457 shape (+instance), PROBLEM_CODES 5 -> the spec's 16-value x-extensible-enum. Fields stay optional so error extraction fails open. - Mock server emits the spec-true wire shapes (incl. 401 code unauthenticated; the internal 'unauthorized' variant maps at the status boundary). - NEW contract-intake harness (externalApiContract.test.ts + captured-spec fixture): the next spec drop is a red/green diff keyed off our schemas, not a manual reread. Also pins invokeCommand as deliberately undocumented. Full suite 4600 green; externalApi 58/58; eslint clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#558) * feat(desktop): route calc/derived-field asks through the author verbs TAS !54 removed tool-by-name teaching from its system prompt (prompts must not name tools); the routing's correct home is this server's own instructions. The dynamic-authoring route trigger now covers calc/derived-field asks, paid for inside the 46k tools/list byte budget by dropping the redundant census clause. The full last-resort document round-trip law lives in the calc-fields knowledge resource (zero tools/list cost). Live routing verification (calc-song re-sing) deferred to a live-Desktop session. Co-Authored-By: Claude <noreply@anthropic.com> * docs(knowledge): purge the retired NotionalSpec dialect from the served corpus The four notional-spec-*.md files (landed #543, extended #552) taught a dead command as the primary path — the eval singer's corpus was steering agents at a corpse. Deleted; every still-true fact relocated stripped of spec framing: the author-* verb routing map + OPEN/MERGE law to the new dynamic-dashboard-authoring.md, calc-command pitfalls to calc-fields.md, column-removal round-trip gotcha to round-trip-normalization.md, error-class diagnosis ladder to recovery.md. Steering surfaces (search fallback, commands-reference recommendations, guard FIX pointer) now teach the current ladder. The defensive guards stay until the spec door is hard-closed. Co-Authored-By: Claude <noreply@anthropic.com> * style: prettier line wraps (CI lint) Co-Authored-By: Claude <noreply@anthropic.com> * style: prettier line wrap in the guard test Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
… errors (#559) Live eval receipt (m1 waterfall, 2026-07-20): sort-nested passed local param validation then returned HTTP 500 three times before the singer found the document round-trip — ~30s and several model turns burned re-guessing. Error-path teaching only (fail-open: no pre-dispatch block — the server may fix the command any build): execution errors for mapped commands append a FIX line naming both blessed fallbacks (bind-template sort proposal; doc round-trip). Unmapped commands byte-identical. Suite 4603 green, lint clean. Co-authored-by: Claude <noreply@anthropic.com>
…er template sort (#560) * feat(desktop): waterfall anchor_category slot + sort proposal wins over template sort Live eval receipts (m1 waterfall FAIL 34/100, 2026-07-20): (1) the singer's sort override 'hit a template limitation' — part-to-whole-waterfall hard-codes computed-sort DESC by SUM(Profit) and the sort field wasn't declared in datasource-dependencies, so the proposal was inert; the singer burned 3 sort-nested 500s + a doc round-trip forcing ASC-by-display_order. (2) subtotal/ total rows were running-totaled with the increments they summarize — final bar ~2x true Net Income. - sort proposal now replaces the built-in computed-sort (declares the sort column/column-instance when missing); every failure path is fail-open with a warnings[] channel on the bind result — template default kept, bind never blocks - new optional virtual slot anchor_category: when bound, splices a categorical EXCLUDE filter (subtotal/total) so anchors leave the bridge; unbound binds are byte-identical (characterization-pinned) - teaching lives in the manifest slot description (model-facing at bind time), NOT tools/list: bind-template/validate-proposal ratchets TIGHTENED 1908->1886 / 1555->1533; combined-lean 45,952 of 46,000 - served-corpus knowledge twins: waterfall subtotal/total law + zero-denominator ratio-sort guard (both analytics paths — legacy twin is live) Suite 4608 green (316 files), lint clean, invariants unchanged. v2.27.0. Co-Authored-By: Claude <noreply@anthropic.com> * fix(desktop): warnings typing on result base + result-carried waterfall discovery hints CI tsc caught warnings? missing on BindTemplateToolResultBase (vitest transpiles without typecheck — the gate that matters is npx tsc). Live re-sing receipt (m1, judge 34 both pre/post-slot): the slot shipped but was never FOUND — a confident bind has no Call-2, so manifest descriptions are never read, and the routeTable teaching was trimmed for the 46k cliff. A capability without a discovery channel is a no-op. Result-carried teaching (the #554 pattern, zero tools/list bytes): waterfall bind results now hint (a) the anchor_category re-call naming the schema's category-like column when the slot is unbound, (b) the one-call sort override when no sort proposal was passed. 6 tests pin presence/absence per case. tsc 0, suite 4616 green (316 files), lint 0 — pipe-proof exits. Co-Authored-By: Claude <noreply@anthropic.com> * fix(desktop): sort splice handles the serializer's paired-empty computed-sort form Third live sing receipt: the singer finally passed the full correct proposal (anchor_category + sort by Display Order asc) and the splice refused it — 'only the safe self-closing <computed-sort/> can be changed'. Root cause: the SOURCE template is self-closing but inject serialization expands it to <computed-sort ...></computed-sort>; the safe-form detector missed our own serializer's output. Safe target detection now covers self-closing, paired-empty, and plain-wrapper forms; truly nested <sort-computation>/manual forms still refuse (the computed-sort-crash class — Desktop crash guard intent preserved, evidence in computedSortCrash.ts). bind-template and refine-worksheet sort_by_field share the helper, so both inherit the fix. Characterization test pinned to the real inject-serialized waterfall shape (failed before, passes after). tsc 0, suite 4620 green (316 files), lint 0 — pipe-proof exits. Co-Authored-By: Claude <noreply@anthropic.com> * fix(desktop): waterfall seam-4 — sort the shelf axis with anchor_category bound; surface knowledge tools Two coupled fixes so a P&L waterfall can carry BOTH an anchor exclude-filter AND a proposal sort, and so the singer can actually reach the corpus that already documents how. Seam 4 (planSortByFieldOnCategoricalAxis): the categorical-axis count included filter-only dimension CIs. A waterfall's anchor_category exclude-filter adds a nominal/None CI that lives only in <filter>; it was counted as a rival axis and tripped "more than one categorical axis is present; sort is ambiguous." Restrict axis candidates to dims whose DS-qualified ref appears on a rows/cols shelf, mirroring planSortInsertion's proven membership check. Substring membership is exact for CI refs (the :suffix] grammar means one ref is never a substring of a longer one; ds + ci.name share the shelf text's escaping domain, e.g. P&L Data). Fail-before/pass-after test uses the real part-to-whole-waterfall template + spliceWaterfallAnchorFilter; adds a prefix-boundary case (RegionName vs Region) and keeps the genuine two-shelf-axis refusal. Knowledge tools: the default dynamic-authoring profile omitted list-knowledge-resources + read-knowledge-resource, yet the singer's system prompt instructs "consult the expertise library BEFORE authoring" — so the corpus (which already states the waterfall subtotal/total exclusion rule and the Top-N-needs-a-context-filter rule) was unreachable on every sing. Add both to the profile (14→16) and sharpen their descriptions to say WHEN to reach (net -27 bytes; combined-lean tools/list surface stays under the 46k cliff with more headroom than before). No a2td port owed — a2td's refine is a different (operation-based) architecture and always registers the knowledge tools. Gates: tsc 0, vitest 4623 passed/1 skipped, lint 0. Andy-review addressed. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
* feat(desktop): waterfall bind hint names the step-order column (routes off-view sort into the bind)
Live m1 receipts (N=3: 83 PASS / 38 FAIL / 82 PASS): the FAIL and the expensive
PASS both came from the same seam — the singer bound the waterfall plain, then
tried refine-worksheet to sort by display_order, which refuses ("unknown sort-by
field") because refine can only sort by a field already on the view. Run 2 gave
up (magnitude order → wrong bridge); run 3 forced it with two rounds of
load-underlying-metadata XML surgery.
The bind ALREADY supports proposal.sort:{by:"display_order"} (ensureSortByColumnDependency
injects the off-view column; tested). The gap was discoverability: the generic
WATERFALL_SORT_HINT said "override with proposal.sort:{by:<field>}" — too vague to
connect to the sequence column. Now, when the schema carries an explicit
sequence/order column (display_order, sort_order, ordinal, …, matched precisely —
line_item and order_count do not false-positive), the hint NAMES it and prescribes
the exact re-call, and says plainly not to use refine (it can't sort off-view).
Mirrors the working anchor_category discovery hint. Falls back to the generic hint
when no order column exists.
Gates: tsc 0, bindTemplate suite 53 passed, lint 0.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(desktop): waterfall anchor hint is imperative + evidence-grounded (kill the hedge)
Live m1 receipt (run 5, FAIL 42): the singer got the order right but its OWN closing
report said "I didn't bind the Category field in… let me know if that's the case and
I'll wire Category in as an anchor to fix it." The data plainly had a category column
with subtotal/total rows; the running total double-counted; groupfilter function=except
count in the whole receipt was 0. The anchor hint's advisory tail ("totals double-count")
read as a WARNING the singer could hedge on, not a directive.
Reword the anchor discovery hint to be imperative and grounded in the schema evidence:
"schema has <col> — a row-type column means this P&L data almost certainly has
subtotal/total rows the running total WILL double-count. Re-bind NOW … do NOT ask the
user or leave it unbound." Advisory wording invites the hedge; a data-grounded directive
closes it. Companion to the step-order hint in this branch.
Gates: tsc 0, bindTemplate suite 53 passed, eslint 0.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(desktop): waterfall bind DEFAULTS the step order to a sequence column
The deterministic completion of the waterfall-hints work. Live m1 receipts (N=3
post-hints): the singer lands the display_order sort in the confident bind only ~1/3
of runs (run 6 PASS 90); the other runs bind plain, attempt the sort at an
intermediate state where it doesn't resolve, and settle for the template's
DESC-by-measure default (runs 7/8 FAIL 43/48, order gate missed) — a build-SEQUENCE
nondeterminism, not a missing capability (the final shape sorts fine, proven against
the recovered stage .twb).
Fix the sequence problem by making the ONE confident bind complete: in the shared
validateAndBuild, when the template is part-to-whole-waterfall AND no usable sort was
proposed AND the schema carries a sequence/order column (display_order/sort_order/…,
matched precisely — Order Date and measures do not false-trigger) that resolves
unambiguously, default proposal.sort to that column ascending, with a warning naming
it. An explicit proposal.sort still wins; no order column → template default kept
(never guess). Waterfall-only guard; 683 other binder tests unaffected.
Gates: tsc 0, full vitest 4627 passed, lint 0. 3 new tests (defaults with order col /
no default without / explicit override).
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(desktop): share one WATERFALL_ORDER_FIELD_RE; name rejected order candidates
Andy-review response on the waterfall-order work:
- De-duplicate the sequence/order-column regex: export it once from binder.ts (the
apply side) and import it into bindTemplate.ts (the hint side) instead of two copies
that could drift. bindTemplate already imports from binder, so no layering violation.
- When >1 sequence column matches, the default-sort warning now names the rejected
candidates ("also matched: sort_order, seq") so the agent/user can correct the
deterministic first-match pick.
Gates: tsc 0, full vitest green, lint 0. Behavior unchanged (same regex, same first-match
default); this is clarity + drift-safety.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(desktop): waterfall bind DEFAULTS anchor_category to a row-type column
The reliability completion of the m1 waterfall work. Live receipts (N-run): even with
the sort default + imperative anchor hint, the singer landed the subtotal/total EXCLUDE
filter only ~half the runs (hedged or skipped anchor_category), so m1 dropped a different
gate run-to-run. Make the anchor deterministic the same way as the sort: in the shared
validateAndBuild, when template=waterfall AND no anchor_category was bound AND the schema
has a category/row-type dimension (categor|type|kind|class|flag|marker) not already bound
to another slot, inject the anchor binding BEFORE validation — it resolves through the
normal path into field_mapping['Anchor Category'] → spliceWaterfallAnchorFilter (the
subtotal/total exclude). Fail-open: if the added anchor breaks validation, drop it and
bind the caller's original (never turn a good bind into an escalation), with a warning.
No row-type column → no anchor. Explicit anchor still wins.
With sort + anchor both defaulted, m1 now passes all three gates deterministically:
N=3 clean live PASS (84/90/85, anchor except-filter x3 each, all gates green).
Gates: tsc 0, full vitest 4629 passed, lint 0. 2 new binder tests (defaults w/ row-type,
no-default without).
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(desktop): honest anchor-default warning + share the anchor regex/slot-id (review)
Andy-review of this PR surfaced three real items in the anchor auto-default:
1. (open-question, fixed) The success warning asserted "excluded subtotal/total
rows" but spliceWaterfallAnchorFilter only excepts members literally named
"subtotal"/"total" — on a plain waterfall whose auto-bound category has no such
members the filter is inert, so the warning claimed an exclusion that never
happened. Reworded to describe what was ADDED (a filter that excludes any
subtotal/total members), not an exclusion that occurred. Agent-facing honesty.
2. (nit, fixed) WATERFALL_ANCHOR_SLOT_ID / WATERFALL_ANCHOR_FIELD_RE were declared
identically in binder.ts AND bindTemplate.ts — the exact drift risk this PR
eliminated for WATERFALL_ORDER_FIELD_RE. Now exported from binder.ts and
imported in bindTemplate.ts, one definition each.
3. (nit, fixed) Replaced the overloaded 'FAILED' string sentinel on
defaultedAnchorField with a dedicated anchorDefaultFailed boolean.
binder.test.ts: the anchor-default warning assertion now checks the honest wording.
All 98 binder/bindTemplate tests + 23 server.desktop guards (byte-cliff, vocab) green.
Version 2.27.0 -> 2.28.0 (this PR was missing its bump).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
… definitions (#563) Two served-corpus additions surfaced by the live m1/h6 eval cycle (the singer now reads knowledge — it's on the dynamic-authoring profile as of #560): - advanced-chart-builds waterfall bullet: the running total is order-dependent, so when the intended P&L order is a NON-displayed sequence field (display_order), carry it in the ORIGINAL bind (proposal.sort:{by,direction}) — refine-worksheet cannot sort by a field not on the view. Kills the m1 "sorted by magnitude" miss when the singer reaches knowledge before building. - new strategy/analytics/profitability-margin-definitions: gross = (revenue - COGS)/ revenue; operating subtracts opex; net subtracts everything; contribution subtracts only variable cost. The h6 song fails if the singer folds opex into a "gross margin" ask. Arithmetic (ratio-of-sums, zero-guard) already lives in the aggregate-ratio + calc-authoring entries; this pins WHICH costs each margin subtracts. Both pass house-format (13617 / 6238 chars, all required sections). a2td source carries the margin entry on claude/h6-margin-knowledge; this vendors both into the tmcp served corpus the desktop singer actually reads. Co-authored-by: Claude <noreply@anthropic.com>
…ontinuous date axis (#565) * feat(desktop): temporal_axis_from_string — bind a string month to a continuous date axis The judge's suggested_fix for e4 (MAU-over-time): the trend-line template's order_date slot requires a real date/datetime, but e4's dataset carries a STRING "2025-08" month, so the bind escalated with kind-mismatch and the singer fell back to value-sorted bars (summing both products) or dead-ended asking the user. This adds an opt-in path — only for a temporal slot that sets temporal_from_string (trend-line-chart's order_date) — that accepts a date-like string source and, on the apply side, injects a DATEPARSE calc so the Month-Trunc axis truncates a real parsed date instead of the raw string. One confident bind now renders a continuous month line off a string month. Pieces: - stringTemporal.ts: inferStringTemporal — accepts a STRING dimension whose NAME is date-like (month/date/period/…) and infers a DATEPARSE format (yyyy-MM for month, yyyy-MM-dd otherwise). Fail-closed: a non-date-like name stays a kind-mismatch. - validate.ts gate 3: the temporal kind-mismatch is waived for an opted-in slot whose string field inferStringTemporal accepts; the slot's field_mapping key is then SKIPPED (the apply splice owns its XML) and a DateparseAxisSpec rides out on the ValidateResult → InjectTemplateArgs. - dateparseTemporalAxis.ts: apply-side splice (modeled on waterfallAnchorFilter) that converts the template's date base column into a DATEPARSE calc and declares the bound string source column, leaving the Month-Trunc CI + every shelf/format ref byte-unchanged (no repoint, no duplicate-CI risk). Identity no-op when no axis requested; fail-closed if the base column can't be converted. - trend-line-chart manifest: order_date opts in (temporal_from_string:true); index regenerated. Tests: 9 splice + 6 inferrer + 2 end-to-end (real trend-line template → coherent DATEPARSE calc + intact namespaced Month-Trunc axis; real-date path byte-identical). Full binder/templates/server suites green (858), byte-cliff unchanged (feature adds no tool-surface bytes), tsc + eslint clean. Version 2.27.0 -> 2.30.0. NOT YET LIVE-VERIFIED: the DATEPARSE format is inferred from the field NAME (the binder sees schema, not cell values), and a wrong format renders a blank axis. The opt-in is enabled on trend-line but MUST be confirmed with a live render against e4's real "2025-08" data before it is trusted in production — XML validity is proven, value parsing is not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(desktop): surface trend-line-chart for natural time-series phrasings (e4 discoverability) The live e4 sing proved temporal_axis_from_string was UNREACHED: the singer never proposed trend-line-chart for "Show me monthly active users over the last 12 months" — that ask scored ZERO whole-token hits against the template's intent_keywords (over-time ≠ "over the last", by-month ≠ "monthly", "line" absent), so trend-line never surfaced as a Call-1 candidate. The singer guessed a nonexistent "line" template, got the general-tools prefill, and fell back to magnitude-simple-bar (bars, both products summed) → FAIL. Add the phrasings a natural time-series ask actually uses: monthly, weekly, daily, per-month, over-the-last, last-12-months, over-months, active-users, mau, dau, line-chart. Probe confirms trend-line-chart now surfaces as a candidate for the MAU ask (was absent). 688 binder/classifier tests green — no keyword-hijacking regression on the other songs. This is the discoverability half of the e4 fix; the temporal_axis_from_string slot (prior commit) is the capability half. Both are needed: the singer must reach trend-line AND be able to bind the string month to order_date. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…timeout (#567) fix(ci): widen vitest teardown window to stop the flaky onTaskUpdate RPC timeout CI's Tests step went red on #564 with `[vitest-worker]: Timeout calling "onTaskUpdate"` thrown as an UNHANDLED error AFTER all 317 test files passed — vitest then exits 1 on the unhandled error, a flaky red on green code. It's a pool→reporter RPC starvation under a ~157s parallel run (the final task-update exchange times out under load), not a test failure: #565 ran the same suite and passed both Node jobs. Set `teardownTimeout: 30_000` in the shared vitest config so the teardown/RPC window is wide enough for the final reporter exchange to complete under CI load. Config-only; local run still green, tsc + lint clean. Version 2.29.0 -> 2.29.1. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…564) * feat(desktop): search-knowledge tool — targeted top-N over list-all (fixes over-consultation) Adds a `search-knowledge(query, limit=5)` desktop tool that ranks the expertise corpus with fuse.js and returns only the top few relevant modules (uri + title + snippet), ported from the a2td side. This replaces the discover-then-read path that forced the model through `list-knowledge-resources` (an unbounded dump of all URIs) before it could find anything. Why: the ETUDE A/B found that surfacing the raw list+read tools REGRESSED a hard ask (e4, string->date calc) PASS->FAIL — the model consumed the entire 108-entry corpus instead of the one relevant module (fragility ledger Shape 10). A targeted search returns the 1-2 modules that matter, so a hard ask gets guidance without a wholesale-read spiral. - searchKnowledge() in desktop/knowledge/index.ts: same weighted-key fuse config as a2td search.ts (searchTerms .3 / tags .22 / title .2 / whenToUse .18 / slug .06 / body .04, threshold .4), lazy-built + cached index, skips _-meta files. - registered on toolName, tools, and the DYNAMIC_AUTHORING_TOOL_PROFILE. - list/read descriptions now steer to search-knowledge first; trimmed cache/validate descriptions to keep the tools/list surface under the 46k ToolSearch cliff. - knowledge.test.ts: relevance assertions (target entry ranks #1 for its ask; no cross-capture between sibling entries). Version 2.27.0 -> 2.28.0 (new tool). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(desktop): bump to 2.29.0 (avoid 2.28.0 collision with #562) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(knowledge): collapse it.each relevance cases into loop-driven blocks (unflake #564 CI) * test(knowledge): raise suite timeout for the genuinely-slow relevance searches (fix #564 CI) Root cause of #564's deterministic CI red (found after the it.each collapse made it surface as a real failure instead of an unhandled RPC error): each fuse.js search scans the full document `body` of the ~108-doc corpus with ignoreLocation:true — ~450ms/query, real work, not a leak. A multi-case relevance block runs several such searches, so on a slow/contended CI worker it exceeds vitest's 5s DEFAULT testTimeout ("Test timed out in 5000ms"), and the hung test also starves the pool's onTaskUpdate reporter RPC (the earlier "flaky" symptom). Local runs just squeak under 5s, which is why it passed here and failed in CI. Fix: `describe('knowledge/search', { timeout: 30_000 }, ...)` — give the honestly- slow relevance blocks headroom on CI. No production code changed; ranking behavior and coverage (44 query→suffix pairs) are untouched. Local: 13 tests pass; tsc + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(knowledge): drop full-body fuse key (9x faster search) — the real #564 CI fix ROOT CAUSE of #564's CI failure (found after ruling out timeouts): fuse.js searched the full document `body` of the ~108-doc corpus with ignoreLocation:true — ~450ms PER QUERY, the dominant cost. knowledge.test.ts's ~44 relevance searches took ~18s, CPU-blocking a vitest worker past the pool's hardcoded 60s onTaskUpdate reporter-RPC deadline under parallel CI load (the "flaky"/timeout symptoms were all downstream of this). No RPC-timeout config knob exists, so the fix is to make search actually fast. - src/desktop/knowledge/index.ts: remove `body` (weight 0.04) as a fuse SEARCH key. Ranking is driven by curated metadata (searchTerms/tags/title/whenToUse/slug); body added negligible signal. `body` is KEPT on the doc for the result snippet. Search drops from ~450ms to ~ms/query; the test file runs 18.6s → 2.0s. - one relevance case regressed (the gantt query matched only in body prose) → carried its exact phrasing into the doc's curated searchTerms, restoring the #1 rank the right way (metadata, not full-text). All 44 relevance pairs green; 13 tests, 2.0s. This also speeds the PRODUCT search the singer uses — a real improvement, not just a test fix. (a2td source-of-truth copy of the searchTerm needs the same port — follow-up.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(desktop): route cache-mutation tools through resolveSession so sidecars honor the session pin The four cache-mutation tools (write-cached-xml, add-field, remove-field, inject-template) stamped the fingerprint sidecar with the raw agent-supplied session arg instead of the resolved pin. When TABLEAU_DESKTOP_SESSION_ID was set, a mismatched or absent session arg produced a sidecar fingerprinted to the wrong instance, so downstream apply-* tools either failed closed with CacheSessionMismatchError or bled cache across instances. Each tool now resolves the session via resolveSession() at callback start, rejecting a conflicting explicit session and stamping the sidecar with the pin. * test(desktop): make pin-wins tests distinguish resolved pin from raw session arg The positive 'stamps the sidecar with the pinned session' tests defaulted the requested session to SESSION, which equals the pin, so the assertion held whether the code stamped the pin or the raw arg. The helpers now preserve an explicitly-undefined requested session (distinct from the pin) while still defaulting to SESSION when omitted, so a regression to stamping the raw arg fails the test.
One-page pointer for the Pulse/Chiron flow: how an insight card becomes a rendered viz via one deterministic bind-template call — card-field → slot-kind mapping, a routing table over the render-verified launch templates, the carry-the-proposal-in-the-card recommendation, and the shared active-sheet → datasource LUID dependency. Co-authored-by: Claude <noreply@anthropic.com>
…tasource-luid) (#570) * @W-23202034: Prevent sign-out teardown from masking the real REST error (#470) useRestApi signs out ephemeral (pat / direct-trust) sessions in a `finally` block. Because a throw inside `finally` replaces whatever the `try` was returning or throwing, an un-caught sign-out failure clobbered the callback's real outcome: a callback that 404s on a missing resource surfaced to the caller as the sign-out's 401 once the session was torn down. That is the misleading "delete-workbook returns 401 for a non-existent workbook LUID" report. Sign-out is best-effort cleanup (the ephemeral session expires on its own), so isolate it in its own try/catch and swallow-and-log any failure at 'warning'. The callback's result or error now always reaches the caller intact. Tests: two unit cases in restApiInstance.test.ts inject a rejecting signOut and assert the callback's error (and a successful result) survive teardown. Both fail without the fix. e2e is N/A — the defect only manifests when signOut itself fails at teardown, which a live run cannot force deterministically. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23202054: Stop get-stale-content-report from silently ignoring invalid projectIds (#473) * @W-23202054: Stop get-stale-content-report from silently ignoring invalid projectIds get-stale-content-report accepts a projectIds list to scope the report. Two resolution paths silently dropped requested IDs: 1. resolveProjectScopeIds dropped IDs outside the server's bounded context (INCLUDE_PROJECT_IDS) with no signal. 2. resolveProjectIdsToNames intersected requested IDs against real site projects; IDs matching no project vanished. Worst case: when EVERY requested ID was invalid, the scope collapsed to empty and buildSiteContentQuery treated it identically to "no scope" — returning the ENTIRE site instead of nothing. A typo in a project scope silently widened the report to all content. Fix: - Track dropped IDs through both resolution paths (unknown-on-site vs not-permitted-by-config) and surface them in mcp.warnings, reusing the result convention established by query-datasource. - Widening guard: when a scope was requested but nothing resolved to a real project, return an empty report (0 rows) plus warnings — never fall through to the unscoped full-site query. - Back-compat: no mcp key is emitted when all requested IDs are valid. Test coverage: partial-invalid (scoped to valid subset + warning), all-invalid (empty report, asserts no VDS query fires), bounded-context drop, all-valid back-compat, plus direct unit tests for the new resolver return shapes and warning assembly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23202054: Branch warning wording on whether a valid scope remains; drop env-var name from output Addresses PR review (Matt): 1. The ignored-projectIds warning message was unconditional — "The report was scoped to the remaining valid projects." fired even in the all-invalid case, asserting a valid subset that does not exist. That false premise is exactly the silent-widening confusion this fix targets. The message now branches on whether any requested project survived: when none do, it states an empty report (0 rows) was returned instead of the full site, giving an LLM caller a self-correction signal. 2. The not-permitted-by-config message no longer names the INCLUDE_PROJECT_IDS env var — "outside this server's configured project scope" carries the same signal for a caller without coupling tool output to a config key. (The docs page still names the env var; that prose is admin-facing config reference.) Tests: added buildProjectIdWarnings cases for both message branches and the env-var-not-leaked assertion; strengthened the all-invalid callback test to assert the empty-report wording. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-22030892: Add friendly client-name mapping for OAuth client_id values in UIP telemetry (#479) * @W-22030892: Add friendly client-name mapping for OAuth client_id in tool telemetry Bearer clients now emit two additive tool_call properties: oauth_client_id (raw CIMD-URL client_id, canonical) and oauth_client_display_name (host-keyed static map -> Claude/Cursor/VS Code; unknown clients fall back to the raw value; absent auth emits '' matching existing optional-prop style). Telemetry/display only — no auth or request-handling changes; no network CIMD fetches on the hot path. Known-host map is best-effort (repo has no observed production client_ids); fail-safe by construction — wrong/missing entries surface raw, never corrupt. Single edit point: knownClientDisplayNamesByHost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * @W-22030892: bump version to 2.25.3 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * @W-22030892: cover embedded-OAuth client ids + bound telemetry values Self-review round fixes: - embedded-OAuth sessions (tableauAuthInfo normalized to X-Tableau-Auth) carry the client id on extra.authInfo.clientId — telemetry now derives Bearer clientId ?? authInfo.clientId (RED-verified: the path emitted blanks before). - client_id is an attacker-influenceable token claim: new sanitizeClientIdForTelemetry bounds it (URL -> origin+pathname, drops search/hash/userinfo; 200-char cap) on both emitted fields, following the accessTokenValidator claim-truncation precedent (divergence from its 256+marker documented in the module header). 150 files / 2,246 tests green, tsc + eslint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * @W-23149256 prerelease-aware npm + Docker publish (hotfix without moving latest) + release runbook (#416) * @W-23149256 prerelease-aware npm + Docker publish (hotfix without moving latest) + release runbook Formalizes the hotfix release path so a livesite fix can ship off an already-deployed version without dragging main drift or moving the latest dist-tag. Andy's spec; continuity-critical (release-process holder departs 07-02). - publish.yml: prerelease-aware npm publish. A version with a SemVer prerelease identifier (a '-', e.g. 2.18.0-hotfix.1) publishes under a 'hotfix-v<version>' dist-tag, never latest; stable path unchanged (bare npm publish). Fails CLOSED if the version can't be read (refuses to risk moving latest). dist-tag is NOT the bare version — npm rejects a dist-tag that parses as valid SemVer. - docker-publish.yml: Docker 'latest' skips prerelease tags (mirror guard), so a hotfix gets its exact-version Docker tag but doesn't steal latest. - docs/RELEASE.md (new): normal + hotfix runbook (branch off tag, fix-on-main- then-cherry-pick, bump to -hotfix.N, release from hotfix branch, pin exact version in HF) + rationale; linked from CONTRIBUTING. Pre-push dual self-review (GPT-5.5 + Opus) caught 3 blockers on local code before this commit: npm rejecting a valid-SemVer dist-tag (fatal), Docker latest moving on hotfixes, and fail-open on empty version. All fixed here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * @W-23149256 preflight: release tag must equal v<package.json version> + runbook clarifications Review follow-up (self-review + Jaehun's walkthrough ask): npm decides prerelease-vs-stable from package.json while Docker decides from the release tag name — a tag/version mismatch could move Docker 'latest' while npm holds (or vice versa). Both workflows now fail closed on mismatch before publishing; the tag name reaches the scripts via env, not inline interpolation. Runbook: Release-publish (not tag push) is the trigger, mark hotfix releases pre-release, and rerun-after-partial-failure semantics (npm immutability). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * @W-23375797: consolidate admin-gated tools into polymorphic dispatchers (#475) * @W-23375797: consolidate admin-gated tools into polymorphic dispatchers Two new polymorphic tools reduce the model-visible admin tool surface: - `query-admin-insights` (kind discriminator) merges 4 insight readers: get-stale-workbooks, get-stale-datasources, get-content-usage-metrics, get-user-activity. - `delete-content` (resourceType discriminator) merges 3 preview→confirm destructive tools: delete-workbook, delete-datasource, delete-extract-refresh-task. Behavior preserved end-to-end. `delete-content` records AppApprovalEvidence under legacy namespaces so the mcp-apps HITL panel flow (flag-ON) continues to dispatch confirm-delete-workbook / confirm-delete-datasource / confirm-delete-extract-refresh-task via the existing iframe machinery. The flag-OFF preview→confirm tag/nonce evidence gates work unchanged. Auth: scope-based auth in scopes.ts adds delete-content and query-admin-insights, each carrying the union of its component tools' MCP + REST scopes. Both are dropped from `getEnabledToolNames()` when `ADMIN_TOOLS_ENABLED=false`. Legacy tools remain wired during the shim window. Bumps package version 2.25.0 → 2.26.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: fix self-review findings on consolidated tools - delete-content: replace three per-resource MCP scopes with the umbrella tableau:mcp:content:delete so authMiddleware's AND-enforcement doesn't 403 callers who only granted one legacy scope. Callers who need per-resource granularity keep using the legacy delete-{workbook, datasource,extract-refresh-task} tools during the shim window. - query-admin-insights: move the missing-query check inside logAndExecute so failed invocations are logged and product-telemetry emitted instead of being silently dropped by the pre-callback return. - query-admin-insights: honor per-kind MAX_RESULT_LIMITS entries for the four legacy admin-insights tool names by taking the tightest of consolidated cap, legacy cap, and caller-provided limit — prevents operators' existing cap config from silently disappearing on migration. * @W-23375797: adapt consolidated stale-content to new resolver shapes PR #473 changed getStaleContentReport's helpers to return richer objects: - _resolveProjectScopeIds now returns {scopeIds, boundedOutOfScopeIds} - _resolveProjectIdsToNames now returns {names, unknownIds} - new _buildProjectIdWarnings helper + _StaleReportWarning type Mirror the legacy get-stale-content-report tool in queryAdminInsights so projectId warnings and the all-invalid widening guard behave the same via kind=stale-content. Re-export the two helpers + type from getStaleContentReport so both tools share one implementation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: fix e2e tool-list test for gated consolidated tools The consolidated query-admin-insights and delete-content tools are disabled unless ADMIN_TOOLS_ENABLED=true, so they don't appear in the server's tool list in the default e2e run. Add them to the adminOnlyTools filter in the default and combined variant "should list tools" tests, matching how the legacy per-kind admin tools are already filtered. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: lock in tightest-cap via legacy MAX_RESULT_LIMITS Add a queryAdminInsights.test.ts case that sets MAX_RESULT_LIMITS=query-admin-insights-ts-events:5, invokes the consolidated tool with kind=ts-events and limit=100, and asserts the underlying VDS query is issued with rowLimit=5 — so the legacy per-kind cap still wins after callers migrate to query-admin-insights. Guards against the silent-migration hazard flagged in the self-review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: fix oauth-embedded scopes_supported count for umbrella scope The new tableau:mcp:content:delete umbrella scope (added by the self-review fix on delete-content) bumps scopes_supported from 14 to 15. Update the three well-known-endpoint tests to expect the new scope by name and the new length. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: address review — trim descriptions, type logAndExecute, document flag-ON path - Trim query-admin-insights description from ~546 to ~120 tokens; remove inline JSON schema + per-kind param tables (Major, Alon). - Trim delete-content description from ~437 to ~100 tokens; remove per-resourceType parameter reference (Major, Alon). - Document that flag-ON preview path writes both TagEvidence AND AppApprovalEvidence in all three resource branches (Minor, Alon). - Add explicit <QueryOutput> type param to raw-VDS logAndExecute for symmetry with <StaleContentResult> branch (Minor, Alon). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23375797: export QueryOutput type from adminInsightsToolBase The previous commit imported QueryOutput in queryAdminInsights.ts from adminInsightsToolBase.js, but that module only used it locally without re-exporting. Add `export type { QueryOutput }` so the consolidated tool's explicit type param compiles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add documentation pages for query-admin-insights and delete-content - docs/docs/tools/admin-insights/query-admin-insights.md — consolidated tool - docs/docs/tools/content/delete-content.md — consolidated tool - docs/docs/tools/content/_category_.json — sidebar category - docs/docs/intro.md — 2 new table rows Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * @W-23415681: make FeatureGateProvider.isFeatureEnabled async (#497) * @W-23415681: make FeatureGateProvider.isFeatureEnabled async * @W-23415681: await async isFeatureEnabled in get-embed-token authz test * @W-23415681: prune redundant feature-gate init tests * @W-23375797: remove deprecated admin tool shims (Phase 2) (#498) * @W-23149256: fix publish.yml startup failure from empty ${{ }} in run: block (#506) The prerelease-aware comment block added in #416 included an empty ${{ }} expression inside a run: | script. GitHub Actions evaluates ${{ }} in run: text before the shell sees it, so the shell-comment # does not protect it; the empty expression is invalid and the workflow fails to load (startup failure, no publish job ever runs). Reword the comment to drop the token. actionlint clean; publish logic unchanged. * @W-12345678 Docs update 7-14 (#509) * 7/14 docs * version bump * @W-22350774: Add Tableau Prep flows tools (list-flows, get-flow) (#344) @W-22350774 Add Tableau Prep flows tools (list-flows, get-flow) Add two read-only MCP tools that wrap the public Tableau REST Flows API: - list-flows: lists Prep flows on a site with field:operator:value filtering (createdAt, name, ownerName, projectId, projectName, updatedAt) and sorting. Every successful response includes an always-present mcp.resultInfo { returnedCount, truncated, truncationReason?, totalAvailable? } so a caller can distinguish a complete list from one cut short by the caller's limit or the admin MAX_RESULT_LIMIT cap. totalAvailable is surfaced only when no server-side bounded context (PROJECT_IDS/TAGS) is configured, since that count is taken before the tool's allow-list filtering. - get-flow: retrieves a single flow with optional connections and run history. Sidecar OAuth scopes are requested only when the corresponding sidecar is included, and access is gated by the bounded-context resource checker (isFlowAllowed) so PROJECT_IDS/TAGS restrictions apply. Run history is bounded by flowRunLimit and emits a FLOW_RUNS_TRUNCATED warning when more runs exist. Supporting changes: - Gate all flow tools behind FLOW_TOOLS_ENABLED, disabled by default; set it to "true" to register list-flows and get-flow (mirrors ADMIN_TOOLS_ENABLED). Individual tools can still be excluded via EXCLUDE_TOOLS. - Add the flow REST API definitions, Zod types, and methods, plus the dedicated tableau:flows:read, tableau:flow_connections:read, and tableau:flow_runs:read scopes. - Harden parseAndValidateFilterString bracket / in: list handling (repo-wide fix). - Make project.description optional (Tableau returns it on embedded <project> elements). - Tools, types, unit tests, a description-quality eval test, and docs for both tools. * @W-23375797: fix unawaited async feature gate in confirmDeleteContent (#513) * fix: await async isFeatureEnabled in confirmDeleteContent disabled field PR #497 made isFeatureEnabled async but this call site was not updated, causing `!Promise` to always evaluate to `false` — the tool was never disabled when mcp-apps was OFF. Wrap in Provider(async () => ...) to match confirmUpdateCloudExtractRefreshTask. Update test mocks from mockReturnValue to mockResolvedValue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version to 3.1.1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): remove flow:read from OAuth scope assertions Flow tools are gated behind FLOW_TOOLS_ENABLED (off by default), so tableau:mcp:flow:read is never advertised in scopes_supported. The assertions were incorrect as shipped by the flows PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23388458 - Add insight-cards MCP tool (deterministic insights from datasource context) (#502) * @W-23388458: Add generate-chiron-insight-cards tool + datasource LUID resolver Add a deterministic Chiron insight-cards path to tableau-mcp: - resolve-datasource-luid: exact case-sensitive contentUrl -> LUID resolution (name is not unique; REST contentUrl filter is case-insensitive). - generate-chiron-insight-cards: resolves a datasource, reads metadata to pick measures/time field/dimensions, runs period-over-period detail bundles via a shared runChironBundle helper, and shapes governed InsightCard JSON (headline, delta, direction, PoPC comparison, series, breakdown). - Register the tool + scopes; expose contentUrl on datasource types. Co-authored-by: Cursor <cursoragent@cursor.com> * @W-23388458: Expand Chiron insight cards to full detail payload + drill-down Extend generate-chiron-insight-cards from a headline card into the full governed insight-detail payload and support recursive drill-down: - InsightCard now carries context, contributors (with % of lift), pronounced (most/least), unusual (anomaly), followUps, actions, and breakdownDimension; extractors read these from the Pulse detail bundle. - Add drill params: breakdownDimension (scopes the breakdown to one dimension) and filters (member scoping). Response returns the full dimension list so drilled cards still propose fresh follow-ups, while only the active dimension scopes allowed_dimensions. - Member-filter contract confirmed live against Pulse: OPERATOR_EQUAL with categorical_values [{ string_value }] only, in metric_specification (adding '='/bool_value/null_value or using basic_specification 400s). - Unit tests cover the detail payload, drill params, and the filter shape. Co-authored-by: Cursor <cursoragent@cursor.com> * @W-23388458: Rename tool to generate-insight-cards; drop Chiron/Pulse from public name + description; fix tsc types; bump version * @W-23388458: Address PR review - bounded-context guards, surface bundle failures, validate drill args, fix nondeterministic/aliased fields * @W-23388458: De-Chiron internal names -> insight* (folder/files/functions/types); no behavior change * @W-23388458: Address review - indistinguishable not-found/not-allowed + gate before identity lookup; opt-in (disabled-by-default) registration; validate measures/timeField; deterministic series sort * @W-23388458: Gate insight tools behind INSIGHTS_TOOLS_ENABLED (off by default) + deterministic field selection --------- Co-authored-by: Cursor <cursoragent@cursor.com> * @W-23377934: fix extract-refresh-task delete preview existence check + confirm double-audit (#529) * @W-23377934: fix extract-refresh-task delete preview existence check + confirm double-audit Two defects in the consolidated delete-content flow: 1. Preview minted a confirmationToken for a non-existent extract-refresh-task. The task branch's resolveTarget was a pure echo with no REST read, so a well-formed but non-existent taskId passed the UUID check and got a live nonce. Unlike the workbook/datasource branches (which 404 early via getWorkbook/queryDatasource), the task branch never read the target and there is no single-get task endpoint. Fix: list-and-find existence check before minting the nonce (preview + confirm), returning a 404 not-found. Requires tableau:tasks:read, added to the delete-content scope map. 2. Confirm phase double-logged audit. guardMutation emitted an unconditional 'allowed' record plus the terminal completed/failed via recordOutcome, so every confirmed mutation logged twice (all resourceTypes / all preview-> confirm tools). Fix: suppress 'allowed' on the confirm phase so a confirm emits exactly one terminal record; preview still emits its single 'allowed'. Audit lifecycle doc comments updated to the one-record contract. Tests: 6 new pinning tests (nonexistent/empty-list task -> 404 + no token + no delete; confirm -> exactly one completed/failed; preview -> single allowed). Docs: delete-content.md documents the existence check and corrects the REST-API reference (List, not the non-existent Get, Extract Refresh Tasks). Version: 3.1.2 -> 3.1.3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23377934: fix e2e list-tools to filter INSIGHTS_TOOLS_ENABLED-gated tools The e2e "should list tools" assertions (default + combined variants) built expectedToolNames from webToolNames but never filtered the insights tools (generate-insight-cards, resolve-datasource-luid) added in #502, which are gated behind INSIGHTS_TOOLS_ENABLED (off by default). The built server omits them, so the toHaveLength check failed (received 20/26 vs expected 22/28). This surfaced on this PR because it runs from the main repo (not a fork), so the fork-guarded E2E CI step actually executes. Added the insights filter to both variants, mirroring the existing FLOW_TOOLS_ENABLED gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23125362: complete mutation-tool safety layer — audit target enrichment + centralized HITL text + durability contract (#533) Scope-completes the common mutation-tool safety layer (guard + evidence + audit already merged). Finishes the in-scope remainder; auth-session tools (revoke/reset), a durable external sink, and e2e tiers are forked to a follow-up story. - AC-3 (Gap B): enrich extract-refresh-task audit target with name/project/ owner, derived from the underlying datasource (else workbook). New shared helper resolveExtractRefreshTaskTarget.ts; best-effort, degrades to id-only on Server sites or 403/404 and never blocks/fails the mutation. Applied to all four task paths (delete, confirm-delete, update, confirm-update). The delete branch reuses its existence-check task list (no extra round-trip). - AC-4 (Gap D): centralize tool-side HITL text into new hitlText.ts (NEXT-STEP preview, confirm-closed, PreviewNotRun recovery). Byte-for-byte behavior-preserving; rethreaded delete-content, update-cloud, and the guard's PreviewNotRun branch. Kept in tools/_lib to avoid a tools→prompts cross-layer import. - AC-3 (Gap C, doc): document the durability contract — audit records emit as structured JSON on the dedicated `audit` logger (bypasses LOG_LEVEL); durable retention is the deployment's responsibility (ship the stream to an immutable store). Added to auditRecord.ts and delete-content.md. - AC-6 (Gap E, unit): per-tool assertions that enriched target fields appear in audit records on allow and deny; forged-confirm reject and preview→confirm happy paths. Version 3.1.3 → 3.1.4. Full suite green (150 files / 2306 tests); tsc, lint, build clean. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23448424: add update-user tool — two-phase HITL site role update (#516) * feat: add update-user tool — two-phase HITL site role update - New `update-user` MCP tool with preview/confirm flow using RegistryEvidence - OAuth scope: `tableau:mcp:users:write` → `tableau:users:update` + `tableau:users:read` - Admin-gated, UUID-validated userId, enum-constrained siteRole - Primary use case: downgrade inactive users to Unlicensed for license reclamation @W-23448424 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: relax update-user response schema to accept partial user The Tableau PUT /users/:userId endpoint only echoes back changed fields (e.g. siteRole, name, email) without `id`. Using `userSchema.partial()` prevents Zodios response validation failure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version to 3.2.0 for update-user feature Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review findings — idempotentHint, test coverage, scope phrasing - Set idempotentHint: false (nonce is single-use; repeated calls reject) - Add preview safety test: mockUpdateUser.not.toHaveBeenCalled() - Add partial-response fallback test (API omits siteRole) - Add missing siteRole/email fallback text test - Add RegistryEvidence binding-isolation test (role-swap protection) - Add e2e behavior test (preview, schema validation, bogus token rejection) - Fix scope phrasing: list both tableau:users:update and :read - Add comment explaining SupportUser/ServerAdministrator omission Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: gate update-user e2e tests behind UPDATE_USER_E2E_ID Unlike the sibling tool, update-user's resolveTarget calls queryUserOnSite which 404s for non-existent user IDs. Gate preview and bogus-token tests behind the env var so they skip gracefully when no real user LUID is configured. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: split mutating e2e test behind UPDATE_USER_E2E_DESTRUCTIVE_ID Mirror sibling pattern (updateCloudExtractRefreshTask) — the preview and bogus-token tests use UPDATE_USER_E2E_ID, while the actual mutation uses a separate UPDATE_USER_E2E_DESTRUCTIVE_ID to prevent accidentally downgrading a real user. All guidance consistently says "disposable user". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23336786 ci: bump GitHub Actions to latest major versions (#507) ci: bump GitHub Actions to latest major versions Updates actions/checkout (v3/v4→v7), actions/setup-node (v4→v7), actions/upload-artifact (v4→v7), actions/upload-pages-artifact (v3→v5), actions/deploy-pages (v4→v5), actions/github-script (v8→v9), docker/setup-buildx-action (v3→v4), docker/login-action (v3→v4), docker/metadata-action (v5→v6), and docker/build-push-action (v5→v7) across all workflows. * @W-23336786 Add action to automatically label "feature" PRs (#451) * Ignore .DS_Store * Add .github/workflows/pr-feature-label.yml, which fires when a PR is opened against a base branch starting with "feature/" and labels it with the branch remainder prefixed "feature:" (e.g. base feature/rounded-corners -> label feature:rounded-corners). Uses actions/github-script to get-or-create the label (auto-created gray if missing) then apply it. The on.pull_request.branches: ['feature/**'] filter scopes the trigger to feature base branches; the job carries issues:write + pull-requests:write and the standard fork/dependabot guard. * Adjust based on source or destination branchs as "feature" * Operate consistently on PR actions Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: pin system time in integration tests to eliminate date-dependent flakiness * Revert "fix: pin system time in integration tests to eliminate date-dependent flakiness" This reverts commit 4cb7d5f. Still want to fix this, but in a separate PR. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * @W-23448422: add user-license-reclamation-inform prompt (#517) * @W-23448422: add user-license-reclamation-inform prompt - New MCP prompt that identifies inactive licensed users as reclamation candidates by paginating list-users (role + lastLogin filters) and cross-referencing activity via query-admin-insights (ts-events) - Config knobs: LICENSE_RECLAIM_INACTIVE_DAYS, LICENSE_RECLAIM_ROLES - 13 unit tests covering defaults, arg overrides, env var wiring - Docs page + intro.md entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version to 3.3.0 New prompt feature warrants a minor version bump. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove broken markdown link to unreleased apply prompt doc The Docusaurus docs build fails when a markdown link targets a file that doesn't exist yet. Replace with inline code reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review — input validation, lookback cap, docs - Add regex validation on `roles` arg to prevent prompt injection - Cap TS Events `rangeN` at 90-day lookback max (avoids false positives when inactiveDays > 90) - Add `limit: 10000` to ts-events query to bound result set - Align inactiveDays regex to 1–3650 (matches env var clamp) - Document MAX_RESULT_LIMIT caveat in docs workflow description - Add LICENSE_RECLAIM_INACTIVE_DAYS and LICENSE_RECLAIM_ROLES to env-vars.md configuration docs - Add source reference comment for TS Events field captions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: correct TS Events field captions and address live-verified findings BLOCKER fixes (live-verified against prod TS Events datasource): - `Created At` → `Event Date` (correct DATETIME field on TS Events) - `Actor User Id` → `Actor User Name` (STRING matching username/email, joinable with list-users; Actor User Id is an internal integer) MEDIUM fixes: - Add second-pass instruction for null-lastLogin users (never signed in — strongest reclamation candidates, previously dropped silently) - Add ETL lag warning (TS Events lags 24–48h behind live state, making the safety net leaky — candidates must be human-verified) - Strengthen "INFORM-only" framing in recommendation note Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23093467 @W-22551424: enforce stale-content row cap server-side + harden Admin Insights LUID cache (#553) Part A (W-23093467): move the stale-content-cleanup safety cap from prompt-text only to server-side enforcement. Add configurable STALE_CONTENT_MAX_ROWS (default 100, range 1-10000) and enforce it in the stale-content branch of query-admin-insights: above the cap the tool returns a structured success with rows:[], the TRUE totalStaleItems/totalStaleSizeBytes, and a ROW_CAP_EXCEEDED warning guiding the caller to narrow scope. Read-only inform prompt keeps working (no throw); the destructive apply flow can no longer receive a huge actionable set even if the model ignores the prompt guard (which remains as defense-in-depth). Part B (W-22551424): light hardening of the existing Admin Insights LUID resolver cache. Add an optional maxSize to ExpiringMap (default unbounded; other callers untouched) with oldest-inserted eviction that clears the pending timeout, wire a 256-entry bound into the resolver cache, and add hit/miss/resolve debug telemetry (no siteId/PII). Cache invalidation remains TTL + siteId-keying; an explicit refresh hook is deferred. Tests: below/at/above-cap + combined-warning cases, config precedence/validation, ExpiringMap eviction, resolver eviction. Docs: env-vars, request-overrides, site-settings, query-admin-insights tool page. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * @W-23336786 Add flexibility to PR title check (docs: prefix and skip-title-check label) (#455) * @W-23336786 Add flexibility to PR title check (docs: prefix and skip-title-check label) * Improve case insensitive title matching Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Avoid race condition around label creation Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Improve least-privilege for write permissions Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Exclude dependabot PRs Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * @W-23448423: add user-license-reclamation-apply prompt (HITL-guarded) (#518) * @W-23448423: add user-license-reclamation-apply prompt HITL-guarded apply prompt that chains 3A inform output into update-user downgrades to Unlicensed. Follows the same two-phase preview→confirm pattern as stale-content and extract-optimization apply prompts. Defaults to dry run; no mutation without explicit human approval at the Step 4 gate. - src/prompts/userLicenseReclamation/apply.ts (prompt impl) - src/prompts/userLicenseReclamation/apply.test.ts (23 tests incl. HITL-refusal adversarial cases) - src/prompts/index.ts (registration) - docs/docs/prompts/user-license-reclamation-apply.md - docs/docs/intro.md (table row) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23448423: add deterministic VDS query templates for Steps 2-3 Self-review finding: Steps 2 (ts-events) and 3 (site-content) told the model to call query-admin-insights but didn't provide VDS query objects with exact field captions. The model would have to guess Tableau-specific field names, causing runtime failure. Now provides deterministic JSON query blocks (same pattern as job-optimization-inform) with exact field captions and filters: - Step 2: ts-events with Login/Login (Embedded) filter, LASTN date - Step 3: site-content with Workbook/Datasource item-type filter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23448423: align apply prompt with validated inform findings - Fix TS Events field names (Actor User Name, Event Date — not Actor User ID / Event Created At) - Use Access events instead of Login (catches embedded/API usage) - Add all compound roles (SiteAdministratorCreator, SiteAdministratorExplorer) to defaults - Cap rangeN at 90 days (TC standard lookback limit) - Add limit: 10000 to prevent unbounded result sets - Add LICENSE_RECLAIM_INACTIVE_DAYS / LICENSE_RECLAIM_ROLES env var support - Handle null-lastLogin users (provisioned but never signed in) - Add ETL lag and lookback cap caveats to Fixed notes - Bump version to 3.5.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23448423: address review findings — Owner LUID, inactivity rule, docs MAJOR fixes: - Replace invalid `Owner LUID` caption with `Owner Email` (join by email) - Fix Step 2 inactivity rule: "whose lastLogin from Step 1 is null" not "absent from Step 1" - Docs: list all 6 license-consuming roles including compound variants MINOR fixes: - Clamp arg-path inactiveDays to max 3650 (matches env-path behavior) - Docs: reword Step 2 from "last login date" to "activity signals" - Docs: add LICENSE_RECLAIM_INACTIVE_DAYS / LICENSE_RECLAIM_ROLES config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prettier formatting for SITE_CONTENT_FIELDS array Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add lastLogin pre-filter, truncation warnings, and tighten regex - Require lastLogin to be null or older than inactiveDays threshold before flagging a user as inactive (prevents false positives from ETL lag on recently-active users) - Warn admin when TS Events or Site Content queries hit the 10k row limit (silent truncation could cause false-positive downgrades) - Tighten inactiveDays regex to reject 3651-9999 at schema level (previously only clamped at runtime via Math.min) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prettier formatting for regex line break Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * @W-23518533: Group embed-viz and HITL MCP-App code into feature folders (#556) * refactor: group embed-viz and HITL MCP-App code into feature folders Split the flat src/web/apps/src/lib/ directory into feature-scoped folders so each browser-side MCP-App bundle is self-contained: - embed/ — Tableau viz embedding for get-view/get-workbook (mcp-app entry, handleToolResult, embedTableauViz, loadTableauEmbeddingApi, getEmbedTokenToolClient, openInTableauLink) + mcp-app.html - hitl/ — in-iframe confirm panel for delete/update preview tools (hitl-confirm entry, handleConfirmResult, *ConfirmClient renderers) + hitl-confirm.html - shared/ — mcp-app.css, showError, vite-env.d.ts, assets/, and a new vizContainer.ts holding TABLEAU_VIZ_CONTAINER_ID (extracted to keep shared/ free of a shared->embed dependency) HTML templates move next to their entry .ts with relative <script src>. Each Vite build's root is set to its feature folder in build.ts so single-file output still lands flat as dist/mcp-app.html and dist/hitl-confirm.html; appConfig.ts and server.web.ts are unchanged. All moves use git mv to preserve history. 2365 unit tests pass, lint clean, build produces flat dist. * chore: bump version to 3.4.1 * bump * remove unused export --------- Co-authored-by: Alon Siman Tov <asimantov@salesforce.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Akash Rastogi <akashrastogi12@gmail.com> Co-authored-by: jarhun88 <34752438+jarhun88@users.noreply.github.com> Co-authored-by: Joey Constantino <15789096+joeconstantino@users.noreply.github.com> Co-authored-by: Jialin Mao <30403691+showmethecold@users.noreply.github.com> Co-authored-by: Tony <3278731+tonythebest@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Brian Cantoni <bcantoni@salesforce.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Akash Rastogi <akash.rastogi@salesforce.com>
…571) * feat(web): slim param on generate-pulse-metric-value-insight-bundle Add an optional slim boolean to generate-pulse-metric-value-insight-bundle. When true, strips the large viz (Vega chart-spec) blobs from every insight and summary result and attaches a metric_context sibling built from the request, so a text/card UI (e.g. the Tableau Agent viz-canvas) gets a compact payload it can render from facts/markup alone. Defaults to false -> response is returned verbatim, including viz, so existing callers are unaffected. - slimBundle(): non-mutating viz-strip; single seam for future UI-unused fields - buildMetricContext(): curated flat fields + verbatim input escape hatch - pulse.ts: extract pulseBundleInputSchema; add metricContextSchema, MetricContext, PulseBundleResponseSlim - 19/19 tool tests green Dependency for tab-agent-south-ui viz-canvas insight bundles (W-23447550). * viz-canvas slim: drop the metric_context input echo Remove the verbatim request-input echo from metric_context (slim response). Per the tool-use contract, the caller already holds the request it sent -- it is carried on the tool_use block and correlated to this result by id -- so echoing input back into the response is redundant payload in a param whose whole purpose is to stay slim. The UI reads the request off the wire (tool_use), not from a response field. metric_context keeps its curated flat fields (name/measure/time_dimension/ breakdown_dimensions). Also revert the pulseBundleInputSchema extraction (it existed only to type the echo) so the request schema is unchanged. - metricContextSchema: drop input - buildMetricContext: drop input - test: assert metric_context has NO input; 19/19 green * viz-canvas slim: drop metric_context entirely — slim is viz-strip only Every field metric_context carried was a pure projection of the request input (name/measure/time_dimension/breakdown_dimensions), and the caller already holds that request — it rides on the tool_use block, correlated to the result by id. So metric_context was a second, redundant path to data the UI already reads off the wire; it added zero information. Drop it and let slim mean exactly one thing: strip the viz blobs. - slimBundle.ts: remove buildMetricContext (now a single viz-strip export) - pulse.ts: remove metricContextSchema/MetricContext/PulseBundleResponseSlim (reverts to base — no longer in the diff) - tool: constrainSuccessResult back to `slim ? slimBundle(x) : x`; logAndExecute typed PulseBundleResponse - test: drop the two metric_context tests; assert slim never adds the sibling; 17/17 green
* feat(fields): add verbosity=slim to list-available-fields
list-available-fields returns the field catalog twice (a human-readable ASCII
table plus a full per-field metadata array incl. column_ref) — ~68KB for a wide
datasource (155 fields), which overflows the host's inline-output cap and gets
truncated to a preview the caller can't fully re-read.
Add an optional verbosity param:
- 'full' (default): unchanged — table + full metadata + column_ref. Backward
compatible for authoring callers that place fields on shelves.
- 'slim': a compact JSON array of { caption, role, datatype } with datasource
hoisted to the top level, no ASCII table, no authoring metadata. Small enough
to land inline, for reasoning over fields to pick measures/dimensions (e.g.
generate-insight-cards). Resolve the exact column_ref via resolve-field.
Adds slim-mode tests.
* docs(fields): tighten verbosity param description
* docs(fields): trim slim-branch comment
* fix(fields): group slim list-available-fields by datasource + add contentUrl
Slim previously hoisted fields[0].datasource and dropped per-field datasource,
misattributing every field past the first in multi-datasource workbooks and
erasing the disambiguator for same-caption fields (review on #535).
Slim now always returns { count, datasources: [{ datasource, contentUrl?, fields }] } —
grouped by datasource so the name is carried once per group (small), and correct
for 2+ datasources. contentUrl (from repository-location) is surfaced per published
datasource as the input resolve-datasource-luid needs; omitted for embedded ones.
Single datasource is one group, so callers parse one consistent shape.
* fix(fields): trim slim verbosity description under the per-tool schema budget
The grouped-shape example inlined in the verbosity param description pushed
list-available-fields to 1229 bytes (over the 1200-byte per-tool cap) and the
total schema 18 bytes over 46000. Trim to a concise description; the grouped
shape is self-describing in the response.
* test(fields): cover slim over the live-session path (no workbookFile)
Addresses review: no existing slim test exercised the live-session path.
Adds a verbosity=slim test with a session and no workbookFile, asserting the
grouped slim shape and that the file cache is never read.
* fix(fields): trim verbosity description under the tool-schema budget
Addresses review: keep the desktop tools/list surface under the 46k
auto-deferral cliff (server.desktop.test.ts). Trims the verbosity param text
to the load-bearing facts rather than raising the cap.
* fix: slim mode keeps reference-building data; validate verbosity; pin default parity
Takeover fixes on the verbosity=slim feature: slim output now carries
localName, columnInstanceName, derivation, type, and typePivot — without
them a consumer of slim output cannot construct the compound column
reference the authoring tools require (the tool's whole purpose). Already-
aggregated calc fields covered in slim (usr: + isAggregated). verbosity is
zod-enum validated; absent verbosity is pinned byte-for-byte to full.
Descriptions tightened for the combined-lean budget.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Matt Filbert <mattcfilbert@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
…s, dialog refusals) (#572) * feat: env-gated External-API command-registry guard on the execute path (2.30.0) execute-tableau-command consults an OPTIONAL runtime registry (EXTERNAL_API_REGISTRY_DIR, provided by the host; never shipped in this repo) to kill two live failure classes on the generic invokeCommand route: wire-name rewrite (167/330 commands have params whose registered wire name differs from CamelToDashed(local) — unknown keys surface as bare 500s) and enum validation against serialized value lists (errors teach the legal values in-band). Registry-known commands with agent_can_invoke=false or opens_blocking_dialog=true are refused; required params are checked with context-filled (UPI_Workspace) awareness. The eight live-observed dialog-popper commands are refused UNCONDITIONALLY (registry or not) — they hang Desktop's UI thread headlessly and pass every static safety flag. Env unset or files unreadable → behavior identical to today (fail-open to the bundled guards). Tests use synthetic fixtures only. Co-Authored-By: Claude <noreply@anthropic.com> * style: lint fixes (prettier + drop unused import after unconditional blocklist hoist) Co-Authored-By: Claude <noreply@anthropic.com> * fix: land Lauren's #572 review — drop clipboard false positive, correct change-site guidance - tabui:copy-zone-to-desktop-clipboard removed from the blocklist (likely false positive per the API owner; removable pending re-sweep evidence) - tabui:workgroup-change-site refusal text corrected: changing sites is a fine user action — the refusal is only that the command opens a dialog headlessly; guidance now points the user at Desktop's own site switcher Co-Authored-By: Claude <noreply@anthropic.com> * refactor: one command-policy table — collapse five scattered special-command maps (#574) refactor: one command-policy table + gate — collapse five scattered special-command maps CRASH_PRONE_COMMANDS, LIVE_DIALOG_BLOCKLIST, LIVE_PARAM_OVERRIDES, KNOWN_LIVE_FAILURE_FIXES, and LIVE_EXTERNAL_API_DIALOG_BLOCKLIST lived in three files, discovered by separate live sweeps and never reconciled — tabdoc:sort-nested was simultaneously 'usable with these 9 params' and 'fails regardless, never retry'. One module (commandPolicy.ts) now holds every special-command disposition with refuse/hint/param-override semantics preserved and per-entry origin comments; all call sites read from it. sort-nested is one coherent record: param contract kept for validation messages, do-not-retry hint authoritative. Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
* feat: data-first tools — get-summary-data + list-site-datasources (2.31.0)
The agent's first play was a structural read (workbook XML); for data
questions and data-shaped authoring choices that is backwards. Two new
desktop tools on the External Client API typed reads:
- get-summary-data: the summary/logical table behind a worksheet
(GET /v0/workbook/worksheets/{id}/summaryData). Resolves worksheet
name→id via the worksheets list (honest ambiguity/not-found errors),
clamps maxRows (200 default / 1000 cap), optional column filter, and
reports shape ('N rows x M columns'). The description teaches the
marks-card mechanic: summary data carries only fields ON the view —
add others to Detail first, then read.
- list-site-datasources: datasources published to the connected site
(GET /v0/site/datasources) — the active-sheet → published-LUID bridge
the Pulse insight flow needs.
Both join the default dynamic-authoring profile. Pre-#60211 Desktop
builds 404 these routes — get-summary-data degrades honestly (named
'endpoint-not-in-this-build' error, no retry loop) with a regression
test on the route-miss detector.
To keep the full-surface tools/list under the 46k deferral cliff while
the (much smaller) dynamic profile keeps rich descriptions, verbose
full-only tool descriptions were tightened (propose-template,
list-templates, delete-worksheet, get-worksheet-xml) — same meaning,
fewer bytes.
Validated: tsc clean, eslint clean, full suite 4878 passed / 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: first-class wave 2 — inventory, workbook datasources, site workbooks, app info
Four typed reads on the External Client API: get-workbook-inventory
(/v0/workbook — one orienting call) + list-workbook-datasources
(/v0/workbook/datasources — the local half of the LUID bridge) join the
dynamic-authoring profile; list-site-workbooks (/v0/site/workbooks, LUIDs)
+ get-app-info (/v0/app, build/version) in the full set. Client methods,
zod pins, mock routes, honest route-missing handling, tests.
Full-only description prose was trimmed for the 46k cliff; the cliff test
retires in the next commit per owner decision, restoring free prose.
Validated: tsc + eslint clean, full suite 4910 passed / 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: first-class wave 3 — full schema pins, per-item documents, slice workaround retired
types.ts now pins every schema in the openapi (24). Client/executor/mock
grow dashboard+storyboard lists, per-item document routes, and
document:validate. On the External Client API transport, get-worksheet-xml
/ get-dashboard-xml fetch the TARGET's own document (list→id→
/{id}/document) and list-worksheets / list-dashboards ride their typed
routes — no whole-document fetch, no client-side slicing; the old path
survives only as the fallback for transports without first-class routes.
Old builds that predate the routes get honest endpoint-not-in-this-build
errors.
Validated: tsc + eslint clean, full suite 4937 passed / 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: first-class wave 4 — server-side validate, prune loop retired (land Lauren's #573 review)
validate-workbook-xml now calls POST /v0/workbook/document:validate on the
External Client API transport and surfaces the server's ValidationResult
(authoritative: schema + semantics, not just well-formed); the local
well-formed check stays as fast pre-flight and as the fallback for other
transports, with the honest old-build error.
The replaceWorkbook + tabdoc:delete-sheet prune loop is DELETED (not kept
as fallback): whole-workbook POST replaces natively per the API owner, and
caller tracing showed the loop only ever ran under externalApiEnabled —
the non-External-API path uses loadUnderlyingMetadataByFilepath and never
touched it. Whole-workbook apply is now a single applyWorkbookText.
Transport-only (client methods, no tool, by design): /v0/site, /v0/health,
/v0/ (ApiRoot), per-item {id} info routes, storyboard documents (a
get-storyboard-xml tool waits for storyboards to become user-facing scope).
Validated: tsc + eslint clean, full suite 4939 passed / 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: first-class waves 5+6 — every functional route a tool; review fixes
Wave 5 (the API owner's final gap list): 8 tools — get-api-root,
get-health, get-site-info, get-worksheet-info, get-dashboard-info,
list-storyboards, get-storyboard-info, get-storyboard-xml — with the 4
missing client methods. ZERO functional routes without a tool; all 8
full-set-only with terse descriptions, dynamic profile unchanged.
Wave 6 (adjudicated dual-context review): optional session arg on every
first-class read tool (multi-Desktop pinning safety); route-miss
detection unified in one shared helper and applied at every call site
incl. prerequisite lists; contentUrl described honestly (spec does not
declare it — surfaced when the build provides it); name resolution
decodes XML entities on fallback. package-lock synced to 2.31.0.
Validated: tsc + eslint clean, full suite 4966 passed / 1 skipped.
NOTE: combined-lean byte margin is now 18 bytes — the 46k ceiling is
effectively exhausted; next surface growth forces the pending decision.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
…nt hardening) (#575) Resync main → feature/desktop (23 commits: id_type, MCP-Apps folders, stale-content hardening, prompts) Second same-day resync bringing main's 23 commits — notably #528's optional datasource id_type on the insight bundle request (Pulse-relevant) and #553's Admin Insights LUID-cache hardening. Conflict resolutions repeat the established pattern: version line stays feature/desktop's (2.31.0, lock regenerated), publish.yml keeps the desktop-variant build + asserts composed with main's guarded publish, server.web.ts absorbs main's changes into the lazy-registration structure. Validated: tsc clean, full suite 4988 passed / 1 skipped (post lock regen). Co-authored-by: Claude <noreply@anthropic.com>
#576) feat: surface invokeCommand output — consume the 0.1.1 result envelope (2.32.0) Monolith #60596 (apiVersion 0.1.1) serializes a SUCCEEDED command's output params into Operation.result. The client now carries it end to end: envelope pin (optional object, scoped to 0.1.1) → normalizeEnvelope → buildCommandStatus → execute-tableau-command's success payload { message, result, warnings } with honest 16KB truncation. FAILED envelopes surface the real error message plus the tableau-error-code extension instead of a generic failure; serialize-degradation warnings render as warning lines. apiVersion <=0.1.0 instances stay silent on missing result (feature-detected from discovery, never probed). The 'envelope never returns output' write-blind claims are corrected and scoped to <=0.1.0. Contract-harness drift detection preserved for the capture that first declares result. Non-goal (unchanged): the bare-500 unknown-param class — that remains the command-registry guard's territory. Validated: tsc + eslint clean, full suite 4994 passed / 1 skipped. Co-authored-by: Claude <noreply@anthropic.com>
…le, slim deprecated (#577) refactor: converge on one slim vocabulary — verbosity enum, slim boolean deprecated Two 'slim' dialects shipped the same week: list-available-fields' verbosity enum vs the Pulse bundle's boolean slim (different types, same word). The bundle tool adopts verbosity: 'full'|'slim' with identical semantics (slim strips the viz Vega blobs); the boolean stays as a deprecated alias so existing callers keep working — verbosity wins when both are supplied, parity-tested byte-identical. Co-authored-by: Claude <noreply@anthropic.com>
…ructional prose (#579) chore: scrub internal engineering references; retire NotionalSpec readback + prose Public-repo hygiene (API owner's #576 review + owner directive): all internal PR/repo references in comments and strings reworded to neutral public-safe phrasing (apiVersion-scoped, behavior-preserving); the commands-reference NotionalSpec entries state constraints only — the spec-authoring instruction is gone (retired path); the generate-viz-from-notional-spec readback special case and its helper are removed from execute-tableau-command (the containment guard stays). Co-authored-by: Claude <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.