Skip to content

Add Calendar / Calculation Group / Function templates + Phase M modernization (2.0.0)#60

Open
marcosqlbi wants to merge 70 commits into
mainfrom
add-calendar
Open

Add Calendar / Calculation Group / Function templates + Phase M modernization (2.0.0)#60
marcosqlbi wants to merge 70 commits into
mainfrom
add-calendar

Conversation

@marcosqlbi

Copy link
Copy Markdown
Collaborator

Summary

This branch delivers two coordinated workstreams on top of main (70 commits):

  1. Three new TOM template entities (the feature goal), each JSON-driven, idempotent, and covered by offline golden-file tests + an opt-in live-server test:

    • Phase 1 — Calendars (Tables/Calendars/CalendarTemplate): attaches a native TOM Calendar (+ CalendarColumnGroups) to an existing table via the public TimeUnitColumnAssociation / TimeRelatedColumnGroup API — no reflection, no TMSL. Requires compat ≥ 1701.
    • Phase 2 — Calculation groups (Tables/CalculationGroups/CalculationGroupTemplate): generic (non-time-intelligence) calc-group generator — builds its own table + CalculationItems, with a foreign-table ownership guard and ordinal-uniqueness enforcement. Requires compat ≥ 1470 / 1500 / 1605.
    • Phase 3 — User-defined functions (Functions/FunctionLibraryTemplate): model-level DAX UDFs attached to Model.Functions from a hybrid structured/RawExpression schema. Requires compat ≥ 1702.
  2. Phase M — modernization & refactor (ships as 2.0.0): C# 14 / .NET 10 migration, analyzer + dotnet format CI gates, CI-only warnings-as-errors (allowlist now empty), file-scoped namespaces, required members, public-API change-detector baseline, coverage floor (80%) + Stryker, plus a batch of behavior-preserving refactors and defect fixes surfaced by characterization testing.

Conventions & guarantees

  • Additive JSON: all existing template configs keep working unchanged (no TemplateEntry schema change).
  • Idempotency: re-applying a template replaces its own prior output and cleans orphans (via SQLBI_Template annotation, or Calendar.Name for calendars).
  • Golden-file gate: existing BIM snapshots stay byte-identical; new entities add their own goldens.

Testing

  • Offline suite: 161 passed + 4 skipped (the 4 skips are the opt-in live-server facts), green under -p:TreatWarningsAsErrors=true; dotnet format --verify-no-changes clean.
  • Live-server validation (2026-07-12): 4 passed / 0 failed / 0 skipped against the published EmptyTestTemplates.pbix (compat ≥ 1702), non-destructive (ApplyTemplates + GetModelChanges, never SaveChanges). The Phase-3 UDF pass confirms the generated ( params ) => body signatures compile server-side — the only DAX check TOM performs, since Function.Expression is opaque offline.

Breaking changes (2.0.0)

  • Target framework narrowed to net10.0 only (was net6.0;net8.0).
  • required members + public-identifier renames (see CHANGELOG.md [Unreleased]). Public-API baseline regenerated accordingly.
  • Reflection helpers / some fields made internal.

Review notes

