Skip to content

feat(mtk): [Master][Worker][Human] | ESS NextGen Migration Toolkit — discovery → DA-compat transform (RULE-002–007) → solution-aware writeback + polished report, single-run CLI & CI#210

Open
anil-adepu wants to merge 24 commits into
mainfrom
users/aniladepu/features/mtk/customization-discovery-and-core-generalization
Open

feat(mtk): [Master][Worker][Human] | ESS NextGen Migration Toolkit — discovery → DA-compat transform (RULE-002–007) → solution-aware writeback + polished report, single-run CLI & CI#210
anil-adepu wants to merge 24 commits into
mainfrom
users/aniladepu/features/mtk/customization-discovery-and-core-generalization

Conversation

@anil-adepu

@anil-adepu anil-adepu commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Builds out the ESS NextGen Migration Toolkit (MTK) end-to-end: input/customization
discovery → CA→DA transformation rules → solution-aware Dataverse writeback → a
polished, customer-facing migration report, plus the runtime (single run CLI) and CI
that surround it. All quality gates green (ruff, format, mypy strict, 113 unit tests).

Role legend

  • [Master] — MTK SDD orchestrator: planning, spec authorship, reviews, and most implementation this pass.
  • [Worker] — background implementation worker (TASK-007 output pipeline).
  • [Human] — @aniladepu_microsoft: branch integration/merges, PM-gated calls, live-validation direction.

Runtime, CLI & CI — [Master]

  • Consolidated start/refresh into one run command; customer runs reset to pristine main with data-loss guards (never discard local commits).
  • Orchestrator --mode selection, run summary + structured error handling (TASK-003).
  • Path-filtered CI workflow: toolkit lint (ruff), format check, mypy (strict), unit tests; .pre-commit-config.yaml scoped to the toolkit; gates documented in dev-specs.

Customization discovery — [Master]

  • Per-agent discovery via RetrieveDependenciesForUninstallWithMetadata → per-component msdyn_componentlayers → classify (Active non-OOB solution + type-9 + ESS schemaname) → hydrate CustomizationComponent (incl. component_type/label + statecode/statuscode).

DA-compatibility transform — [Master]

  • ApplyDaCompatibilityStep: foundational CA→DA rewrite (AI model kind + template + configuration) run before the rule steps (TASK-016).

Migration rules (deterministic, disable-but-preserve) — [Master]

  • RULE-002 — replace EndConversation with CancelAllDialogs (TASK-011).
  • RULE-003/004 — disable OnActivity + OnGeneratedResponse topics (TASK-012/013).
  • RULE-006/007 — disable unsupported-trigger topics (5) and unsupported-node topics (7), delivered as one thin step per construct over shared UnsupportedTopicTriggerStep / UnsupportedNodeStep bases, each carrying a tailored, user-facing mitigation message (TASK-018/019).
  • Disable = statecode/statuscode → Inactive + [DEPRECATED] name prefix; topic logic (data) is never rewritten; idempotent (MIG-005).

Writeback & output pipeline — [Worker] + [Master]

  • [Worker] TASK-007 output pipeline: generic Dataverse writeback + validation + report rendering.
  • [Master] WritebackPlan accumulator — coalesces multiple edits per record into a single PATCH, with a meaningful-change no-op guard (skip writeback when the actual vs. modified copy don't meaningfully differ). Solution-aware writeback via the MSCRM.SolutionUniqueName header so edits land in the customer's target unmanaged solution (clean ALM).

Polished migration report — [Master]

  • migration_report.md is rendered from MigrationContext as a well-structured, eye-catching document with a status banner and emoji section headers:
    • 📋 Summary — run metrics table (mode, selected agent, customizations, components changed, warnings/errors with status glyphs).
    • 🌐 Environment — URL / tenant / signed-in user / target solution table.
    • 🤖 Agents — discovered ESS agents table with the selected one marked.
    • 🧩 Customizations Extracted — count + type/name/schema/state table.
    • 🔧 Migration Mitigations by ComponentComponent Type | Component | Mitigations Applied (Rules), one row per topic, every rule's mitigation merged (styled after the CA→DA component-support analysis).
    • ⚠️ Warnings / ❌ Errors — numbered # | Component | Reason/Error | Recommendation tables (graceful empty states).
    • ✅ Next Steps — Action Required — checkbox checklist leading with "run your own end-to-end evaluations" before promoting through ALM.
  • Every topic rule (RULE-002/003/004/006/007) records a structured per-component Change via record_topic_change (TASK-020).

SDD / dev-specs — [Master]

  • Backlog status upkeep; TASK-010 (RULE-001) BLOCKED pending ESS PM input; TASK-009 unblocked with enriched live-validation criteria; botcomponent statecode/statuscode semantics confirmed against Dataverse docs.

Integration & decisions — [Human]

  • Merged the individual rule/worker branches into this super-branch and pushed to remote.
  • Held RULE-001 (agent-instructions payload) for ESS PM sign-off.
  • Set the policy to disable + deprecate every unsupported construct and drove the live-E2E (TASK-009) validation plan.

Follow-ups (out of scope here)

  • TASK-010 (RULE-001) — blocked on ESS PM input for the canonical DA instructions payload.
  • TASK-009 live E2E — confirm the analysis-sourced node kind: tokens against real topic YAML, and confirm whether a single PATCH may combine a state change (statecode/statuscode) with content columns (name/data); if rejected live, Writeback splits state into its own PATCH.

@anil-adepu
anil-adepu force-pushed the users/aniladepu/features/mtk/customization-discovery-and-core-generalization branch from 3107669 to 6cea706 Compare July 22, 2026 22:32
@anil-adepu anil-adepu changed the title feat(mtk): [Master] | Input discovery + DA-compat transform, framework generalization, single run CLI & CI feat(mtk): [Master][Worker][Human] | ESS NextGen Migration Toolkit — discovery → DA-compat transform (RULE-002–007) → solution-aware writeback + per-topic report, single-run CLI & CI Jul 23, 2026
@anil-adepu anil-adepu changed the title feat(mtk): [Master][Worker][Human] | ESS NextGen Migration Toolkit — discovery → DA-compat transform (RULE-002–007) → solution-aware writeback + per-topic report, single-run CLI & CI feat(mtk): [Master][Worker][Human] | ESS NextGen Migration Toolkit — discovery → DA-compat transform (RULE-002–007) → solution-aware writeback + polished report, single-run CLI & CI Jul 23, 2026
anil-adepu and others added 24 commits July 25, 2026 00:02
…alization

Build out the Input and Transformation stages and generalize the core
framework so it carries no product-specific vocabulary.

Input (preprocessing):
- RetrieveAgentConfigurationStep: fetch bot record + gpt.default component.
- RetrieveCustomizationsStep: resolve ESS base solution by vertical,
  RetrieveDependenciesForUninstallWithMetadata, bulk-fetch msdyn_componentlayers
  (chunked + paginated), classify via the ~1900 sentinel rule.
- GatherALMCustomerInputStep (renamed): GetPreferredSolution cross-check.

Transformation (renamed from migration):
- ApplyDaCompatibilityStep: idempotent CA->DA rewrite (PreviewModels ->
  MicrosoftCopilotModels, template default-* -> gptagent-1.0.0, config model
  block), producing pending_writes for the Output stage.

Core framework generalization (core/ is now extractable):
- ExecutionMode moved out of core into modules/transformation/models; base
  ExecutionContext carries a generic opaque mode: str.
- Reporter moved core/logging -> service/ (core/logging keeps generic Logger +
  SessionManager only).
- SessionManager report filename is a constructor arg; generic default
  telemetry_report.md, ESS supplies migration_report.md via service.constants.
- Modes model sanitized to two authoritative values (READONLY/WRITEBACK);
  Discover/Preview/Migrate kept only as customer-journey intents.

Docs/specs: new CUSTOMIZATION_DISCOVERY.md, reframed TASK-006/007, new TASK-016,
synced the meta/product/architecture/engineering specs + CHANGELOG.

Gates: ruff clean, mypy clean (61 files), pytest 67 passed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add an `mtk-toolkit` job to .github/workflows/ci.yml that runs the ESS
NextGen Migration Toolkit's quality gates on push/PR to main:

- uv sync --frozen (locked deps from uv.lock, Python 3.11)
- uv run ruff check .
- uv run ruff format --check .
- uv run mypy src
- uv run pytest  (the unit tests)

Runs from tools/ess-nextgen-migration-toolkit via job working-directory, so it
is scoped to the toolkit and independent of the existing maker-skills jobs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…kflow

Move the toolkit lint/types/tests job out of ci.yml into a dedicated
.github/workflows/mtk-toolkit-ci.yml so it only runs when the toolkit (or the
workflow file) changes — a workflow-level `paths:` filter in ci.yml would have
gated the unrelated maker-skills jobs too.

The job is unchanged (uv sync --frozen, ruff check, ruff format --check,
mypy src, pytest); ci.yml is restored to its original lint + installer jobs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reflect the new GitHub Actions workflow in the specs:
- REPOSITORY_STRUCTURE.md §11 reframed to "Quality Gates (Pre-Commit + CI)":
  pre-commit enforces format/lint/type locally; CI (mtk-toolkit-ci.yml) enforces
  the same plus unit-test execution on push/PR to main. Corrects the prior
  claim that pre-commit runs the unit tests (it does not; CI does).
- CHANGELOG: added the CI workflow entry.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
TASK-006 (Input: agent config + customization discovery) is complete — all
deliverables implemented, unit-tested, and gated (ruff/mypy/pytest in CI). Its
"confirm field names" AC was mis-scoped (it listed template/configuration/data,
which TASK-006 never parses — it fetches whole records and uses the msdyn_*
discovery fields verified from live API samples). Rewrote that AC to the fields
it actually uses (confirmed) and flipped ACTIVE -> DONE.

TASK-016 (Transformation: DA-compatibility) stays ACTIVE: implemented, registered,
and gated, but the transform-target fields (template/configuration/data) are still
UNCONFIRMED against a live record — a genuine open item that must be verified
before TASK-007 writeback can be trusted.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…SK-009

Reconcile the 016/007 boundary (they are producer -> consumer, not duplicates):
- TASK-016 (Transformation) computes pending_writes, no I/O — now DONE. Removed
  its live field-name confirmation AC.
- TASK-007 (Output) consumes/persists pending_writes — added a Boundary note.
- The single live field-name confirmation (template/configuration/data/
  botcomponentid via ./mtk.sh start --dev, WRITEBACK) moved to TASK-009 (E2E),
  where it belongs end-to-end; TASK-009 now Consumes TASK-016.

TASK-006/015 left as-is (phased Input-stage tasks, both DONE).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ent Instructions)

TASK-010 (RULE-001) and TASK-016 (ApplyDaCompatibilityStep) overlapped on
Template + Model Kind. Resolve by clean split, not merge:

- TASK-016 keeps the foundational DA-compat nomenclature rewrite (Template,
  Model Kind, config) — DONE.
- TASK-010 / RULE-001 re-scoped to ONLY the Agent Instructions override — TODO,
  delivered later by a new OverrideAgentInstructionsStep.

Renamed RULE-001 "Override Agent Metadata" -> "Override Agent Instructions" and
its step OverrideAgentMetadataStep -> OverrideAgentInstructionsStep across
MIGRATION_RULES, PIPELINES, DIAGNOSTICS, CODING_STANDARDS, IMPLEMENTATION_GUIDE,
and the TASKS index.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…y + error handling

Take TASK-003 (Migration Orchestrator) to closure:
- Parse `--mode readonly|writeback` (`_resolve_mode`; `--mode X` and `--mode=X`,
  case-insensitive; invalid -> friendly SystemExit) -> MigrationContext.mode,
  default READONLY.
- On success, log a one-line summary (mode, agent, changes/warnings/errors,
  bundle path); on failure, LogError a user-friendly message with the session-log
  path and always close the logger.
- `mtk.sh` / `mtk.ps1` parse + forward `--mode` / `-Mode` (bash 3.2 + set -u safe).
- Unit tests for `_resolve_mode` and `_log_summary`; FakeLogger gains LogError.

Marks TASK-003 DONE. Gates: ruff + format + mypy (61) + pytest (71) green.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…`; customer resets to pristine main

Replace `mtk start` + `mtk refresh` with a single `mtk run` to remove the
start/refresh confusion. One command, two modes via `--dev`:

- customer (`mtk run`, no --dev): reset the checkout to pristine origin/main
  (`git fetch` + `git checkout -f -B main origin/main` + `git clean -fd`),
  discarding any local branch/commits/uncommitted/untracked changes so the tool
  only ever runs from reviewed main (gitignored .venv/.local/output preserved),
  then provision runtime-only and run.
- contributor (`mtk run --dev`): provision runtime + dev tooling, install hooks,
  and SKIP the reset (contributors manage their own branches).

`--mode readonly|writeback` works with both, in any order.

Docs sanitized to match: scripts/mtk.{sh,ps1} + monorepo-root forwarders,
.pre-commit-config.yaml, toolkit README, REPOSITORY_STRUCTURE §11a/§11b,
CODING_STANDARDS, TASK-003/009/015/016, and reconciled the remaining middle-stage
naming "Migration Pipeline" -> "Transformation Pipeline" across ARCHITECTURE,
PIPELINES, VOCABULARY, IMPLEMENTATION_GUIDE. CHANGELOG updated.

bash -n + PowerShell parser clean; ruff + mypy (61) + pytest (71) green.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ard local commits

The customer `mtk run` path must not destroy saved work. Two safeguards:

- Run from a DETACHED checkout of origin/main (`git checkout -f origin/main`),
  never `checkout -f -B main` — so no branch pointer is moved and local commits
  and branches are always preserved. Only uncommitted changes + untracked files
  are discarded (`git clean -fd`; gitignored .venv/.local/output preserved).
- Confirmation guard: proceeds silently only when the work tree is clean;
  otherwise prints exactly what will be discarded and requires an interactive
  `yes` (or `--yes`); REFUSES in a non-interactive shell. `--dev` skips it all.

Implemented in scripts/mtk.sh (confirm_reset_or_abort / sync_to_main) and
scripts/mtk.ps1 (Confirm-ResetOrAbort / Sync-ToMain). Verified in a scratch repo
that a clean feature branch with local commits triggers no prompt and keeps its
commit after sync. bash -n + PowerShell parser clean. Docs updated (README,
REPOSITORY_STRUCTURE §11a, CHANGELOG).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…nComponent

Bring RetrieveCustomizationsStep in line with the live Dataverse API and
hydrate the discovered customizations for the transformation stage.

- call RetrieveDependenciesForUninstallWithMetadata(SolutionId=<guid>): resolve
  the ESS base solution's solutionid first; call_function now inlines a
  GUID-shaped value as an unquoted Edm.Guid literal (others stay quoted strings).
- fetch msdyn_componentlayers per component (pairing msdyn_componentid with
  msdyn_solutioncomponentname from dependentcomponententitylogicalname); the
  virtual table won't OR ids, so sequential per-id reads replace the chunked-OR
  bulk fetch.
- classify by solution/type/schemaname (not the unreliable ~1900 overwritetime):
  keep customer changes (multi-layer, or a lone non-OOB-solution layer) that are
  a migrated sub-type (Topic V2) owned by an ESS HR/IT agent. Adds
  OOB_ESS_SOLUTIONS, BOT_COMPONENT_TYPE_LABELS, ALLOWED_BOT_COMPONENT_TYPES,
  ESS_AGENT_SCHEMANAMES.
- hydrate kept components into CustomizationComponent (component_id, schemaname,
  name, component_type[_label], data + raw layers); context.component_layers /
  customizations keyed by id; add context.customized_dependencies.
- temp: dump customizations to .local for navigation (guarded, remove later).
- docs: CUSTOMIZATION_DISCOVERY.md §3-4, SERVICES.md, TASK-006, CHANGELOG.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…guard) + rule task plan

Foundational transformation primitive + the plan for the incoming migration
rules, so a worker can pick RULE-002/003/004 up.

- WritebackPlan / WritebackTarget: steps stage edits per record instead of
  appending to a flat list. Keyed by (entity_set, record_id) -> coalescing (one
  PATCH/record), chaining (target.get returns the working value), and a
  meaningful-change guard (pending_writes diffs working vs original, so an
  unchanged value writes nothing -> no needless Active overlay).
- track both copies per record: original (actual) vs working (final after all tx
  steps); target_for()/targets() read-only accessors expose them for reporting.
- MigrationContext.pending_writes is now a derived property over context.writeback
  (Output/TASK-007 contract unchanged).
- refactor ApplyDaCompatibilityStep to stage via the plan.
- rule authoring rule: transforms edit surgically and must not reserialize
  untouched regions (keeps the exact-string diff honest).
- TASK-017 (writeback plan) + reshaped TASK-011/012/013 to consume
  context.customizations topics and stage via the plan (with the concrete pattern
  + golden-test expectations); touched TASK-007/016; PIPELINES writeback-plan
  contract; CUSTOMIZATION_DISCOVERY §6-7 (+ future overlay-removal note); CHANGELOG.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…teback + validate + report)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…Dialogs (TASK-011)

Implement RULE-002 on top of the TASK-007 output pipeline, exercising the full
discover -> transform -> writeback slice on the discovered topics.

- ReplaceEndConversationStep (src/modules/transformation/steps/), registered in
  build_transformation_pipeline after ApplyDaCompatibilityStep.
- iterates context.customizations and rewrites every `kind: EndConversation`
  node to `kind: CancelAllDialogs` (End All Topics) in each topic's `data` YAML.
  Node-anchored line substitution preserves the list-item prefix, indentation,
  node ids, and all other logic (no YAML round-trip); a topic with no such node
  produces no write. Stages on the WritebackPlan (chaining-aware via target_for),
  supported_modes=("READONLY","WRITEBACK").
- tests: unit (pure transform + step wiring + chaining) + golden
  (tests/golden/test_replace_end_conversation_golden.py). Gates green (103 tests).
- TASK-011 -> DONE (task file + index) + CHANGELOG.
- enriched TASK-012/013 with bring-up findings: topic title/enabled state are the
  botcomponent `name` + `statecode`/`statuscode` record fields (NOT the `data`
  YAML); trigger type is beginDialog.kind; Inactive statecode/statuscode values
  are UNCONFIRMED and must be confirmed live under TASK-009.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…sponse topics (TASK-012/013)

Disable-but-preserve unsupported-trigger topics, per the migration philosophy.

- HandleOnActivityTopicStep (RULE-003) + HandleGeneratedResponseTopicStep
  (RULE-004): thin subclasses of a shared DeprecateTriggerTopicStep base
  (src/modules/transformation/steps/), registered after ReplaceEndConversationStep.
- detect the unsupported trigger from the topic `data` YAML (beginDialog.kind,
  lightweight regex — no YAML round-trip; `data` is never rewritten), and for
  matches stage record-field edits on the WritebackPlan: `name` prefixed once with
  [DEPRECATED], statecode/statuscode -> Inactive (1/2), plus a manual-review
  warning via LogWarning (rendered in the report Warnings section).
- idempotent (MIG-005): a topic already Inactive AND [DEPRECATED]-prefixed is skipped.
- CustomizationComponent now hydrates statecode/statuscode from the componentjson
  attributes already fetched during discovery (no extra Dataverse call).
- tests: unit (trigger detection + both steps + idempotency) + golden. Gates green
  (110 tests). TASK-012/013 -> DONE (task files + index) + CHANGELOG.

Note: Inactive statecode=1/statuscode=2 values confirmed; whether a plain PATCH
persists botcomponent state (vs a dedicated state-change) is confirmed live under
TASK-009. The sibling topic .xml (msdyn_employeeselfservicetemplateconfigs, a
managed Workday scenario template config) is a separate entity, excluded from
discovery, and out of scope for CA->DA dialog migration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…tics from Dataverse docs

The Dataverse botcomponent table reference confirms statecode (0=Active,
1=Inactive) and statuscode are writable columns set via a normal
PATCH /botcomponents(id) — so title (name) + state (statecode/statuscode) +
dialog (data) are record columns, not an XML blob. Replaced the "UNCONFIRMED
statecode mechanism" notes in deprecate_trigger_topic_step.py, TASK-012/013, and
the CHANGELOG with the confirmed semantics.

The only remaining live item (TASK-009) is whether a single PATCH may combine the
State change with content columns (data) when a topic is rewritten by another
rule; if not, the Writeback step will split state into its own PATCH.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… + sanitize dev-specs

- TASK-010 (RULE-001 Override Agent Instructions) -> BLOCKED in the task file and
  TASKS.md index: implementation is paused pending ESS PM input on the canonical
  Declarative Agent instructions payload to apply. Added the blocker note to
  RULE-001 in MIGRATION_RULES.md.
- align specs with the shipped transformation stage:
  - PIPELINES.md pipeline example comments out the blocked RULE-001 step and tags
    RULE-002/003/004 with their tasks.
  - replaced the stale "append to pending_writes" wording with the writeback-plan
    staging model in TASK-010 and TASK-016.
  - REPOSITORY_STRUCTURE.md lists the new customization_component.py /
    writeback_plan.py models.
- CHANGELOG updated.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…e live checks

TASK-009 (End-to-End Framework Validation) dependencies (TASK-003/006/007/016)
are all DONE, so it moves BLOCKED -> TODO. Added the shipped rule tasks
(TASK-011/012/013/017) to its Consumes, and enriched its live-validation criteria
to confirm the topic-rule writeback targets: RULE-002 (data) and RULE-003/004
(name + statecode=1/statuscode=2) on botcomponents — including whether a single
PATCH may combine the state change with content columns (else the Writeback step
splits state into its own PATCH). Updated the TASKS index + CHANGELOG.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…des) + per-topic report (TASK-018/019/020)

Handle the remaining CA->DA unsupported constructs from the component-support
analysis with a uniform disable-but-preserve mitigation, and add a per-topic view
to the migration report.

- RULE-006 DisableUnsupportedTriggerTopicsStep: disable + [DEPRECATED] topics whose
  trigger is OnUnknownIntent/OnPlanComplete/OnSystemRedirect/OnSelectIntent/OnEscalate.
  Generalized the DeprecateTriggerTopicStep base to a {trigger: guidance} mapping.
- RULE-007 DisableUnsupportedNodeTopicsStep: disable + deprecate any topic whose data
  uses an unsupported node kind (service.constants.UNSUPPORTED_TOPIC_NODES:
  AnswerQuestionWithAI, RecognizeIntent, SearchAndSummarizeContent, TransferConversationV2,
  ConversationHistory, InvokeAIBuilderModelAction, IncludeSelectedTopics), naming the
  found node(s) in the report. data is never rewritten.
- shared deprecate_topic action reused by RULE-003/004/006/007 (disable state +
  [DEPRECATED] name + preserve logic + warn + record); multi-reason disables coalesce
  to one PATCH while recording each reason.
- per-topic report (TASK-020): record_topic_change helper + wired RULE-002/003/004/006/007;
  Reporter "Per-Topic Migration Summary" grouping changes by topic (READONLY + WRITEBACK).
- specs: MIGRATION_RULES RULE-006/007; TASK-018/019/020 + index; PIPELINES pipeline;
  CHANGELOG. Node kind tokens analysis-sourced; confirm live under TASK-009.
- gates green (114 tests).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…LE-006/007) over shared bases

Restructure the two batch "disable unsupported construct" steps into a
base-class + one-thin-step-per-construct design so each unsupported CA
trigger/node carries its own tailored, user-facing mitigation message
into the per-topic migration report.

- Add `unsupported_construct_base.py`: `UnsupportedTopicTriggerStep` and
  `UnsupportedNodeStep` abstract bases + shared `deprecate_topic` action
  and `topic_trigger_kind` helper.
- RULE-006: 5 thin trigger steps (OnUnknownIntent, OnPlanComplete,
  OnSystemRedirect, OnSelectIntent, OnEscalate) replacing the batch
  `DisableUnsupportedTriggerTopicsStep`.
- RULE-007: 7 thin node steps (AnswerQuestionWithAI, RecognizeIntent,
  SearchAndSummarizeContent, TransferConversationV2, ConversationHistory,
  InvokeAIBuilderModelAction, IncludeSelectedTopics) replacing the batch
  `DisableUnsupportedNodeTopicsStep`; node `kind:` token now lives on each
  step (drop dead `UNSUPPORTED_TOPIC_NODES` constant).
- Rework RULE-003/004 (`HandleOnActivityTopicStep`,
  `HandleGeneratedResponseTopicStep`) as subclasses of the same base.
- Register all steps in `build_transformation_pipeline`; consolidate tests.
- Sanitize dev-specs (MIGRATION_RULES, PIPELINES, CHANGELOG, TASK-012/013/
  018/019) to the individual-step naming and relax the "one rule → one
  step" statement (RULE-006/007 fan out to one step per construct).

Gates green: ruff, format, mypy, 112 tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ents + customizations + mitigations + next steps)

Turn migration_report.md into a well-structured, eye-catching customer report
rendered from MigrationContext, surfacing the run context and every mitigation
so the customer can review and validate before relying on the changes.

- Reporter (typed to MigrationContext) now renders, with a status banner and
  emoji section headers:
  - 📋 Summary — run metrics table (mode, selected agent, customizations,
    components changed, warnings/errors with status glyphs).
  - 🌐 Environment — URL / tenant / signed-in user / target solution table.
  - 🤖 Agents — discovered ESS agents table with the selected one marked.
  - 🧩 Customizations Extracted — count + type/name/schema/state table.
  - 🔧 Migration Mitigations by Component — Component Type | Component |
    Mitigations Applied (Rules), one row per topic with every rule's mitigation
    merged via <br> (styled after the CA→DA component-support analysis).
  - ⚠️ Warnings / ❌ Errors — numbered # | Component | Reason/Error |
    Recommendation tables (graceful empty states).
  - ✅ Next Steps — Action Required — checkbox checklist leading with "run your
    own end-to-end evaluations" before promoting through ALM.
  - Closing "Thanks for using the ESS Migration Toolkit (MTK)."
- Add `ChangeEntry.component_type`; thread a component-type label
  (botcomponent / Topic (V2)) through LogChange + record_topic_change.
- Retain discovered agents on MigrationContext.discovered_agents
  (AgentSelectionStep) for the report.
- Tests: full section/table coverage + selected-agent marker + empty-state
  degradation. Dev-specs: TASK-020 + CHANGELOG refreshed.

Gates green: ruff, format, mypy, 113 tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…wer-safe); retain 10 session bundles

Follow-up to the polished migration report. Created as a new commit rather than
an amend because the prior commit (217c3d0) is already on the remote — amending
would rewrite published history and force a force-push.

- Next Steps used GFM task-list `- [ ]` syntax, which some Markdown viewers
  double-render (plain + formatted), making the list look duplicated. Switched to
  a plain numbered list that renders consistently everywhere. No content change.
- Bump `_DEFAULT_MAX_SESSIONS` 3 → 10 to retain more recent session bundles.
- Specs: TASK-020 + CHANGELOG wording "checkbox" → "numbered checklist".

Gates green: ruff, format, mypy, 113 tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…solution (ALM)

For ALM customers who declare a preferred solution (an unmanaged solution they
imported and marked preferred), narrow discovery to the components that belong to
that solution, so transformations and writeback only touch that solution's own
customizations (topics layered on the managed ESS base). Run the tool once per
preferred solution.

- RetrieveCustomizationsStep._narrow_to_preferred_solution: when
  context.preferred_solution is set, keep only customizations whose id is a member
  of that solution; non-ALM path (no preferred solution) keeps everything and
  issues no extra query.
- Membership comes from the solution's solutioncomponents (_solutionid_value eq
  <solutionid> -> objectid), NOT the component layers — every unmanaged solution
  shares the single "Active" layer, so msdyn_solutionname can't distinguish them.
- Confirmed live: a Copilot topic is its own solutioncomponents row with
  objectid == botcomponentid, componenttype 10213 (botcomponent), so matching
  customization ids against objectid holds.
- customized_dependencies derived after narrowing so it stays consistent; ids
  normalized case/brace-insensitively (_norm_guid).
- Tests: solutioncomponents fake branch + scoped/no-preferred/empty cases.
- Dev-specs: CUSTOMIZATION_DISCOVERY.md §4.4 + §7, CHANGELOG, TASK-009 live check.

Gates green: ruff, format, mypy, 116 tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…hot in the session bundle

Promote the filtered-customizations dump from a temp .local/ debug aid to a
first-class session-bundle artifact: RetrieveCustomizationsStep now writes
output/session-<timestamp>/customizations.json — a focused, per-topic snapshot of
each in-scope customization's ORIGINAL definition (component_id, schemaname, name,
component_type[+label], statecode/statuscode, and data = the original topic YAML),
excluding the verbose raw layers.

- Written early (Input stage, before any transformation/writeback) so it survives a
  later partial failure and captures the true pre-migration state (the frozen
  CustomizationComponent is never mutated; transforms stage edits on WritebackPlan).
- Internal engineering/recovery aid — original topic YAML can be read back to revert
  manually in a worst-case incident; NOT a customer-facing "undo" (the tool remains
  positioned as irreversible + idempotent).
- Best-effort: never fails the run; no-ops when no session bundle is available
  (unit tests with a fake logger). Dropped the .local path, the pytest skip, and the
  os/asdict imports.
- Tests: +1 (real Logger → snapshot lands in the bundle with original data, no layers).
- Docs: INVARIANTS DIAG-004 (bundle = report + log + snapshot), CUSTOMIZATION_DISCOVERY
  §4.5, TASK-005/007/009, README, CHANGELOG.

Gates green: ruff, format, mypy, 117 tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@anil-adepu
anil-adepu force-pushed the users/aniladepu/features/mtk/customization-discovery-and-core-generalization branch from eeecf2e to ac8c7a4 Compare July 24, 2026 18:32
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