Every code change on this branch already passed the dotnet-claude-kit:code-reviewer gate per phase (see .claude/SESSION_HANDOFF.md). Docs (CHANGELOG, AGENTS.md, docs/design/*) were synced alongside each change.

🤖 Generated with Claude Code

marcosqlbi and others added 30 commits June 28, 2026 16:10
Foundations for adding new DAX template entities (calendars, calc groups, UDFs)
with regression safety.

- Bump Microsoft.AnalysisServices + AdomdClient 19.86.6 -> 19.114.0.
- Guard the 3 Table.RequestRefresh calls in Engine via RequestTableRefresh: a
  disconnected (in-memory) model has no Server and cannot be refreshed (TOM throws),
  so skip the request when not connected. Server deployments always run connected,
  so behavior is unchanged; this enables offline metadata generation and tests.
- Offline golden-file harness (Dax.Template.Tests):
  - OfflineModelFixture builds a synthetic disconnected model (Sales fact + target
    measures, Orders table) to drive the real Engine.ApplyTemplates dispatch.
  - GoldenFile serializes to BIM, normalizes volatile lineageTag GUIDs + line
    endings, and snapshot-compares against _data/Golden (UPDATE_GOLDEN=1 regenerates).
  - ApplyTemplatesGoldenTests snapshots the Standard config (holidays, custom date
    table + reference, and time-intelligence measures).
  - LiveServerFactAttribute: opt-in, skippable live-server test category (env-gated),
    never gating CI.
  - Copy HolidaysDefinition.json into test _data/Templates (Standard config needs it).

Suite: 7 passed + 1 skipped on net6.0 and net8.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AddHierarchies populated Level.TabularLevel with an orphaned object and
never assigned Hierarchy.TabularHierarchy at all: a first loop created a
TabularLevel, stored it on level.TabularLevel, then discarded it, while a
second loop created and added a different instance to the model without
storing it back. The emitted BIM was correct, so the golden-file test could
not catch the defect (it lived purely in the internal back-references).

Collapse to a single loop mirroring the correct AddColumns pattern: create
each TabularHierarchy/TabularLevel once, assign it to the model object's
back-reference, and add that same instance to the model. Preserve ordinal,
names, column binding, IsHidden/DisplayFolder and the compat-level LineageTag
guard, and restore a per-level cancellation check for parity with AddColumns.

Also remove a redundant Description re-assignment in CustomTableTemplate
(value already set in the object initializer; no-op).

Add HierarchyTabularReferenceTests covering the back-reference identity
contract plus ordinal/column-binding and Reset() behavior. Output unchanged
(golden BIM byte-identical); offline suite 13/13 on net6.0 and net8.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce a Keep a Changelog-style CHANGELOG.md. Document the AddHierarchies
internal back-reference fix and the new HierarchyTabularReferenceTests under
[Unreleased] (version is build-managed, so no concrete version assigned yet).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate the whole solution off multi-targeting net6.0;net8.0 to a single
net10.0 target (net10.0-windows for the TestUI), raise LangVersion to 14.0,
and pin the build to .NET SDK 10 (global.json 8.0.400 -> 10.0.301).

- global.json: SDK 8.0.400 -> 10.0.301 (rollForward latestFeature unchanged)
- Dax.Template / Dax.Template.Tests: net6.0;net8.0 -> net10.0, C# 12 -> 14
- Dax.Template.TestUI: net8.0-windows -> net10.0-windows, add LangVersion 14.0
- GitHub Actions ci.yml: install .NET 10.0.x SDK (was 6.0.x)
- Azure pipeline ci.yml: drop orphaned ".NET 6.0 runtime" install step
  (SDK comes from useGlobalJson; net6 is EOL)

Verified with SDK 10.0.301: restore/build/pack succeed, offline suite 13/13
on the single net10.0 target, golden BIM snapshot unchanged.

BREAKING: the published Dax.Template NuGet now targets net10.0 only and no
longer supports net6.0/net8.0 consumers. Documented in CHANGELOG [Unreleased].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Align lagging NuGet dependencies with the net10.0 / C# 14 / SDK 10 toolchain.

Test project (Dax.Template.Tests):
- Microsoft.NET.Test.Sdk   17.11.1 -> 18.7.0   (major)
- xunit                    2.9.2   -> 2.9.3
- xunit.runner.visualstudio 2.8.2  -> 3.1.5    (major; 3.x runs v1/v2/v3 assemblies,
                                                so it remains compatible with xunit 2.9.3)
- coverlet.collector       6.0.2   -> 10.0.1   (major)

TestUI (Dax.Template.TestUI):
- Microsoft.Extensions.Configuration.CommandLine 8.0.0 -> 10.0.9

Note: three coupled major test-tooling bumps land together here (Test.Sdk 18,
runner.visualstudio 3, coverlet 10) -- natural bisection point if a CI test-
runner regression ever surfaces. Verified with SDK 10.0.301: clean restore,
0 build errors, offline suite 13/13 on net10.0 (golden snapshot unchanged).
Shipped Dax.Template package unaffected (test/TestUI-only deps).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Onboard the repo with agent-facing documentation:

- AGENTS.md (root, tool-agnostic, <200 lines): what the project is,
  build/test/single-test commands, local run, architecture mental model,
  layout & dependency direction, repo-specific conventions (additive JSON
  templates, offline golden-file harness, SQLBI_Template idempotency,
  internal Tabular* back-references), a Documentation map, and a
  documentation-maintenance rule.
- docs/design/: index + focused design docs (overview, apply-templates
  lifecycle with a Mermaid dispatch flow, table generation, measures,
  domain model & conventions, testing).
- CLAUDE.md: add @AGENTS.md import alongside the existing experiment-team
  delegation policy (policy retained verbatim).
- Dax.Template.sln: add a Solution Items folder referencing the new docs
  (verified: dotnet sln list still lists all three projects).

Content verified against source with Serena; reviewer-approved. Root
README.md intentionally left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Bash(git --no-pager log|diff|status *) to project permissions.allow to
reduce prompts for read-only inspection commands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the Phase M initiative that precedes Phase 1 (Calendars): modernize to
C# 14 / .NET 10 idioms, uniform style, readability, and safe behavior-preserving
refactors. Captures Stages 0-4 (test-hardening safety net first, then style/
analyzer infra, mechanical modernization sweeps, deeper refactors, docs sync),
the per-subsystem specialist cadence, and the locked decisions:
warnings-as-errors (CI-only), file-scoped namespaces + primary constructors,
required-member migration under a major version bump, public API open to
improvement, and a coverage target (80% core floor / ~90% on refactor targets +
Stryker.NET mutation testing) instead of a flat 100%.

Planning only -- no code/tests/infra changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopt the dotnet-claude-kit plugin alongside experiment-team and codify
how the two work together, without weakening existing conventions.

- CLAUDE.md: add "Using dotnet-claude-kit" delegation subsection (lead-only
  cross-plugin composition, modern-C# injection into briefs, direct kit-agent
  delegation for .NET-idiom work, unchanged mandatory reviewer gate, scope
  discipline); rewrite navigation section to cover Serena + cwm-roslyn-navigator
  as complementary; document disabled post-edit-format hook and active
  pre-bash-guard / post-scaffold-restore hooks.
- AGENTS.md: note Roslyn Navigator MCP availability and C# 14 / .NET 10 baseline,
  cross-referencing CLAUDE.md.
- .claude/settings.json: enable dotnet-claude-kit plugin for this project.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an additive tooling layer to the roadmap mapping work to kit skills,
Roslyn Navigator tools, and kit agents. The five LOCKED Phase M decisions
and all stage/phase scope, targets, and checklists are unchanged.

- New "Phase M — dotnet-claude-kit alignment" subsection: Stage 0-4 -> skills
  (testing/tdd, ci-cd, modern-csharp, de-sloppify, error-handling, verify,
  security-scan), Roslyn tools (get_public_api, get_test_coverage_map,
  get_diagnostics, find_dead_code, detect_antipatterns,
  detect_circular_dependencies), and agents (build-error-resolver,
  refactor-cleaner, dotnet-architect, code-reviewer, security-auditor), with
  scope-out of web/EF/HTTP-oriented material and a de-sloppify dead-code
  safety note for the reflection-heavy paths.
- New "Feature phases (1-3) — kit defaults" subsection extends the same
  baseline to Calendars/calc-groups/UDFs: modern-csharp + dotnet-architect for
  new code, Serena+Roslyn navigation to wire Engine dispatch, get_public_api
  API-baseline, tdd/testing (scoped, FakeTimeProvider) + verify self-check,
  reviewer supplemented by code-reviewer/security-auditor.
- Generalize the alignment intro (kit is the repo-wide default per CLAUDE.md;
  Phase M is its heaviest usage) and relocate the annotated specialist cadence
  to close the alignment subsection so it no longer forward-references skills.
- Hard gates kept authoritative (additive JSON, byte-identical golden BIM,
  opt-in live-server, mandatory experiment-team:reviewer); kit verify framed
  as a pre-reviewer self-check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make SESSION_HANDOFF.md execution-ready for the next session:
- Resume banner now points at Phase M Stage 0 (test hardening).
- Phase M heading and locked-decisions preamble flipped from PLANNED/under
  review to IN EXECUTION with Stage 0 as the active work.
- "Next session" steps updated: add-calendar is pushed (@6bbad5d); Stage 0 is
  the active work, using the kit tooling per the alignment subsection.

No change to the five LOCKED decisions or any stage/phase scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reflection-based dump of Dax.Template's public+protected surface,
snapshotted via the existing GoldenFile harness (PublicApi.txt).
Deterministic (stable ordering, culture-invariant); regenerate with
UPDATE_GOLDEN=1. Surfaces intended-vs-accidental public API changes
for review per PR (change-detector, not a freeze gate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pin current behavior as a pre-refactor safety net: Engine dispatch
(Class->handler, unknown/invalid Class), idempotency (apply-twice +
SQLBI_Template orphan cleanup), dependency ordering (TSort DAG + cycle
detection), reflection paths (ReflectionHelper, GetModelChanges), and
broadened golden coverage (measures-only, holidays-only configs).
Additive configs/goldens only; existing snapshots untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pin StringExtensions helpers, Package load/invalid-config exception
mapping, CustomTableTemplate.GetHierarchies non-date path, MeasuresTemplate
time-intelligence wrapping, determinism, and cancellation honoring.
Also pins two reviewer-surfaced quirks: AutoScan-omitted ->
InvalidMacroReferenceException, and the Holidays handler's phantom-table-
on-validation-throw. Additive configs only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Targeted top-up on the thin refactor-target subsystems: MeasuresTemplate
86%->100%, MeasureTemplateBase 51%->98%, Package 48%->100% (SaveTo
round-trip + packaged-config load branches). Overall core coverage
75.2%->81.1%, clearing the locked 80% target. Meaningful assertions only,
no coverage padding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e 0 P0)

Restore the missing coverlet.runsettings (Dax.Template-only, Cobertura)
that ci.yml already referenced (CI was broken), record the coverage
baseline in docs/design/coverage.md, and set the CI gate to the locked
80% line-coverage floor (measured 81.1%, ~1.1pt headroom). Add
stryker-config.json (Tables/Measures/dependency-sort) as a non-gating
mutation-testing signal. Add three justified [ExcludeFromCodeCoverage]
attributes on genuinely live-server-only / trivial members
(ModelChanges.PopulatePreview, GetPreviewData; EntityBase.ToString).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update SESSION_HANDOFF: Stage 0 (test hardening) COMPLETE — suite 13->129
passed + 1 skipped, coverage 81.1% / CI floor 80, public-API baseline +
Stryker. Add the defect backlog surfaced by characterization tests (for
Stage 2/3) and the deferred PublicApiSurface renderer nits. Repoint
"next session" at Stage 1 (style/analyzer infrastructure).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Centralize LangVersion 14 / Nullable / EnableNETAnalyzers /
AnalysisLevel=latest-recommended / EnforceCodeStyleInBuild in
src/Directory.Build.props (TargetFramework left per-project so TestUI
stays net10.0-windows; no TreatWarningsAsErrors — WAE is CI-only, wired
later). Remove the now-redundant LangVersion/Nullable from the three
csprojs. Fix the pre-existing CS8602 in TestUI/ApplyDaxTemplate.cs:315
(SelectedItem?.ToString() — turns a latent NRE into the intended
TemplateException). Analyzers now surface 200 warnings (WAE off; build
green) to be triaged/cleaned in Stage 1B/1C and Stage 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marcosqlbi and others added 29 commits July 2, 2026 18:15
… 2.6b)

File-scoped namespaces across the 6 Tables/Dates files; CA1874 ×5
(BaseDateTemplate Regex.IsMatch), CA1805 ×8 (HolidaysDefinitionTable),
collection expressions, expression-bodied members, inlined out-vars
(TryGetValue always-assigns). Emitted date/holidays DAX byte-identical
(every @"..."/$@"..." literal untouched; goldens unchanged). Collection
expressions applied only where the declared type was already identical
(no covariance drift). CA1305/CA1309 (culture formatting in date code)
deliberately left untouched — deferred correctness decision. Remove CA1874
from the WarningsNotAsErrors allowlist (fully eliminated repo-wide; WAE
build green with it enforced). No public API change, suite 129 passed + 1
skipped, BOM preserved. Completes the Tables subsystem.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
File-scoped namespaces across the 7 interface files; CA1805 ×1
(ITemplates.TemplateEntry.IsHidden redundant = false). Public interface +
TemplateEntry POCO shapes unchanged (JSON-deserialization back-compatible).
No public API change (PublicApi.txt byte-identical), goldens byte-identical,
suite 129 passed + 1 skipped, BOM preserved. CA1805 stays allowlisted
(5 sites remain in CustomTemplateDefinition/Translations — next sweep).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e 2.7b)

File-scoped namespaces (Engine/Package/CustomTemplateDefinition/Translations);
CA1805 ×5 (CustomTemplateDefinition ×4, Translations ×1), collection
expressions, foreach over the Templates array (dropped a redundant ToList()
allocation — Templates is a fixed array, no mutation during iteration),
dropped unused locals/tuple element, expression-bodied FindTemplateFiles,
removed dead usings. Engine dispatch throw-semantics, GetModelChanges
reflection, and Package LoadFromFile/SaveTo/ReadDefinition exception types
untouched (pinned characterization tests green). Remove CA1805 from the
WarningsNotAsErrors allowlist — fully eliminated repo-wide (WAE build green
with it enforced). No public API change, goldens byte-identical, suite 129
passed + 1 skipped, BOM preserved. Completes the Dax.Template library
modernization.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
File-scoped namespaces across the 3 hand-written TestUI files; CA2263 ×5
(generic JsonSerializer.Deserialize<T>), CA1869 ×3 (hoisted two static
readonly JsonSerializerOptions, settings identical + correctly wired,
mutation-safe). Conservative sweep — TestUI has no automated coverage, so
mechanical/file-scoped only, no logic/UI change; Designer.cs untouched;
CS8602 fix intact; per-file BOM state preserved. Remove CA2263 from the
WarningsNotAsErrors allowlist (fully eliminated repo-wide; WAE build green
with it enforced). Goldens + PublicApi.txt byte-identical, suite 129 passed
+ 1 skipped. Completes Stage 2 non-breaking modernization.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Non-breaking fixes for the last 4 mechanical allowlist codes: CA1852 ×2
(seal internal test helpers), CA1816 (GC.SuppressFinalize in
PackageSaveToCharacterizationTests.Dispose), CA1859 (ModelChanges.
GetPreviewData private return object?->List<object>?, exact widening,
caller unaffected), CA1869 (Package.SaveTo JsonSerializerOptions hoisted to
static readonly, settings identical -> SaveTo JSON byte-identical). Remove
CA1816/CA1852/CA1859/CA1869 from the WarningsNotAsErrors allowlist — all
fully eliminated repo-wide (WAE build green with them enforced). No public
API change (PublicApi.txt byte-identical), goldens byte-identical, suite
129 passed + 1 skipped, BOM preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e 2.10a)

BREAKING: convert 4 `= default!` members to `required` — EntityBase.Name
(and subclasses Column/DateColumn/Hierarchy/Level/Measure), Level.Column,
Var.Name (and VarGlobal/VarRow), DaxStep.Name. Source-breaking for
consumers constructing these via object initializers; behavior-preserving
at runtime; JSON template config unaffected (none of these types are
JSON-deserialized — verified). All existing construction sites already set
the members (0 CS9035). Bump Dax.Template to 2.0.0 (Assembly/File/Version-
Prefix). Enhance PublicApiSurface to detect RequiredMemberAttribute and
regenerate the PublicApi.txt baseline (the 4 members now render `required`).
CHANGELOG updated. Golden BIM byte-identical, suite 129 passed + 1 skipped,
WAE build green.

NOTE: maintainer must bump the Azure DevOps `AppVersionMajor` pipeline
variable to 2 (ADO variables can't be changed from the repo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…age 2.10b)

BREAKING (2.0.0): rename public identifiers flagged by CA1707/CA1711/
CA1716/CA1725 — de-underscore 18 public constants (values unchanged, e.g.
SQLBI_TEMPLATE_ATTRIBUTE -> SqlbiTemplate = "SQLBI_Template"); drop the
Enum suffix on 5 enum types (AutoScanEnum->AutoScan, AutoNamingEnum->
AutoNaming, SubstituteEnum->Substitute, TestUI WeeklyType/QuarterWeekType,
DayOfWeekEnum->WeekDay to avoid System.DayOfWeek); rename reserved-keyword
identifiers (param template->templateDefinition, nested type Step->
TemplateStep); align BaseDateTemplate.ApplyTemplate param dateTable->
tabularTable with the base. Enum source files renamed to match. All
identifier-only: constant values, enum member values, and JSON property
names unchanged -> emitted BIM byte-identical, existing template JSON still
loads. PublicApi.txt regenerated (renames only); CHANGELOG updated. Remove
CA1707/CA1711/CA1716/CA1725 from the WarningsNotAsErrors allowlist — all
fully eliminated repo-wide (WAE build green with them enforced). Allowlist
now holds only CA1051 (deferred) + CA1305/CA1309 (Stage 3). Suite 129
passed + 1 skipped, BOM preserved. Completes the option-2 breaking pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update stale identifier references in docs/design/domain-model-and-
conventions.md and measures.md (AutoScanEnum->AutoScan, AutoNamingEnum->
AutoNaming, SQLBI_TEMPLATE_ATTRIBUTE->SqlbiTemplate, CONFLICT_RENAME_PREFIX
->ConflictRenamePrefix) and comments in MeasureTemplateBaseCharacterization
Tests (ENTITY_*->Entity*). Identifier references only — string values
("SQLBI_Template", "_old") unchanged; CHANGELOG's rename mapping left as
historical record. Build + suite unaffected (129 passed + 1 skipped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record Stage 2 outcomes: 10 modernization sweeps + bucket-C, WAE allowlist
ratcheted 17->3 (CA1051/CA1305/CA1309 remaining), and the required/2.0.0
API-breaking pass (2.10a/2.10b). Note the deferred CA1051 field->property
follow-up and the ADO AppVersionMajor bump. Repoint "next session" at
Stage 3 (deeper refactors + Stage 0 defect-backlog fixes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Stage 2 tail)

BREAKING (2.0.0): convert the 4 remaining visible protected instance fields
to properties — MeasureTemplateBase.Template (get-only, ctor-assigned,
readonly semantics preserved), TableTemplateBase.FixRelationshipsTo/From
(get/set), Translations.LanguageDefinitions (get/set, ctor-assigned).
Source/binary-breaking for subclasses; no runtime/DAX-BIM/JSON impact
(these hold template-build state, none are JSON-deserialized; verified no
ref/out usage). PublicApi.txt regenerated (only the 4 field->property
changes); CHANGELOG updated. Remove CA1051 from the WarningsNotAsErrors
allowlist — fully eliminated repo-wide (WAE build green with it enforced).
Allowlist now holds only CA1305/CA1309 (Stage 3 culture decision). Golden
BIM byte-identical, suite 129 passed + 1 skipped, BOM preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record that the CA1051 field->property follow-up completed (9ead9ae); the
2.0.0 public-API cleanup is complete and the WAE allowlist now holds only
CA1305/CA1309 (Stage 3 culture decision). Stage 3 remains the active,
opt-in-per-item stage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the duplicated annotation-upsert loop (closing the long-standing
TODO in TableTemplateBase) into a new internal extension
AnnotationCollectionExtensions.UpsertAnnotations, generic over TOM's
NamedMetadataObjectCollection<Annotation, TOwner> base. TableTemplateBase
.AddAnnotations keeps its protected virtual seam and delegates (retaining
its upfront cancellation throw); MeasureTemplateBase's local ApplyAnnotations
is removed in favor of the shared helper.

Behavior-preserving: build green, offline suite 129 passed + 1 skipped,
golden BIM and PublicApi.txt byte-identical. Reviewed (code-reviewer: GO).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix the 'Circulare'->'Circular' typo in CircularDependencyException and
rename misleading public ctor params: daxExpressionmessage->daxExpression
(CircularDependency, InvalidVariableReference, InvalidMacroReference x3)
and entitymessage->entityName (InvalidAttributeException). These name a DAX
expression / entity, not a 'message'.

Public ctor param renames -> PublicApi.txt regenerated deliberately (diff
limited to the 6 renamed lines); ships under the in-progress 2.0.0. All
in-repo throw sites use positional args, so no internal breakage. Golden BIM
byte-identical; offline suite 129 passed + 1 skipped. Reviewed (code-reviewer: GO).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add /// summaries to the 9 previously-undocumented public/internal Syntax
types (DaxBase, Var, VarGlobal, VarRow, VarScope, IDependencies<T>, IDaxName,
IDaxComment, IGlobalScope), matching the existing DaxElement/DaxStep tone.
The subsystem is already modern (Stage 2.4) and holds only POCOs/interfaces,
so documentation was the genuine readability win rather than a refactor.

Doc-comment-only: no code/signature changes, build green (no CS1570), offline
suite 129 passed + 1 skipped, golden BIM and PublicApi.txt byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tem 2)

Make ReflectionHelper and its GetPropertyValue/SetPropertyValue extensions
internal (were public; tests reach them via InternalsVisibleTo) -> PublicApi.txt
loses those 3 lines; ships under the in-progress 2.0.0. SetPropertyValue kept
(no prod caller yet; reserved for Phase 1 Calendar internal-member writes).

Centralize the 6 reflected TOM-internal member names (TxManager, CurrentSavepoint,
AllBodies, Owner, LastParent, Parent) as documented consts in a private nested
TomInternalMembers class, extract the TxManager->AllBodies traversal into
GetChangedBodies (exact null-propagation + hard (IEnumerable) cast preserved),
modernize string.Format->interpolation, and document the TOM-version fragility
plus the offline-apply-returns-empty behavior on GetModelChanges.

Behavior-preserving: whole-solution build green, offline suite 129 passed +
1 skipped, golden BIM byte-identical. Reviewed (code-reviewer: GO).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
B1 - Engine.ApplyHolidaysDefinitionTable validated an empty Template only
AFTER adding the table, leaving a phantom empty table in the model on throw.
Hoist the validation above the create/add block so it fails before any model
mutation (matching ApplyCustomDateTable/ApplyMeasuresTemplate).

B2 - CustomTableTemplate.GetHierarchies used Columns.First(...) for a level's
column, throwing a bare InvalidOperationException on an unknown column. Use
FirstOrDefault + a TemplateException naming the hierarchy, level, and column.

Both are error-path fixes: the two characterization tests that pinned the buggy
behavior are converted to fix-tests. Golden BIM + PublicApi.txt byte-identical,
offline suite 129 passed + 1 skipped. Reviewed (code-reviewer: GO).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… B: B3)

AddHierarchies built TOM Hierarchy/Level objects without copying Description,
silently dropping descriptions supplied in template JSON (the source
Model.Hierarchy/Level.Description are populated by GetHierarchies). Add the
Description assignment to both initializers, mirroring the existing AddColumns
pattern. Converted the characterization test that pinned the drop into a
fix-test.

Latent-correctness fix: no shipped config carries hierarchy/level descriptions
today, so golden BIM + PublicApi.txt are byte-identical; offline suite 129
passed + 1 skipped. Reviewed (code-reviewer: GO).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TSort.VisitDependencies only caught direct self-cycles immediately; a 2+ node
mutual cycle (A->B->A) was caught only after the crude nestedCalls > 1000
backstop, throwing a generic {STACK OVERFLOW} message. Replace both the
nestedCalls/MAX_NESTED_CALLS guard and the self-only Contains(item) check with
proper DFS recursion-path tracking (HashSet path + try/finally backtrack), so
self-, 2-node, and N-node cycles all throw CircularDependencyException promptly
with the real offending node's expression. Remove the now-unused MAX_NESTED_CALLS.

Valid acyclic graphs sort identically (backtrack preserves the level arithmetic
and diamond re-joins), so golden BIM + PublicApi.txt stay byte-identical; offline
suite 129 passed + 1 skipped. Converted the characterization test that pinned the
nested-guard behavior into a prompt-detection fix-test. Reviewed (code-reviewer:
GO-WITH-NITS; the tautological assertion nit was fixed).

Note: removing the soft 1000-depth backstop means a valid but pathologically deep
acyclic graph now overflows the CLR stack (uncatchable) instead of throwing; no
current config approaches this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A disabled CustomDateTable entry returned early without removing a
previously-created date table, asymmetric with the Holidays handlers that
clean up on disable. Remove the date table (and its ReferenceTable, if set)
in the disabled branch; orphan relationships are swept centrally by the
existing post-pass, so this is safe and consistent with precedent.

Converted the characterization test that pinned the no-cleanup behavior into
a fix-test, added a Dispatch-08 fixture + test covering the ReferenceTable
removal branch, and corrected two now-stale comments. Golden BIM + PublicApi.txt
byte-identical; offline suite 130 passed + 1 skipped (+1 new test). Reviewed
(code-reviewer: GO-WITH-NITS; the coverage + comment nits are addressed here).

Completes Phase M Stage 3 Group B (all five Stage-0 defect-backlog items fixed).

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

Make all year-int -> DAX formatting culture-invariant so generated DAX is
locale-independent: int.ToString() -> ToString(CultureInfo.InvariantCulture)
and interpolated int holes wrapped in FormattableString.Invariant(...) across
GenerateMinYearExpression / GenerateMaxYearExpression / GenerateCalendarExpression
(CALENDARAUTO fallback). DAX text (incl. trailing spaces) byte-preserved.
Make MeasuresTemplate's measure-name match explicitly StringComparison.Ordinal
(was already ordinal via the one-arg overload).

Removes CA1305 and CA1309 -- the last two codes -- from the CI warnings-as-errors
allowlist in Directory.Build.props, which is now empty: the Stage-2 analyzer debt
is fully cleared and any new analyzer/compiler warning fails CI.

Behavior-preserving on Latin-digit cultures: golden BIM + PublicApi.txt
byte-identical, offline suite 130 passed + 1 skipped, solution build green with
-p:TreatWarningsAsErrors=true (0 warnings repo-wide). Reviewed (code-reviewer:
GO-WITH-NITS; the CALENDARAUTO-fallback consistency nit is addressed here).

Completes Phase M Stage 3 (deeper refactors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…4 closeout)

Document all Phase M Stage 3 changes and close out Phase M:
- CHANGELOG [Unreleased]/2.0.0: exception ctor param renames (daxExpression/
  entityName) + Circular typo; ReflectionHelper -> internal; the five Group B
  behavior fixes (Holidays phantom table, GetHierarchies TemplateException,
  Hierarchy/Level Description copy, prompt multi-node cycle detection,
  CustomDateTable disable cleanup); invariant-culture DAX year formatting.
- AGENTS.md: WarningsNotAsErrors allowlist now empty; CA1707 production-clean.
- .editorconfig: correct the stale CA1707 'deferred to Stage 2' comment.
- docs/design: TSort DFS cycle detection, dispatch disable/validation behavior,
  GetModelChanges offline note, Description copy-through, invariant year
  formatting, Syntax XML docs; coverage.md dated Stage-3 suite-count note.

Reviewed (code-reviewer: GO-WITH-NITS; all claims verified against source, the
coverage.md wording nit fixed). Completes Phase M (Stages 0-4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…TOM API)

Investigation (reflection over TOM 19.114.0 + cross-check against Tabular
Editor 2) shows the Calendar binding has a fully PUBLIC typed API in our pinned
19.114.0 via the concrete subclasses TimeUnitColumnAssociation /
TimeRelatedColumnGroup (public ctors, public TimeUnit/PrimaryColumn/
AssociatedColumns/Columns). The earlier RISK looked at the abstract base
CalendarColumnGroup and its internal low-level members and missed the subclasses.

Phase 1 therefore needs NO reflection and NO TMSL/JSON injection. Remaining
constraints recorded: min compatibility level 1701, and Calendar has no
Annotations (idempotency must key off the parent table's SQLBI_Template
annotation). Updates the RISK section, Open Question #1, and the TOM object-model
note in SESSION_HANDOFF.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase M (Stages 0-4) is complete; update the top-of-file resume line from the
stale 'start Phase M Stage 3' to 'start Phase 1 (Calendars)' with the resolved
public-API binding note, and bump Last updated to 2026-07-04.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nt (Phase 1)

Add a new template Class "CalendarTemplate" that attaches a native TOM
Calendar (with column groups) to an existing table, using the public typed
API (TimeUnitColumnAssociation / TimeRelatedColumnGroup) — no reflection,
no TMSL. JSON config is purely additive (reuses Class/Table/Template/
IsEnabled; no TemplateEntry change).

Backend:
- CalendarTemplateDefinition POCO + nested CalendarColumnGroupDefinition
  (Tables/Calendars/).
- CalendarTemplate.ApplyTemplate: find-or-create the Calendar keyed by
  Calendar.Name (Calendar has no Annotations, so SQLBI_Template can't live
  on it); re-apply clears+rebuilds column groups; IsEnabled=false removes
  the named calendar.
- Engine dispatch wired via nameof(CalendarTemplate). Disabled entry with a
  missing target table is a safe no-op, matching sibling handlers; enabled
  entry with a missing table throws TemplateException.
- Compatibility guard: TOM enforces level >= 1701 at Table.Calendars.Add();
  the handler pre-checks and throws InvalidConfigurationException (also
  guards Model?/Database null on the public method).

Tests:
- Separate compat-1701 CalendarOfflineModelFixture (shared 1600 fixture
  untouched, existing goldens byte-identical).
- CalendarGoldenTests: shape, snapshot (Config-02 - Calendar.bim),
  idempotency, disabled-removal, disabled-with-missing-table regression,
  and opt-in live-server. New template fixtures added.
- PublicApi.txt regenerated (diff limited to the 3 new Calendar types).
- Suite: 135 passed + 2 skipped; build green under warnings-as-errors;
  dotnet format clean.

Known limitation (documented, deferred): deleting/renaming a CalendarTemplate
entry orphans its calendar — matches the existing CustomDateTable rename-TODO
and MeasuresTemplate entry-deletion precedent.

Docs: CHANGELOG, AGENTS, apply-templates-lifecycle, table-generation,
SESSION_HANDOFF updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…generation (Phase 2)

Add a new template Class "CalculationGroupTemplate" that generates a native
TOM calculation group (a Table whose Table.CalculationGroup holds
CalculationItems). Generic generator — the JSON authors any calc group; it
has NO dependency on the Measures/Syntax/time-intelligence machinery (calc
groups are being deprecated for time intelligence). JSON config is purely
additive (reuses Class/Table/Template/IsHidden/IsEnabled; no TemplateEntry
change).

Backend:
- CalculationGroupTemplateDefinition POCO + nested CalculationItemDefinition
  (Tables/CalculationGroups/); CalculationGroupExpression selection
  expressions (MultipleOrEmptySelection / NoSelection, each with an optional
  format-string expression) supported from day 1.
- CalculationGroupTemplate.ApplyTemplate validates the whole definition
  before mutating (non-empty items; per-item name + expression; non-empty
  ColumnName; ordinal uniqueness), then stamps the table ownership
  annotation, builds/reuses the CalculationGroup (Precedence/Description +
  selection expressions), ensures one String backing column + a
  CalculationGroupSource partition, and reconciles calc items
  full-replace-by-name (orphans removed).
- Engine dispatch wired via nameof(CalculationGroupTemplate). Disabled entry
  removes the table (missing table = safe no-op). Foreign-table guard: a
  pre-existing table lacking the SQLBI_Template="CalculationGroup" annotation
  throws TemplateException rather than being overwritten. Validate-before-
  mutate + build-then-add => no phantom table on invalid input. Backing
  column receives a LineageTag at compat >= 1540. No RequestTableRefresh.
- New const Attributes.SqlbiTemplateTableCalculationGroup = "CalculationGroup".

Compatibility (enforced by TOM at assignment): calc group >= 1470,
calc items >= 1500, selection expressions >= 1605.

Idempotency: keyed on the calc-group table's SQLBI_Template annotation
(the table has Annotations, unlike a Calendar). Re-apply reconciles items and
clears omitted selection expressions.

Known limitation (documented, deferred): renaming ColumnName orphans the old
backing column — matches the Calendar / CustomDateTable rename precedent.

Tests:
- Dedicated compat-1605 CalcGroupOfflineModelFixture (shared 1600 + Calendar
  1701 fixtures untouched, existing goldens byte-identical).
- CalculationGroupGoldenTests: shape, snapshot (Config-03), idempotency,
  orphan-item removal, selection-expression clear-on-empty, disabled-removal,
  disabled-with-missing-table, foreign-table collision, ordinal uniqueness,
  and opt-in live-server.
- PublicApi.txt regenerated (diff limited to the new types + the new const).
- Suite: 144 passed + 3 skipped; build green under warnings-as-errors;
  dotnet format clean.

Docs: CHANGELOG, AGENTS, apply-templates-lifecycle, table-generation,
README, SESSION_HANDOFF updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ctions (Phase 3)

Add a new template Class "FunctionLibraryTemplate" that generates DAX
user-defined functions (UDFs) into Model.Functions (model-level; one
sub-template file declares a library of one-or-more functions). Adopts the
current DAX UDF standard (GA Sept 2025 + March 2026 reference types),
including the full parameter type system and optional parameters. JSON
config is purely additive (reuses Class/Template/IsHidden/IsEnabled; no
TemplateEntry change).

Schema (hybrid): parameters are modeled structurally and the engine
assembles the "( params ) => body" signature per the grammar, with a
RawExpression escape hatch used verbatim.
- Types: ANYVAL (default), SCALAR + subtype, TABLE, and the REF family
  (ANYREF/MEASUREREF/COLUMNREF/TABLEREF/CALENDARREF, which force EXPR and
  never carry an explicit passing mode); scalar-subtype shorthands
  (INT64/DECIMAL/DOUBLE/STRING/BOOLEAN/DATETIME/VARIANT/NUMERIC).
- Passing modes VAL/EXPR; a trailing "= <DefaultExpression>" makes a
  parameter optional. Body via Body or MultiLineBody.

Backend:
- FunctionLibraryTemplateDefinition + FunctionDefinition (+ GetBody()) +
  ParameterDefinition POCOs in src/Dax.Template/Functions/.
- FunctionLibraryTemplate.ApplyTemplate validates the entire definition
  before mutating, then find-or-creates each function by name, sets
  Expression/Description/IsHidden (+ LineageTag on new functions), stamps
  the ownership annotation, and reconciles by name with orphan cleanup.
  IsEnabled=false removes all functions carrying this template's annotation.
- Validation (all throw InvalidConfigurationException): RawExpression XOR
  Body/MultiLineBody; SCALAR requires a Subtype; PassingMode rejected on REF
  types; PassingMode requires an explicit Type; optional params must trail
  mandatory ones; no duplicate function names in a library; non-blank
  function/parameter names.
- Engine dispatch wired via nameof(FunctionLibraryTemplate); model-level, no
  table, no RequestTableRefresh.
- New const Attributes.SqlbiTemplateFunctions = "Functions".

Compatibility: UDFs require level >= 1702 (TOM enforces at
Model.Functions.Add); the handler pre-checks and throws a friendly
InvalidConfigurationException below that. TOM performs no grammar validation
of Function.Expression, so the engine's structured validation is the only
offline safety net; the generated signatures are only compiled by the
opt-in live-server test.

Idempotency: annotation-keyed (SQLBI_Template="Functions"), like Measures
and calculation groups. Re-apply reconciles by name and sweeps orphans; a
renamed function's old object is correctly removed (no rename limitation,
unlike Calendars).

Known limitation (documented, deferred): cross-file name collisions across
separate library entries are not detected (last-applied-wins) — matches the
MeasuresTemplate precedent.

Tests:
- Dedicated compat-1702 FunctionOfflineModelFixture (the 1600/1605/1701
  fixtures untouched, existing goldens byte-identical).
- FunctionLibraryGoldenTests: shape (exact rendered Expression strings),
  snapshot (Config-04), idempotency, orphan cleanup, enable/disable, the
  compat-1702 guard, RawExpression pass-through, and every validation rule
  (incl. PassingMode-without-Type, blank names, and the neither-branch).
- PublicApi.txt regenerated (diff limited to the 4 new types + the new const).
- Suite: 161 passed + 4 skipped; build green under warnings-as-errors;
  dotnet format clean.

Docs: new docs/design/functions.md (indexed in AGENTS.md + README.md),
CHANGELOG, AGENTS, apply-templates-lifecycle updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tests in a new session

Offline roadmap (Calendars, Calc groups, UDFs) is done, reviewed, and pushed.
Update the top-of-file resume instructions to start a NEW session (this one is
ending for maintenance) and run the opt-in [LiveServerFact] tests for all three
phases against a compat >= 1702 database — the only place the generated DAX is
actually compiled. Record the Phase 3 completion detail and the 161+4 suite count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oncile committed/pushed status

All three [LiveServerFact] tests plus the Phase-0 baseline passed against the
published EmptyTestTemplates.pbix (compat >= 1702): 4 passed / 0 failed / 0 skipped.
Non-destructive (ApplyTemplates + GetModelChanges, never SaveChanges). The Phase-3
UDF pass confirms the generated ( params ) => body signatures compile server-side.
Also reconciled the stale 'NOT yet committed' per-phase notes with HEAD d70fb32.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant