From 56b80e6dcfbba0c20350435d77c9f746432db299 Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Fri, 4 Sep 2026 12:53:54 +0200 Subject: [PATCH 1/4] Add knowledge-backed AL development Add read-only planning and repository-changing development skills so BCQuality knowledge can guide features, bug fixes, refactors, upgrades, and maintenance before the existing AL review gate runs. Track Microsoft Learn ingestion and add development and BCApps-shaped guidance evaluation fixtures. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 638b66d2-9f06-4f60-8781-808709e1485c --- .claude-plugin/marketplace.json | 4 +- .github/scripts/validate_frontmatter.py | 80 +- .github/workflows/development-coverage.yml | 26 + README.md | 65 +- agent-consumption.md | 59 +- coverage/README.md | 44 + coverage/development-capabilities.json | 129 + coverage/learn-coverage.json | 456 + .../microsoft-learn-developer-catalog.json | 8282 +++++++++++++++++ evaluation/README.md | 47 + evaluation/development-fixtures.json | 188 + evaluation/development-guidance-fixtures.json | 50 + ...ize-document-defaults-in-initrecord.bad.al | 28 + ...ze-document-defaults-in-initrecord.good.al | 45 + ...tialize-document-defaults-in-initrecord.md | 30 + ...und-direction-symbols-use-magnitude.bad.al | 8 + ...nd-direction-symbols-use-magnitude.good.al | 10 + .../round-direction-symbols-use-magnitude.md | 30 + .../guard-interface-casts-with-is.bad.al | 20 + .../guard-interface-casts-with-is.good.al | 24 + .../guard-interface-casts-with-is.md | 30 + ...ationarea-required-on-page-controls.bad.al | 18 +- ...tionarea-required-on-page-controls.good.al | 20 +- ...plicationarea-required-on-page-controls.md | 16 +- ...oup-respects-lookup-page-visibility.bad.al | 9 + ...up-respects-lookup-page-visibility.good.al | 20 + ...ldgroup-respects-lookup-page-visibility.md | 30 + ...ropagation-both-refreshes-main-page.bad.al | 26 + ...opagation-both-refreshes-main-page.good.al | 27 + ...atepropagation-both-refreshes-main-page.md | 30 + ...on-meaning-depends-on-execution-context.md | 26 + ...and-upgrade-codeunits-have-no-order.bad.al | 28 + ...nd-upgrade-codeunits-have-no-order.good.al | 18 + ...all-and-upgrade-codeunits-have-no-order.md | 30 + .../skills/development/al-development-plan.md | 68 + .../skills/development/al-development.md | 91 + .../skills/review/al-data-modeling-review.md | 4 +- .../skills/review/al-interfaces-review.md | 3 +- microsoft/skills/review/al-style-review.md | 3 +- microsoft/skills/review/al-ui-review.md | 7 +- microsoft/skills/review/al-upgrade-review.md | 4 +- plugin.json | 5 +- skills/README.md | 32 +- skills/al-development/SKILL.md | 29 + skills/do.md | 148 +- skills/entry.md | 7 +- tools/Test-DevelopmentFixtures.ps1 | 516 + tools/Test-DevelopmentGuidanceFixtures.ps1 | 298 + tools/Test-LearnCoverage.ps1 | 240 + tools/Update-LearnCatalog.ps1 | 262 + 50 files changed, 11590 insertions(+), 80 deletions(-) create mode 100644 .github/workflows/development-coverage.yml create mode 100644 coverage/README.md create mode 100644 coverage/development-capabilities.json create mode 100644 coverage/learn-coverage.json create mode 100644 coverage/microsoft-learn-developer-catalog.json create mode 100644 evaluation/development-fixtures.json create mode 100644 evaluation/development-guidance-fixtures.json create mode 100644 microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.bad.al create mode 100644 microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.good.al create mode 100644 microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md create mode 100644 microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.bad.al create mode 100644 microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.good.al create mode 100644 microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md create mode 100644 microsoft/knowledge/interfaces/guard-interface-casts-with-is.bad.al create mode 100644 microsoft/knowledge/interfaces/guard-interface-casts-with-is.good.al create mode 100644 microsoft/knowledge/interfaces/guard-interface-casts-with-is.md create mode 100644 microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.bad.al create mode 100644 microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.good.al create mode 100644 microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md create mode 100644 microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.bad.al create mode 100644 microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.good.al create mode 100644 microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md create mode 100644 microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md create mode 100644 microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.bad.al create mode 100644 microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.good.al create mode 100644 microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md create mode 100644 microsoft/skills/development/al-development-plan.md create mode 100644 microsoft/skills/development/al-development.md create mode 100644 skills/al-development/SKILL.md create mode 100644 tools/Test-DevelopmentFixtures.ps1 create mode 100644 tools/Test-DevelopmentGuidanceFixtures.ps1 create mode 100644 tools/Test-LearnCoverage.ps1 create mode 100644 tools/Update-LearnCatalog.ps1 diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 752ae8d2..5926b79f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,8 +8,8 @@ { "name": "bcquality", "source": "./", - "description": "Business Central AL quality knowledge base and review skills, packaged as an installable plugin. Exposes an AL review adapter while preserving BCQuality's internal Entry and action-skill protocols.", - "version": "0.2.0", + "description": "Business Central AL quality knowledge base and skills, packaged as an installable plugin. Exposes development and review adapters while preserving BCQuality's internal Entry and action-skill protocols.", + "version": "0.3.0", "skills": [ "./skills/" ] diff --git a/.github/scripts/validate_frontmatter.py b/.github/scripts/validate_frontmatter.py index b0076cce..ada7505c 100644 --- a/.github/scripts/validate_frontmatter.py +++ b/.github/scripts/validate_frontmatter.py @@ -39,6 +39,7 @@ } ACTION_SKILL_OPTIONAL_KEYS = { "bc-version", "technologies", "countries", "application-area", "sub-skills", + "quality-skill", "guidance-skill", } META_SKILL_REQUIRED_KEYS = {"kind", "id", "version", "title"} ENTRY_SKILL_REQUIRED_KEYS = {"kind", "id", "version", "title"} @@ -46,8 +47,11 @@ STANDARD_INPUTS = { "pr-diff", "object-list", "file-path", "repository", "telemetry-query", + "development-request", "development-plan", +} +ALLOWED_OUTPUTS = { + "findings-report", "implementation-report", "development-guidance-report", } -ALLOWED_OUTPUTS = {"findings-report"} VALID_SAMPLE_KINDS = {"good", "bad"} ACTION_SKILL_SECTIONS = ["Source", "Relevance", "Worklist", "Action", "Output"] @@ -340,9 +344,11 @@ def validate_action_skill(path: Path, parsed: Parsed, report: Report) -> None: if not is_non_empty_list_of_str(out): report.error(path, "R18", "outputs must be a non-empty list of strings", 1) else: + if len(out) != 1: + report.error(path, "R18", "outputs must contain exactly one output kind", 1) bad = [x for x in out if x not in ALLOWED_OUTPUTS] if bad: - report.error(path, "R18", f"outputs contains non-allowed values {bad}; currently only {sorted(ALLOWED_OUTPUTS)} is defined", 1) + report.error(path, "R18", f"outputs contains non-allowed values {bad}; allowed values are {sorted(ALLOWED_OUTPUTS)}", 1) # R19 optional filter dimensions, if present if "bc-version" in fm: @@ -382,6 +388,20 @@ def validate_action_skill(path: Path, parsed: Parsed, report: Report) -> None: if bad: report.error(path, "R20", f"sub-skills entries must end in '.md': {bad}", 1) + if "quality-skill" in fm: + quality_skill = fm["quality-skill"] + if not isinstance(quality_skill, str) or not quality_skill.endswith(".md"): + report.error(path, "R31", "quality-skill must be one repo-relative .md path", 1) + if fm.get("outputs") != ["implementation-report"]: + report.error(path, "R31", "quality-skill is valid only with outputs: [implementation-report]", 1) + + if "guidance-skill" in fm: + guidance_skill = fm["guidance-skill"] + if not isinstance(guidance_skill, str) or not guidance_skill.endswith(".md"): + report.error(path, "R32", "guidance-skill must be one repo-relative .md path", 1) + if fm.get("outputs") != ["implementation-report"]: + report.error(path, "R32", "guidance-skill is valid only with outputs: [implementation-report]", 1) + # R21 five required sections, in order, each exactly once heads = [h for h, _ in headings_in_order(parsed.body)] indices: list[int] = [] @@ -603,6 +623,58 @@ def validate_sub_skills_registry(path: Path, fm: dict[str, Any], root: Path, rep report.error(path, "R26", f"leaf not registered in sub-skills: {leaf}", 1) +def validate_quality_skill(path: Path, fm: dict[str, Any], root: Path, report: Report) -> None: + """R30: implementation quality-skill paths resolve to a findings producer.""" + quality_skill = fm.get("quality-skill") + if not isinstance(quality_skill, str) or not quality_skill.endswith(".md"): + return + + normalized = quality_skill.lstrip("./") + target = root / normalized + if not target.is_file(): + report.error(path, "R30", f"quality-skill does not exist on disk: {normalized}", 1) + return + if target.resolve() == path.resolve(): + report.error(path, "R30", "quality-skill must not reference the implementation skill itself", 1) + return + + try: + target_parsed = parse_markdown(target.read_text(encoding="utf-8")) + except UnicodeDecodeError as e: + report.error(path, "R30", f"quality-skill is not valid UTF-8: {e}", 1) + return + target_outputs = (target_parsed.frontmatter or {}).get("outputs") + if target_outputs != ["findings-report"]: + report.error(path, "R30", f"quality-skill must emit findings-report: {normalized}", 1) + + +def validate_guidance_skill(path: Path, fm: dict[str, Any], root: Path, report: Report) -> None: + """R33: implementation guidance-skill paths resolve to a read-only planner.""" + guidance_skill = fm.get("guidance-skill") + if not isinstance(guidance_skill, str) or not guidance_skill.endswith(".md"): + return + + normalized = guidance_skill.lstrip("./") + target = root / normalized + if not target.is_file(): + report.error(path, "R33", f"guidance-skill does not exist on disk: {normalized}", 1) + return + if target.resolve() == path.resolve(): + report.error(path, "R33", "guidance-skill must not reference the implementation skill itself", 1) + return + + try: + target_parsed = parse_markdown(target.read_text(encoding="utf-8")) + except UnicodeDecodeError as e: + report.error(path, "R33", f"guidance-skill is not valid UTF-8: {e}", 1) + return + target_fm = target_parsed.frontmatter or {} + if target_fm.get("outputs") != ["development-guidance-report"]: + report.error(path, "R33", f"guidance-skill must emit development-guidance-report: {normalized}", 1) + if "development-plan" not in (target_fm.get("inputs") or []): + report.error(path, "R33", f"guidance-skill must accept development-plan: {normalized}", 1) + + def run(root: Path) -> Report: report = Report() skill_records: list[SkillRecord] = [] @@ -670,9 +742,11 @@ def run(root: Path) -> Report: others = [q.relative_to(root).as_posix() for q in paths if q != p] report.error(p, "R24", f"skill id '{sid}' ({kind}) is not unique; also defined in: {others}") - # Fourth pass: R26 sub-skills registry matches leaf files on disk + # Fourth pass: cross-skill references for path, fm in action_skill_fms: validate_sub_skills_registry(path, fm, root, report) + validate_quality_skill(path, fm, root, report) + validate_guidance_skill(path, fm, root, report) return report diff --git a/.github/workflows/development-coverage.yml b/.github/workflows/development-coverage.yml new file mode 100644 index 00000000..73383a16 --- /dev/null +++ b/.github/workflows/development-coverage.yml @@ -0,0 +1,26 @@ +name: Validate development coverage + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + validate-development-coverage: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Validate Microsoft Learn coverage ledger + shell: pwsh + run: ./tools/Test-LearnCoverage.ps1 -Root . + + - name: Validate and prepare development fixtures + shell: pwsh + run: ./tools/Test-DevelopmentFixtures.ps1 -Root . -PrepareDirectory "$env:RUNNER_TEMP/bcquality-development-fixtures" + + - name: Validate and prepare development-guidance fixtures + shell: pwsh + run: ./tools/Test-DevelopmentGuidanceFixtures.ps1 -Root . -PrepareDirectory "$env:RUNNER_TEMP/bcquality-development-guidance-fixtures" diff --git a/README.md b/README.md index 43032d42..8a97d214 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Skills define how agents consume knowledge. They come in three flavors: READ and DO are read on demand — typically when the first dispatched action skill runs. They are not prerequisites for invoking Entry. WRITE is only used when scaffolding new content. -- **Action skills** — concrete skills that follow the Action Skill template to do real work (review code, audit telemetry, etc.). Action skills live inside the layers that own them (`/microsoft/skills/`, `/community/skills/`, `/custom/skills/`). An action skill is either a **leaf** that evaluates knowledge files directly, or a **super-skill** that composes other action skills (declared via `sub-skills` in frontmatter). The canonical reference is [`microsoft/skills/review/al-code-review.md`](microsoft/skills/review/al-code-review.md) (super-skill), which composes the AL review leaf skills under [`microsoft/skills/review/`](microsoft/skills/review/) — one per knowledge domain. +- **Action skills** — concrete skills that follow the Action Skill template to do real work. Review skills emit findings reports; read-only planning skills emit development-guidance reports; implementation skills emit implementation reports. Action skills live inside the layers that own them (`/microsoft/skills/`, `/community/skills/`, `/custom/skills/`). [`microsoft/skills/development/al-development-plan.md`](microsoft/skills/development/al-development-plan.md) turns an existing plan into knowledge constraints, [`microsoft/skills/development/al-development.md`](microsoft/skills/development/al-development.md) consumes those constraints while implementing features, bugs, refactors, upgrades, and maintenance, and [`microsoft/skills/review/al-code-review.md`](microsoft/skills/review/al-code-review.md) provides the final quality gate. ### Agent bootstrapping @@ -64,10 +64,10 @@ An orchestrator (such as AL-Go) points the agent at BCQuality's URL and provides ### Standalone plugin installation -BCQuality can also be installed directly as a plugin. The plugin registers one -host-native skill, -[`al-code-review`](skills/al-code-review/SKILL.md), which adapts the caller's -request to the same Entry protocol used by orchestrators. +BCQuality can also be installed directly as a plugin. The plugin registers +host-native adapters for [`al-code-review`](skills/al-code-review/SKILL.md) and +[`al-development`](skills/al-development/SKILL.md). Both adapt +the caller's request to the same Entry protocol used by orchestrators. For GitHub Copilot CLI: @@ -81,32 +81,37 @@ must be updated. The name remains distinct from BC-ALAgents' public `al-review` skill because current hosts may load plugin skill names into one shared inventory. -The adapter is intentionally not a second review implementation: +Plugin version `0.3.0` adds `al-development`, the knowledge-backed +implementation adapter for features, bugs, refactors, upgrades, and maintenance. + +The adapters are intentionally not second implementations: ```text standalone host skill: skills/al-code-review/SKILL.md -> routing contract: skills/entry.md -> review coordinator: microsoft/skills/review/al-code-review.md -> domain review leaves + +standalone host skill: skills/al-development/SKILL.md + -> routing contract: skills/entry.md + -> implementation skill: microsoft/skills/development/al-development.md + -> knowledge-guided implementation + AL review quality gate ``` -Only the first file follows the host's `SKILL.md` packaging format. The -remaining files are BCQuality's internal protocol and layered action skills. -Entry remains the single owner of routing and index preparation; -`al-code-review.md` remains the single owner of broad-review composition. This -separation keeps standalone installation available without duplicating those -policies in the plugin adapter. +Only the files under `skills/*/SKILL.md` follow the host's packaging format. +The remaining files are BCQuality's internal protocol and layered action +skills. Entry remains the single owner of routing and index preparation. This +separation keeps standalone installation available without duplicating policy +in either adapter. Note that a plugin install ships the entire tree, so `BCQUALITY_ENABLED_LAYERS` narrows discovery without removing any files. Layer selection is a filter here, not a deny mechanism — see [the adapter](skills/al-code-review/SKILL.md) for the difference from the pruned-clone model. -The host adapter and internal action skill intentionally share the -`al-code-review` name: they expose the same operation in two different skill -formats. Their paths make the boundary explicit. The adapter lives under -`skills/al-code-review/SKILL.md`; the internal Microsoft-layer coordinator -lives at `microsoft/skills/review/al-code-review.md`. +Each host adapter and internal action skill intentionally share a name: they +expose the same operation in two different skill formats. Their paths make the +boundary explicit. ## Knowledge file format @@ -138,10 +143,29 @@ Code examples belong in separate files, not in the knowledge file itself. Knowle ## Scope -The current curated corpus is focused on **technical AL code review**: Agents, AppSource and compatibility, data modeling, error handling, events, interfaces, performance, privacy, Query objects, security, style, telemetry, testing, UI, upgrade, and web services. These are the domains backed by knowledge files and registered review leaves today. +The current curated corpus covers technical AL concerns across Agents, AppSource and compatibility, data modeling, error handling, events, interfaces, performance, privacy, Query objects, security, style, telemetry, testing, UI, upgrade, and web services. Review skills evaluate existing changes against those domains. The `al-development` skill applies them before and during implementation, then runs the review coordinator as a final gate. + +Repository-specific orchestrators do not need to delegate implementation to +`al-development`. They can invoke `al-development-plan` with their existing +plan, feed its read-only guidance report into their own phases, and retain their +specialized environment, test, propagation, and delivery gates. Business Central functional domains (Finance, Supply Chain Management, Manufacturing, Jobs, Warehousing, Service), PowerShell, pipelines, and Power Platform remain valid future repository scope, but they are **not current coverage claims** until corresponding knowledge and action skills exist. Consumers should derive supported review scope from the live knowledge index and dispatched skills, not from roadmap breadth. +## Tracking developer coverage + +BCQuality tracks source ingestion and implementation capability separately: + +- [`coverage/microsoft-learn-developer-catalog.json`](coverage/microsoft-learn-developer-catalog.json) is the generated inventory of Business Central developer training. +- [`coverage/learn-coverage.json`](coverage/learn-coverage.json) records editorial progress and the disposition of each extracted concern. +- [`coverage/development-capabilities.json`](coverage/development-capabilities.json) tracks representative development capabilities and their evaluation fixtures. + +Run `pwsh ./tools/Test-LearnCoverage.ps1` for current source progress and +`pwsh ./tools/Test-DevelopmentFixtures.ps1` for capability coverage. Article +count alone is not a completion metric: a capability becomes `validated` only +after its generated implementation passes compilation, tests, and the review +quality gate. + ## How agents consume BCQuality Action skills follow a four-step pattern: @@ -151,7 +175,7 @@ Action skills follow a four-step pattern: 3. **Worklist** — narrow from N candidates to the M that apply to the current task 4. **Action** — apply the relevant knowledge and produce structured output -Every action skill produces output in a common format that orchestrators can consume without skill-specific parsing. The format is JSON and includes an `outcome` (so a clean run, a not-applicable skill, and a partial failure are all distinguishable), `findings` (what the skill observed), structured `references` back to the knowledge files that informed each finding, per-finding `confidence`, and a `suppressed` list recording any knowledge files overridden by layer precedence. This contract is defined in the Action Skill meta-skill so that orchestrators and action skills remain independently evolvable. +Every action skill declares one structured JSON output. Review skills emit a `findings-report`; planning skills emit a read-only `development-guidance-report`; implementation skills emit an `implementation-report` containing the plan, knowledge used, changed files, real validation results, and post-implementation review. All contracts are defined in the Action Skill meta-skill so orchestrators and action skills remain independently evolvable. BCQuality is an **additive** knowledge layer: it augments the agent's review judgement, it does not replace it. Super-skills (such as `al-code-review`) run a self-review pass alongside their sub-skills and surface concerns the agent identified on its own, marked with `from-sub-skill: "agent"` and an empty `references: []` so consumers can render them distinctly from knowledge-backed findings. See [agent-consumption.md](agent-consumption.md) and [`skills/do.md`](skills/do.md) for the full contract. @@ -163,7 +187,8 @@ For the end-to-end flow — from orchestrator trigger through to how output reac ``` ├── /skills/ # Global: entry-point skill + meta-skill contracts (READ, DO, WRITE) -├── /evaluation/ # Neutral good/bad review fixtures and scoring contract +├── /coverage/ # Source-ingestion ledger and development capability matrix +├── /evaluation/ # Review and development evaluation fixtures ├── /.github/ # Actions and workflows ├── /microsoft/ # Microsoft-endorsed layer │ ├── /knowledge/ # Knowledge files by domain diff --git a/agent-consumption.md b/agent-consumption.md index 37a7a100..d703aa24 100644 --- a/agent-consumption.md +++ b/agent-consumption.md @@ -13,8 +13,10 @@ For the high-level framing and repo structure, start with the [README](README.md - **Layer content** in `/microsoft/`, `/community/`, and `/custom/` — knowledge files and action skills grouped by authority. When BCQuality is installed as a standalone plugin, it additionally exposes -`skills/al-code-review/SKILL.md`. This is a host-format adapter, not another -action skill: it creates the task context and enters the same flow at Entry. +`skills/al-code-review/SKILL.md` and +`skills/al-development/SKILL.md`. These are host-format adapters, not +additional action skills: each creates the task context and enters the same +flow at Entry. ## The flow @@ -25,7 +27,7 @@ flowchart LR E -->|3 dispatch record| A A -->|4 invoke dispatched skill| S[Action skill
e.g. al-code-review] S -->|5 execute| P[Source → Relevance
→ Worklist → Action
reading READ · DO on demand] - P -->|6 emit| R[Findings · Domain labels
· References · Confidence] + P -->|6 emit| R[Findings report
or implementation report] R -->|7 integrate| O ``` @@ -35,11 +37,11 @@ The orchestrator has a URL setting that points at BCQuality (default: `github.co ### 2. Agent invokes Entry The agent reads `/skills/entry.md` and runs it against the task context. Entry applies its Source → Relevance → Worklist → Action steps over the action skills under `*/skills/**/*.md` and returns a **dispatch record**: the set of action skills to invoke, plus a list of candidates it skipped (with reasons). Routing is a skill, not orchestrator logic. -For a standalone plugin installation, the host activates the -`skills/al-code-review/SKILL.md` adapter first. That adapter preserves the -caller's actual goal, constructs the task context, and invokes Entry. It does -not select the internal `microsoft/skills/review/al-code-review.md` action skill -itself or duplicate Entry's preparation, routing, and failure semantics. +For a standalone plugin installation, the host activates the matching adapter +first. The adapter preserves the caller's actual goal, constructs the task +context, and invokes Entry. It does not select the internal review or +development action skill itself or duplicate Entry's preparation, routing, and +failure semantics. ### 3. Agent consumes the dispatch record The dispatch record names one or more action skills and the subset of inputs each should receive. If the outcome is `no-match` or `failed`, the agent returns the record to the orchestrator unchanged. @@ -71,19 +73,44 @@ The index is **owned and produced by BCQuality**, not by each consumer: its gene The index changes only *how candidates are discovered*, never *which are selected*. The Worklist predicate is unchanged — `keywords` still drive selection — and the agent still opens each worklisted article **in full** to read its `## Best Practice` / `## Anti Pattern` rule bodies; the index is discovery metadata only and never substitutes for the article body. When no index is present, skills fall back to path-based discovery (collect by domain folder), so review still works. ### 6. Agent emits structured output -The output contract is defined in the DO meta-skill so that every action skill — today's and next year's — produces the same shape: +The output contracts are defined in the DO meta-skill: -- **Outcome** — `completed`, `not-applicable`, `no-knowledge`, `partial`, or `failed`. An orchestrator can distinguish a clean run from a no-op from a failure without guessing. -- **Findings** — what the skill observed (severity, message, optional location). -- **Domain** — the producer-owned, human-readable display label on each review finding. -- **References** — structured objects (`path` plus optional commit `sha`) pointing to the knowledge files that informed each finding. -- **Confidence** — per-finding evidence strength. -- **Suppressed** — knowledge files that were discarded by layer precedence or configuration, so reviewers can see what was overridden. +- A **findings report** carries review findings, domain labels, references, confidence, and suppressions. +- A **development guidance report** carries read-only knowledge constraints and validation considerations for an existing plan. +- An **implementation report** carries the development plan, classified mode, knowledge applied, changed files, validation results, final review, and remaining work. The orchestrator parses this **without skill-specific logic**. This is the point of the contract: orchestrators and action skills evolve independently. +For development, the action happens before the report: the skill +first invokes the read-only planning skill to select applicable knowledge, then +changes the target repository, runs its native validation, and invokes the +configured review quality-skill over the resulting diff. A specialized +repository orchestrator may invoke only the planning skill and feed its +guidance report into its own implementation phases. The implementation report +is a machine-readable record of persisted work, not a code proposal for the +orchestrator to apply later. + ### 7. Orchestrator integrates -The orchestrator turns findings into PR comments, build gates, or IDE diagnostics, and links the references back to the knowledge files so the PR author — human or agent — can read the guidance. +The orchestrator turns findings into PR comments, build gates, or IDE diagnostics. For implementation it presents the changed files and validation state, while the agent has already persisted the requested change in the target repository. + +## Repository-specific development orchestrators + +A repository-specific workflow can keep ownership of implementation and consume +BCQuality only for planning and review: + +1. Produce its normal development plan after repository investigation. +2. Invoke Entry with `inputs-available: [development-plan, repository]` plus + the resolved applicability dimensions. +3. Execute the dispatched `al-development-plan` skill and preserve its + `development-guidance-report`. +4. Pass the selected article references, constraints, samples, and validation + considerations into its own test, implementation, and critique phases. +5. Run its existing BCQuality-backed review gate over the completed diff. + +This is the integration model for specialized bug-fix or release workflows. +They keep environment provisioning, retries, state, commits, propagation, and +pull-request delivery; BCQuality supplies shared product knowledge before and +after the code change. ## Knowledge-backed and agent findings diff --git a/coverage/README.md b/coverage/README.md new file mode 100644 index 00000000..3b2bb18a --- /dev/null +++ b/coverage/README.md @@ -0,0 +1,44 @@ +# Developer knowledge coverage + +This directory separates **source coverage** from the knowledge corpus itself. +Microsoft Learn units are inputs to editorial work, not articles to import +one-for-one. + +## Files + +- `microsoft-learn-developer-catalog.json` is generated source metadata for all + Microsoft Learn modules tagged with both `dynamics-business-central` and + `developer`. +- `learn-coverage.json` is the maintained editorial ledger. Units absent from + this file are unreviewed. +- `development-capabilities.json` tracks whether representative Business + Central development capabilities have implementation fixtures. + +Each tracked unit has a `reviewStatus`: + +- `in-progress` — at least one concern has been identified, but editorial + triage of the unit is not complete. +- `complete` — every relevant concern in the unit has a recorded outcome. A + complete unit may have no outcomes when it contains no remedial knowledge. + +Each concern has one disposition: + +- `candidate` — worth authoring or reconciling with existing knowledge. +- `authored` — produced one or more new knowledge articles. +- `covered-existing` — already represented by the linked article. +- `rejected` — fails BCQuality's remedial admission test. +- `deferred` — valid but intentionally postponed, with a rationale. + +An authored article does not make its source unit complete automatically. One +unit can contain several independent concerns. + +## Update and report + +```powershell +pwsh ./tools/Update-LearnCatalog.ps1 +pwsh ./tools/Test-LearnCoverage.ps1 +``` + +The catalog updater also accepts `-CatalogPath` for an exported Microsoft Learn +Platform API response. CI validates the committed snapshot and editorial ledger +without network access. diff --git a/coverage/development-capabilities.json b/coverage/development-capabilities.json new file mode 100644 index 00000000..493316a0 --- /dev/null +++ b/coverage/development-capabilities.json @@ -0,0 +1,129 @@ +{ + "version": 1, + "capabilities": [ + { + "id": "setup-and-master-data", + "title": "Setup and master data", + "status": "fixture", + "domains": [ + "data-modeling", + "security", + "testing", + "ui" + ], + "fixtureIds": [ + "setup-backed-master-data" + ] + }, + { + "id": "document-workflows", + "title": "Document header and lines workflows", + "status": "fixture", + "domains": [ + "data-modeling", + "events", + "performance", + "testing", + "ui" + ], + "fixtureIds": [ + "document-header-and-lines" + ] + }, + { + "id": "api-integrations", + "title": "Versioned API integrations", + "status": "fixture", + "domains": [ + "security", + "testing", + "web-services" + ], + "fixtureIds": [ + "versioned-master-data-api" + ] + }, + { + "id": "bug-diagnosis-and-fix", + "title": "Bug diagnosis and surgical repair", + "status": "fixture", + "domains": [ + "performance", + "testing" + ], + "fixtureIds": [ + "fix-filtered-batch-processing" + ] + }, + { + "id": "journals-and-posting", + "title": "Journals and posting routines", + "status": "planned", + "domains": [ + "data-modeling", + "error-handling", + "events", + "performance", + "testing" + ], + "fixtureIds": [] + }, + { + "id": "reports-and-documents", + "title": "Reports and document layouts", + "status": "planned", + "domains": [ + "performance", + "testing", + "ui" + ], + "fixtureIds": [] + }, + { + "id": "install-and-upgrade", + "title": "Installation and data upgrade", + "status": "planned", + "domains": [ + "breaking-changes", + "testing", + "upgrade" + ], + "fixtureIds": [] + }, + { + "id": "external-services", + "title": "Outbound services and authentication", + "status": "planned", + "domains": [ + "error-handling", + "privacy", + "security", + "telemetry" + ], + "fixtureIds": [] + }, + { + "id": "role-centers-and-onboarding", + "title": "Role Centers, setup, and onboarding", + "status": "planned", + "domains": [ + "security", + "testing", + "ui" + ], + "fixtureIds": [] + }, + { + "id": "appsource-lifecycle", + "title": "AppSource packaging and lifecycle", + "status": "planned", + "domains": [ + "appsource", + "breaking-changes", + "testing", + "upgrade" + ], + "fixtureIds": [] + } + ] +} diff --git a/coverage/learn-coverage.json b/coverage/learn-coverage.json new file mode 100644 index 00000000..53934d6d --- /dev/null +++ b/coverage/learn-coverage.json @@ -0,0 +1,456 @@ +{ + "version": 1, + "catalog": "coverage/microsoft-learn-developer-catalog.json", + "units": [ + { + "uid": "learn-dynamics.use-document-standards-business-central.4a-use-round-function", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "round-direction-symbols-use-magnitude", + "title": "Round direction symbols use magnitude rather than mathematical ordering", + "disposition": "authored", + "domain": "data-modeling", + "articlePaths": [ + "microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md" + ] + } + ] + }, + { + "uid": "learn-dynamics.use-document-standards-business-central.3-use-initrecord-function", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "initialize-document-defaults-in-initrecord", + "title": "Initialize document defaults in InitRecord after assigning the number", + "disposition": "authored", + "domain": "data-modeling", + "articlePaths": [ + "microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md" + ] + } + ] + }, + { + "uid": "learn-dynamics.business-central-interfaces.type-testing", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "guard-interface-casts-with-is", + "title": "Guard optional interface casts with is", + "disposition": "authored", + "domain": "interfaces", + "articlePaths": [ + "microsoft/knowledge/interfaces/guard-interface-casts-with-is.md" + ] + } + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-table.add-field-group", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "dropdown-fieldgroup-respects-lookup-page-visibility", + "title": "DropDown fields remain hidden when their lookup-page controls are hidden", + "disposition": "authored", + "domain": "ui", + "articlePaths": [ + "microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md" + ] + } + ] + }, + { + "uid": "learn-dynamics.work-with-pages.8-controls", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "updatepropagation-both-refreshes-main-page", + "title": "UpdatePropagation Both refreshes the main page after line edits", + "disposition": "authored", + "domain": "ui", + "articlePaths": [ + "microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md" + ] + }, + { + "id": "applicationarea-parent-inheritance", + "title": "Page-level ApplicationArea inheritance excludes extension controls", + "disposition": "covered-existing", + "domain": "style", + "articlePaths": [ + "microsoft/knowledge/style/applicationarea-required-on-page-controls.md" + ] + } + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.3-installation-upgrade-codeunits", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "install-and-upgrade-codeunits-have-no-order", + "title": "Separate install or upgrade codeunits have no execution order", + "disposition": "authored", + "domain": "upgrade", + "articlePaths": [ + "microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md" + ] + }, + { + "id": "appversion-meaning-depends-on-execution-context", + "title": "ModuleInfo AppVersion changes meaning with execution context", + "disposition": "authored", + "domain": "upgrade", + "articlePaths": [ + "microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md" + ] + } + ] + }, + { + "uid": "learn-dynamics.work-with-tables.text-search", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "optimize-for-text-search-requires-double-ampersand-filter", + "title": "Optimized full-text search requires the double-ampersand filter operator", + "disposition": "candidate", + "domain": "query" + } + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-table.define-extension-objects", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "same-app-extension-references-follow-object-id-order", + "title": "Same-app extension objects can only reference lower-ID extension objects", + "disposition": "candidate", + "domain": "data-modeling" + } + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.resource-policy-settings", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "resource-exposure-policy-is-not-nondebuggable", + "title": "Resource exposure policy and NonDebuggable protect different surfaces", + "disposition": "candidate", + "domain": "security" + } + ] + }, + { + "uid": "learn-dynamics.work-entitlements-permission-sets.3-create-entitlements-permission-sets", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "entitlement-objects-reference-same-app-permission-sets", + "title": "Entitlement objects are online-only and reference same-app permission sets", + "disposition": "candidate", + "domain": "security" + } + ] + }, + { + "uid": "learn-dynamics.work-entitlements-permission-sets.override-entitlements", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "inherent-permissions-cannot-elevate-other-extensions", + "title": "Inherent permissions cannot elevate access to another extension", + "disposition": "candidate", + "domain": "security" + } + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.3a-snapshot", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "snapshot-debugging-captures-only-snappoints-and-exceptions", + "title": "Snapshot debugging captures state only at snappoints and exceptions", + "disposition": "candidate", + "domain": "testing" + } + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.recovery-failures", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "publish-recovery-cannot-restore-every-extension-state", + "title": "Publish recovery cannot restore every upgrade or app-move failure", + "disposition": "candidate", + "domain": "upgrade" + } + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.database-wait-statistics", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "database-wait-statistics-are-exposed-as-a-virtual-table", + "title": "Database wait statistics are exposed as a Business Central virtual table", + "disposition": "candidate", + "domain": "performance" + } + ] + }, + { + "uid": "learn-dynamics.work-with-pages.use-rich-text-editor", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "rich-text-controls-use-html-backed-blob-fields", + "title": "Rich text controls use HTML-backed Blob fields and isolated layout groups", + "disposition": "candidate", + "domain": "ui" + } + ] + }, + { + "uid": "learn-dynamics.work-with-pages.scan-barcodes", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "barcode-integration-scenarios-have-different-platform-support", + "title": "Barcode field, camera action, and hardware-scanner scenarios have different platform support", + "disposition": "candidate", + "domain": "ui" + } + ] + }, + { + "uid": "learn-dynamics.work-with-pages.9-search", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "card-pages-should-not-be-directly-searchable", + "title": "Card pages should be opened through their list rather than Tell Me", + "disposition": "candidate", + "domain": "ui" + } + ] + }, + { + "uid": "learn-dynamics.work-with-pages.hidden-fields", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "allowincustomizations-controls-the-add-field-pane", + "title": "AllowInCustomizations controls exposure through the Add field pane", + "disposition": "candidate", + "domain": "privacy" + } + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.differentiate-apps", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "application-modules-depend-only-on-same-or-lower-layers", + "title": "Application modules depend only on the same or lower architectural layers", + "disposition": "candidate", + "domain": "data-modeling" + } + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code.2-retrieve-data", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "legacy-find-directions-do-not-use-top-one", + "title": "Legacy Find direction calls do not have FindFirst or FindLast query behavior", + "disposition": "candidate", + "domain": "performance" + } + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.2-upgrade-responsibilities", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "uninstall-preserves-extension-data", + "title": "Uninstall preserves extension data for reinstall or upgrade", + "disposition": "candidate", + "domain": "upgrade" + } + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.consider-update-lifecycle", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "incompatible-cloud-extensions-have-a-remediation-window", + "title": "Incompatible cloud extensions have a fixed remediation window", + "disposition": "candidate", + "domain": "upgrade" + } + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.6-answers-about-updates", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "minor-and-major-releases-update-apps-differently", + "title": "Minor and major Business Central releases update AppSource apps differently", + "disposition": "candidate", + "domain": "upgrade" + }, + { + "id": "dependency-version-is-a-minimum", + "title": "An app.json dependency version is a minimum rather than an exact pin", + "disposition": "candidate", + "domain": "upgrade" + } + ] + }, + { + "uid": "learn-dynamics.application-types.3-library-dependency-applications", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "library-apps-install-through-the-dependency-chain", + "title": "Library apps install and update through the dependency chain", + "disposition": "candidate", + "domain": "appsource" + } + ] + }, + { + "uid": "learn-dynamics.business-central-interfaces.extending-interfaces", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "interfaces-can-compose-multiple-base-interfaces", + "title": "Interfaces can compose multiple base interfaces", + "disposition": "candidate", + "domain": "interfaces" + } + ] + }, + { + "uid": "learn-dynamics.test-automation.page-scripting-tool", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "page-scripting-cannot-drive-non-al-ui", + "title": "Page scripting cannot drive control add-ins or other non-AL UI", + "disposition": "candidate", + "domain": "testing" + } + ] + }, + { + "uid": "learn-dynamics.test-automation.2-test-automation-responsibilities", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "appsource-validation-must-use-the-current-build", + "title": "AppSource validation must use the current validation build", + "disposition": "candidate", + "domain": "appsource" + }, + { + "id": "permission-testing-also-protects-the-essential-experience", + "title": "Permission testing must also protect the unextended Essential experience", + "disposition": "candidate", + "domain": "testing" + } + ] + }, + { + "uid": "learn-dynamics.test-automation.4-answers-about-testing", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "appsource-tests-run-for-every-supported-country", + "title": "AppSource tests must run separately for every supported country", + "disposition": "candidate", + "domain": "appsource" + }, + { + "id": "upgrade-tests-cover-nonadjacent-versions", + "title": "Upgrade tests cover nonadjacent historical versions", + "disposition": "candidate", + "domain": "testing" + } + ] + }, + { + "uid": "learn-dynamics.bring-app-appsource.4-technical-validation", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "appsource-submissions-cannot-use-runtime-packages", + "title": "AppSource submissions cannot use runtime packages", + "disposition": "candidate", + "domain": "appsource" + }, + { + "id": "per-tenant-and-marketplace-apps-need-distinct-identities", + "title": "Per-tenant and Marketplace variants need distinct app identities", + "disposition": "candidate", + "domain": "appsource" + }, + { + "id": "profiles-are-declared-with-profile-objects", + "title": "Profiles are shipped with AL profile objects rather than table inserts", + "disposition": "candidate", + "domain": "appsource" + }, + { + "id": "extension-layout-uses-relative-placement", + "title": "Extension layout uses named relative placement anchors", + "disposition": "candidate", + "domain": "breaking-changes" + } + ] + }, + { + "uid": "learn-dynamics.test-automation.3-documentation-examples", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "tests-use-reproducible-random-data", + "title": "Tests use reproducible randomized data instead of shared literals", + "disposition": "candidate", + "domain": "testing" + }, + { + "id": "al-tests-run-through-vscode-test-explorer", + "title": "AL tests can run and debug through Visual Studio Code Test Explorer", + "disposition": "candidate", + "domain": "testing" + } + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.4-manage-apps", + "reviewStatus": "in-progress", + "outcomes": [ + { + "id": "manage-apps-separates-install-and-update-requirements", + "title": "Manage Apps separates missing dependencies from dependency updates", + "disposition": "candidate", + "domain": "upgrade" + } + ] + } + ] +} diff --git a/coverage/microsoft-learn-developer-catalog.json b/coverage/microsoft-learn-developer-catalog.json new file mode 100644 index 00000000..68fe8595 --- /dev/null +++ b/coverage/microsoft-learn-developer-catalog.json @@ -0,0 +1,8282 @@ +{ + "version": 1, + "source": { + "provider": "Microsoft Learn", + "catalogUri": "https://learn.microsoft.com/api/catalog/?locale=en-us", + "locale": "en-us", + "filters": { + "product": "dynamics-business-central", + "role": "developer" + }, + "catalogLastModified": "2026-09-03T23:14:00Z" + }, + "counts": { + "learningPaths": 12, + "modules": 74, + "units": 623 + }, + "learningPaths": [ + { + "uid": "learn-dynamics.application-foundation-al-language", + "title": "Learn the application foundation with the AL programming language for Microsoft Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/application-foundation-al-language", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 573, + "moduleUids": [ + "learn-dynamics.al-built-in-functions", + "learn-dynamics.al-statements", + "learn-dynamics.create-custom-functions", + "learn-dynamics.events-triggers", + "learn-dynamics.handle-errors", + "learn-dynamics.interfaces", + "learn-dynamics.intro-basics-al-programming", + "learn-dynamics.manage-multilanguage-development", + "learn-dynamics.manipulate-data-via-code", + "learn-dynamics.work-with-codeunits" + ] + }, + { + "uid": "learn-dynamics.build-reports", + "title": "Build reports for Microsoft Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/build-reports", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 482, + "moduleUids": [ + "learn-dynamics.add-layout-report", + "learn-dynamics.add-reports-tell-me", + "learn-dynamics.complete-report-request-page", + "learn-dynamics.design-data-model-report", + "learn-dynamics.implement-multilanguage-reports", + "learn-dynamics.introduction-report-components", + "learn-dynamics.report-extensibility", + "learn-dynamics.understand-report-design-process", + "learn-dynamics.understand-report-triggers-functions", + "learn-dynamics.work-with-reports" + ] + }, + { + "uid": "learn-dynamics.continuous-integration", + "title": "Continuous integration with Azure DevOps for Microsoft Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/continuous-integration", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 439, + "moduleUids": [ + "learn-dynamics.introduction-test-automation", + "learn-dynamics.use-application-lifecycle", + "learn-dynamics.use-azure-pipelines", + "learn-dynamics.use-branch-merge-git", + "learn-dynamics.use-docker-business-central", + "learn-dynamics.work-azure-boards", + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.data-management-foundation-business-central", + "title": "Learn about the data management foundation in Microsoft Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/data-management-foundation-business-central", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 205, + "moduleUids": [ + "learn-dynamics.introduction-basics-file-management", + "learn-dynamics.work-with-queries", + "learn-dynamics.work-with-xmlports" + ] + }, + { + "uid": "learn-dynamics.development-get-started-business-central", + "title": "Get started with development in Microsoft Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/development-get-started-business-central", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 262, + "moduleUids": [ + "learn-dynamics.add-new-objects", + "learn-dynamics.assisted-setup-tooltips", + "learn-dynamics.extend-functionality-using-events", + "learn-dynamics.extend-tables-page-objects", + "learn-dynamics.initialize-data-extension-installation", + "learn-dynamics.introduction-development-process" + ] + }, + { + "uid": "learn-dynamics.essential-development-standards", + "title": "Work with essential development standards for Microsoft Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/essential-development-standards", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 223, + "moduleUids": [ + "learn-dynamics.identify-table-types", + "learn-dynamics.implement-data-process-model-business-central", + "learn-dynamics.use-document-standards-business-central", + "learn-dynamics.work-with-master-data-business-central" + ] + }, + { + "uid": "learn-dynamics.foundation-customize-business-central", + "title": "Discover the foundation of customizing Microsoft Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/foundation-customize-business-central", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 608, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension", + "learn-dynamics.extend-modify-existing-pages", + "learn-dynamics.extend-modify-existing-table", + "learn-dynamics.intro-development-environment", + "learn-dynamics.work-entitlements-permission-sets", + "learn-dynamics.work-with-pages", + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.interface-with-business-central", + "title": "Interface with Microsoft Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/interface-with-business-central", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 283, + "moduleUids": [ + "learn-dynamics.access-rest-services", + "learn-dynamics.build-control-add-ins", + "learn-dynamics.use-al-controlled-barcode-scanning", + "learn-dynamics.use-azure-functions", + "learn-dynamics.work-with-api", + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.power-apps-power-automate-business-central", + "title": "Develop using Power Apps and Power Automate for Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/power-apps-power-automate-business-central", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 396, + "moduleUids": [ + "learn-dynamics.create-canvas-app", + "learn-dynamics.create-custom-connector-power-platform", + "learn-dynamics.introduction-power-platform-business-central", + "learn-dynamics.use-model-driven-apps-common-data-service", + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.tailor-roles-design-ui", + "title": "Tailor roles and design the UI for Microsoft Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/tailor-roles-design-ui", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 256, + "moduleUids": [ + "learn-dynamics.build-assisted-setup", + "learn-dynamics.customize-ui-experience", + "learn-dynamics.develop-role-center", + "learn-dynamics.send-notifications", + "learn-dynamics.set-up-onboarding-experiences" + ] + }, + { + "uid": "learn-dynamics.use-application-development-business-central", + "title": "Use application development best practices in Microsoft Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/use-application-development-business-central", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 377, + "moduleUids": [ + "learn-dynamics.application-monetization", + "learn-dynamics.application-performance-monitor", + "learn-dynamics.application-types", + "learn-dynamics.bring-app-appsource", + "learn-dynamics.easy-application-upgrade", + "learn-dynamics.implement-saas-techniques", + "learn-dynamics.test-automation" + ] + }, + { + "uid": "learn-dynamics.use-power-bi", + "title": "Use Power BI in Microsoft Dynamics 365 Business Central", + "url": "https://learn.microsoft.com/en-us/training/paths/use-power-bi", + "lastModified": "2025-03-24T22:55:00Z", + "durationInMinutes": 233, + "moduleUids": [ + "learn-dynamics.build-power-bi-report", + "learn-dynamics.create-data-set", + "learn-dynamics.work-power-bi" + ] + } + ], + "modules": [ + { + "uid": "learn-dynamics.access-rest-services", + "title": "Access REST services from within Dynamics 365 Business Central", + "summary": "Learn how to use HTTP data types, connect to external REST services, and handle JSON data in Dynamics 365 Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 62, + "levels": [ + "intermediate" + ], + "subjects": [ + "cross-development" + ], + "learningPathUids": "learn-dynamics.interface-with-business-central", + "unitUids": [ + "learn-dynamics.access-rest-services.1-introduction", + "learn-dynamics.access-rest-services.2-http-classes", + "learn-dynamics.access-rest-services.3-get-send-data", + "learn-dynamics.access-rest-services.4-use-read-json", + "learn-dynamics.access-rest-services.5-get-json-data", + "learn-dynamics.access-rest-services.6-exercise", + "learn-dynamics.access-rest-services.7-check", + "learn-dynamics.access-rest-services.8-summary" + ] + }, + { + "uid": "learn-dynamics.add-layout-report", + "title": "Add a layout to a report in Dynamics 365 Business Central", + "summary": "Do you want to learn how to add a layout to a report in Microsoft Dynamics 365 Business Central? In this module, you'll learn about the client report definition (RDLC) layout and Word layout formats and how to create them.", + "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 70, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.build-reports", + "unitUids": [ + "learn-dynamics.add-layout-report.1-introduction", + "learn-dynamics.add-layout-report.2-add-report-definition", + "learn-dynamics.add-layout-report.3-add-word-layout", + "learn-dynamics.add-layout-report.word-add-in", + "learn-dynamics.add-layout-report.add-excel-layout", + "learn-dynamics.add-layout-report.4-exercise", + "learn-dynamics.add-layout-report.5-check", + "learn-dynamics.add-layout-report.6-summary" + ] + }, + { + "uid": "learn-dynamics.add-new-objects", + "title": "Add new objects to Dynamics 365 Business Central", + "summary": "Do you want to know how to add objects, like tables and pages, to Microsoft Dynamics 365 Business Central? If so, this module is for you. In this module, you'll learn how to create tables and pages for an extension.", + "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 57, + "levels": [ + "beginner" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.development-get-started-business-central", + "unitUids": [ + "learn-dynamics.add-new-objects.1-introduction", + "learn-dynamics.add-new-objects.2-reward", + "learn-dynamics.add-new-objects.3-activation-code", + "learn-dynamics.add-new-objects.4-customer-rewards", + "learn-dynamics.add-new-objects.5-reward-list", + "learn-dynamics.add-new-objects.6-exercise", + "learn-dynamics.add-new-objects.7-exercise", + "learn-dynamics.add-new-objects.8-exercise", + "learn-dynamics.add-new-objects.9-check", + "learn-dynamics.add-new-objects.10-summary" + ] + }, + { + "uid": "learn-dynamics.add-reports-tell-me", + "title": "Add reports to Tell Me and substitute a report in Dynamics 365 Business Central", + "summary": "Do you want to learn how to add reports to Tell Me and substitute a report in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to add reports to Tell Me and how to substitute a report.", + "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 27, + "levels": [ + "intermediate" + ], + "subjects": [ + "finance-accounting" + ], + "learningPathUids": "learn-dynamics.build-reports", + "unitUids": [ + "learn-dynamics.add-reports-tell-me.1-introduction", + "learn-dynamics.add-reports-tell-me.2-add-reports-tell-me", + "learn-dynamics.add-reports-tell-me.3-substitute-report", + "learn-dynamics.add-reports-tell-me.4-check", + "learn-dynamics.add-reports-tell-me.5-summary" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions", + "title": "Work with AL built-in functions in Dynamics 365 Business Central", + "summary": "Do you want to know more about the built-in programming functions of Business Central? This module will discuss the different available functions that you can use when developing for Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 86, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.application-foundation-al-language", + "unitUids": [ + "learn-dynamics.al-built-in-functions.1-introduction", + "learn-dynamics.al-built-in-functions.2-interaction-functions", + "learn-dynamics.al-built-in-functions.3-string-functions", + "learn-dynamics.al-built-in-functions.4-date-functions", + "learn-dynamics.al-built-in-functions.5-numeric-functions", + "learn-dynamics.al-built-in-functions.6-array-functions", + "learn-dynamics.al-built-in-functions.7-list-functions", + "learn-dynamics.al-built-in-functions.8-system-functions", + "learn-dynamics.al-built-in-functions.9-variable-functions", + "learn-dynamics.al-built-in-functions.10-exercise", + "learn-dynamics.al-built-in-functions.11-check", + "learn-dynamics.al-built-in-functions.12-summary" + ] + }, + { + "uid": "learn-dynamics.al-statements", + "title": "Learn the AL statements in Dynamics 365 Business Central", + "summary": "Do you want to know how to use basic AL statements? This module shows you how to use conditional statements, compound statements, and comments when developing for Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/al-statements", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 67, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.application-foundation-al-language", + "unitUids": [ + "learn-dynamics.al-statements.1-introduction", + "learn-dynamics.al-statements.2-compound", + "learn-dynamics.al-statements.3-conditional", + "learn-dynamics.al-statements.4-repetitive", + "learn-dynamics.al-statements.5-exercise", + "learn-dynamics.al-statements.6-check", + "learn-dynamics.al-statements.7-summary" + ] + }, + { + "uid": "learn-dynamics.application-monetization", + "title": "Consider application monetization responsibilities in Business Central", + "summary": "Do you want to understand your application monetization responsibilities? In this module, you'll learn about monetization responsibilities, and where to find useful guides and documentation.", + "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 23, + "levels": [ + "intermediate" + ], + "subjects": [ + "marketing-sales" + ], + "learningPathUids": "learn-dynamics.use-application-development-business-central", + "unitUids": [ + "learn-dynamics.application-monetization.1-introduction", + "learn-dynamics.application-monetization.2-responsibilities", + "learn-dynamics.application-monetization.3-documentation-examples", + "learn-dynamics.application-monetization.4-check", + "learn-dynamics.application-monetization.5-summary" + ] + }, + { + "uid": "learn-dynamics.application-performance-monitor", + "title": "Learn about application performance and monitoring in Business Central", + "summary": "Do you want to learn about application performance and monitoring? Learn what is required to write performant code, understand how you can monitor application performance, and get required insights.", + "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 33, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.use-application-development-business-central", + "unitUids": [ + "learn-dynamics.application-performance-monitor.1-introduction", + "learn-dynamics.application-performance-monitor.2-prepare-application", + "learn-dynamics.application-performance-monitor.3-get-insights", + "learn-dynamics.application-performance-monitor.4-check", + "learn-dynamics.application-performance-monitor.5-summary" + ] + }, + { + "uid": "learn-dynamics.application-types", + "title": "Introduction to application types in Business Central", + "summary": "Explore the differences between connect and addon applications, understand library and dependency apps, and learn app development resources in Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/application-types", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 66, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.use-application-development-business-central", + "unitUids": [ + "learn-dynamics.application-types.1-introduction", + "learn-dynamics.application-types.2-connect-vs-add-on", + "learn-dynamics.application-types.3-library-dependency-applications", + "learn-dynamics.application-types.4-find-development-resources", + "learn-dynamics.application-types.5-set-up-code-cop-tools", + "learn-dynamics.application-types.6-check", + "learn-dynamics.application-types.7-summary" + ] + }, + { + "uid": "learn-dynamics.assisted-setup-tooltips", + "title": "Introduction to assisted setup and tooltips in Dynamics 365 Business Central", + "summary": "Do you want to know an effective way to let a user perform the installation steps of your extension by using an efficient and accessible wizard in Microsoft Dynamics 365 Business Central? If so, this module is for you. In this module, you'll learn how to add a wizard page by using a navigate page object.", + "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 47, + "levels": [ + "beginner" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.development-get-started-business-central", + "unitUids": [ + "learn-dynamics.assisted-setup-tooltips.1-introduction", + "learn-dynamics.assisted-setup-tooltips.2-rewards-wizard", + "learn-dynamics.assisted-setup-tooltips.3-assisted-setup", + "learn-dynamics.assisted-setup-tooltips.4-tooltips", + "learn-dynamics.assisted-setup-tooltips.5-exercise", + "learn-dynamics.assisted-setup-tooltips.6-exercise", + "learn-dynamics.assisted-setup-tooltips.7-check", + "learn-dynamics.assisted-setup-tooltips.8-summary" + ] + }, + { + "uid": "learn-dynamics.bring-app-appsource", + "title": "Understand the process of bringing an app to AppSource in Business Central", + "summary": "Do you want to know about technical and marketing AppSource validation procedures? In this module, you'll learn what is required from a technical and marketing point of view for your application to be validated for AppSource.", + "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 45, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.use-application-development-business-central", + "unitUids": [ + "learn-dynamics.bring-app-appsource.1-introduction", + "learn-dynamics.bring-app-appsource.2-get-accounts", + "learn-dynamics.bring-app-appsource.3-marketing-validation", + "learn-dynamics.bring-app-appsource.4-technical-validation", + "learn-dynamics.bring-app-appsource.5-check", + "learn-dynamics.bring-app-appsource.6-summary" + ] + }, + { + "uid": "learn-dynamics.build-assisted-setup", + "title": "Build an Assisted Setup in Dynamics 365 Business Central", + "summary": "Assisted Setup guides you in configuring Business Central. Do you want to learn how you can integrate with the Assisted Setup feature and create your own wizard pages? If so, this module explains that process.", + "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 54, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.tailor-roles-design-ui", + "unitUids": [ + "learn-dynamics.build-assisted-setup.1-introduction", + "learn-dynamics.build-assisted-setup.2-assisted-setup", + "learn-dynamics.build-assisted-setup.3-wizard-pages", + "learn-dynamics.build-assisted-setup.4-extensions", + "learn-dynamics.build-assisted-setup.5-exercise", + "learn-dynamics.build-assisted-setup.6-check", + "learn-dynamics.build-assisted-setup.7-summary" + ] + }, + { + "uid": "learn-dynamics.build-control-add-ins", + "title": "Build control add-in objects in Dynamics 365 Business Central", + "summary": "Do you need to build control add-ins? This module discusses how to build and integrate control add-ins with JavaScript in AL.", + "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 51, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.interface-with-business-central", + "unitUids": [ + "learn-dynamics.build-control-add-ins.1-introduction", + "learn-dynamics.build-control-add-ins.2-apply-look-feel", + "learn-dynamics.build-control-add-ins.3-create", + "learn-dynamics.build-control-add-ins.4-asynchronous-considerations", + "learn-dynamics.build-control-add-ins.5-connect", + "learn-dynamics.build-control-add-ins.6-exercise", + "learn-dynamics.build-control-add-ins.7-check", + "learn-dynamics.build-control-add-ins.8-summary" + ] + }, + { + "uid": "learn-dynamics.build-power-bi-report", + "title": "Build a Power BI report with Business Central data", + "summary": "Do you want to build a Business Central report using Power BI Desktop? This module will guide you through the steps required to create a Power BI report.", + "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 74, + "levels": [ + "intermediate" + ], + "subjects": [ + "cloud-computing", + "data-visualization" + ], + "learningPathUids": "learn-dynamics.use-power-bi", + "unitUids": [ + "learn-dynamics.build-power-bi-report.1-introduction", + "learn-dynamics.build-power-bi-report.2-build-data-model", + "learn-dynamics.build-power-bi-report.3-add-data-dimension", + "learn-dynamics.build-power-bi-report.4-introduction-dax", + "learn-dynamics.build-power-bi-report.5-create-report", + "learn-dynamics.build-power-bi-report.6-build-dashboard", + "learn-dynamics.build-power-bi-report.7-exercise", + "learn-dynamics.build-power-bi-report.8-exercise", + "learn-dynamics.build-power-bi-report.9-check", + "learn-dynamics.build-power-bi-report.10-summary" + ] + }, + { + "uid": "learn-dynamics.business-central-teams-integration", + "title": "Integrate Dynamics 365 Business Central with Teams", + "summary": "Learn to integrate Dynamics 365 Business Central with Microsoft Teams.", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 48, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": null, + "unitUids": [ + "learn-dynamics.business-central-teams-integration.introduction", + "learn-dynamics.business-central-teams-integration.manage-install-use", + "learn-dynamics.business-central-teams-integration.extend-teams-cards", + "learn-dynamics.business-central-teams-integration.create-tabs", + "learn-dynamics.business-central-teams-integration.get-session-details", + "learn-dynamics.business-central-teams-integration.check", + "learn-dynamics.business-central-teams-integration.summary" + ] + }, + { + "uid": "learn-dynamics.complete-report-request-page", + "title": "Complete the report request page in Dynamics 365 Business Central", + "summary": "Do you want to learn how to complete the report request page for a report in Business Central? In this module, you'll learn about the report request page and how to create it.", + "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 36, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.build-reports", + "unitUids": [ + "learn-dynamics.complete-report-request-page.1-introduction", + "learn-dynamics.complete-report-request-page.2-design-request-page", + "learn-dynamics.complete-report-request-page.3-design-request-options", + "learn-dynamics.complete-report-request-page.4-check", + "learn-dynamics.complete-report-request-page.5-summary" + ] + }, + { + "uid": "learn-dynamics.create-canvas-app", + "title": "Create a canvas app based on Business Central data", + "summary": "Do you want to create a canvas app using Business Central data? This module focuses on how to create canvas app using Business Central data and how to customize the created app.", + "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app", + "lastModified": "2026-08-03T17:47:00Z", + "durationInMinutes": 73, + "levels": [ + "intermediate" + ], + "subjects": [ + "cross-development" + ], + "learningPathUids": "learn-dynamics.power-apps-power-automate-business-central", + "unitUids": [ + "learn-dynamics.create-canvas-app.1-introduction", + "learn-dynamics.create-canvas-app.2-data-app", + "learn-dynamics.create-canvas-app.3-blank-app", + "learn-dynamics.create-canvas-app.4-customizations", + "learn-dynamics.create-canvas-app.5-save-publish-share", + "learn-dynamics.create-canvas-app.6-embed", + "learn-dynamics.create-canvas-app.7-exercise", + "learn-dynamics.create-canvas-app.8-check", + "learn-dynamics.create-canvas-app.9-summary" + ] + }, + { + "uid": "learn-dynamics.create-custom-connector-power-platform", + "title": "Create a custom connector to extend Microsoft Power Platform functionalities with Business Central", + "summary": "Do you need to create custom connectors for Business Central? Use this module to find out the details of creating custom connectors.", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform", + "lastModified": "2026-08-05T23:09:00Z", + "durationInMinutes": 59, + "levels": [ + "intermediate" + ], + "subjects": [ + "cross-development" + ], + "learningPathUids": "learn-dynamics.power-apps-power-automate-business-central", + "unitUids": [ + "learn-dynamics.create-custom-connector-power-platform.1-introduction", + "learn-dynamics.create-custom-connector-power-platform.2-custom-connector", + "learn-dynamics.create-custom-connector-power-platform.3-power-apps", + "learn-dynamics.create-custom-connector-power-platform.4-power-automate", + "learn-dynamics.create-custom-connector-power-platform.5-share-custom-connectors", + "learn-dynamics.create-custom-connector-power-platform.6-exercise", + "learn-dynamics.create-custom-connector-power-platform.7-check", + "learn-dynamics.create-custom-connector-power-platform.8-summary" + ] + }, + { + "uid": "learn-dynamics.create-custom-functions", + "title": "Create custom procedures in Dynamics 365 Business Central", + "summary": "Do you want to create custom procedures in AL? This module focuses on explaining how you can create new procedures. Additionally, it describes the difference between local and global procedures, the difference between local and global variables, and how you can pass variables to a procedure.", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 37, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.application-foundation-al-language", + "unitUids": [ + "learn-dynamics.create-custom-functions.1-introduction", + "learn-dynamics.create-custom-functions.2-custom-function", + "learn-dynamics.create-custom-functions.3-local-global-function", + "learn-dynamics.create-custom-functions.4-local-global-variable", + "learn-dynamics.create-custom-functions.5-pass", + "learn-dynamics.create-custom-functions.6-parameter", + "learn-dynamics.create-custom-functions.7-check", + "learn-dynamics.create-custom-functions.8-summary" + ] + }, + { + "uid": "learn-dynamics.create-data-set", + "title": "Create a dataset for a Power BI report in Dynamics 365 Business Central", + "summary": "Learn how to create a dataset for a Power BI report in Business Central using assisted reporting setup and custom data sets.", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 94, + "levels": [ + "intermediate" + ], + "subjects": [ + "data-analytics" + ], + "learningPathUids": "learn-dynamics.use-power-bi", + "unitUids": [ + "learn-dynamics.create-data-set.1-introduction", + "learn-dynamics.create-data-set.2-install", + "learn-dynamics.create-data-set.3-use-data-source", + "learn-dynamics.create-data-set.5-develop-query-page-objects", + "learn-dynamics.create-data-set.develop-api-query", + "learn-dynamics.create-data-set.use-dedicated-apis", + "learn-dynamics.create-data-set.6-mind-performance-impact", + "learn-dynamics.create-data-set.create-power-bi-dataflow", + "learn-dynamics.create-data-set.7-exercise", + "learn-dynamics.create-data-set.9-exercise", + "learn-dynamics.create-data-set.10-exercise", + "learn-dynamics.create-data-set.11-check", + "learn-dynamics.create-data-set.12-summary" + ] + }, + { + "uid": "learn-dynamics.customize-ui-experience", + "title": "Customize the UI experience in Dynamics 365 Business Central", + "summary": "Do you want to further customize the UI experience for a user? Do you want to be able to add your pages and reports to the search functionality? In this module, you'll learn how to create page customizations for specific user profiles and extend the application areas.", + "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 50, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.tailor-roles-design-ui", + "unitUids": [ + "learn-dynamics.customize-ui-experience.1-introduction", + "learn-dynamics.customize-ui-experience.2-objects", + "learn-dynamics.customize-ui-experience.3-customizations", + "learn-dynamics.customize-ui-experience.4-profiles", + "learn-dynamics.customize-ui-experience.extend-customize", + "learn-dynamics.customize-ui-experience.5-views", + "learn-dynamics.customize-ui-experience.6-application-areas", + "learn-dynamics.customize-ui-experience.7-check", + "learn-dynamics.customize-ui-experience.8-summary" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension", + "title": "Debug and deploy your extension in Dynamics 365 Business Central", + "summary": "Discover how to debug with Visual Studio Code and deploy your extension using RAD feature in Dynamics 365 Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 134, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.foundation-customize-business-central", + "unitUids": [ + "learn-dynamics.debug-deploy-extension.1-introduction", + "learn-dynamics.debug-deploy-extension.2-debug", + "learn-dynamics.debug-deploy-extension.recovery-failures", + "learn-dynamics.debug-deploy-extension.3-attach", + "learn-dynamics.debug-deploy-extension.3a-snapshot", + "learn-dynamics.debug-deploy-extension.4-rapid-application-development", + "learn-dynamics.debug-deploy-extension.5-publish", + "learn-dynamics.debug-deploy-extension.resource-policy-settings", + "learn-dynamics.debug-deploy-extension.protect-sensitive-values", + "learn-dynamics.debug-deploy-extension.projects-workspaces", + "learn-dynamics.debug-deploy-extension.code-actions", + "learn-dynamics.debug-deploy-extension.generate-help", + "learn-dynamics.debug-deploy-extension.5a-performance-toolkit", + "learn-dynamics.debug-deploy-extension.database-wait-statistics", + "learn-dynamics.debug-deploy-extension.6-exercise", + "learn-dynamics.debug-deploy-extension.7-exercise", + "learn-dynamics.debug-deploy-extension.8-check", + "learn-dynamics.debug-deploy-extension.9-summary" + ] + }, + { + "uid": "learn-dynamics.design-data-model-report", + "title": "Design the data model of a report in Dynamics 365 Business Central", + "summary": "Do you want to learn how to build the data model for a report in Microsoft Dynamics 365 Business Central? This module will discuss the different components of a report data model.", + "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 48, + "levels": [ + "intermediate" + ], + "subjects": [ + "data-modeling" + ], + "learningPathUids": "learn-dynamics.build-reports", + "unitUids": [ + "learn-dynamics.design-data-model-report.1-introduction", + "learn-dynamics.design-data-model-report.2-create-report-data-items", + "learn-dynamics.design-data-model-report.3-add-columns-report-dataset", + "learn-dynamics.design-data-model-report.4-link-order-indent-data-items", + "learn-dynamics.design-data-model-report.use-query", + "learn-dynamics.design-data-model-report.5-check", + "learn-dynamics.design-data-model-report.6-summary" + ] + }, + { + "uid": "learn-dynamics.develop-role-center", + "title": "Develop a Role Center in Dynamics 365 Business Central", + "summary": "Create a Role Center page for Microsoft Dynamics 365 Business Central and learn how it's constructed.", + "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 60, + "levels": [ + "intermediate" + ], + "subjects": [ + "finance-accounting" + ], + "learningPathUids": "learn-dynamics.tailor-roles-design-ui", + "unitUids": [ + "learn-dynamics.develop-role-center.1-introduction", + "learn-dynamics.develop-role-center.2-create-page", + "learn-dynamics.develop-role-center.3-activity-parts", + "learn-dynamics.develop-role-center.4-cue-setup", + "learn-dynamics.develop-role-center.5-role-center-lists", + "learn-dynamics.develop-role-center.6-headline-parts", + "learn-dynamics.develop-role-center.7-exercise", + "learn-dynamics.develop-role-center.8-check", + "learn-dynamics.develop-role-center.9-summary" + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade", + "title": "Prepare for an easy application upgrade experience in Business Central", + "summary": "Do you want to learn about how to build efficient installation and upgrade procedures? There are essential techniques you should learn to build good and user-friendly applications.", + "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 88, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications", + "data-management" + ], + "learningPathUids": "learn-dynamics.use-application-development-business-central", + "unitUids": [ + "learn-dynamics.easy-application-upgrade.1-introduction", + "learn-dynamics.easy-application-upgrade.2-upgrade-responsibilities", + "learn-dynamics.easy-application-upgrade.consider-update-lifecycle", + "learn-dynamics.easy-application-upgrade.3-installation-upgrade-codeunits", + "learn-dynamics.easy-application-upgrade.transfer-data", + "learn-dynamics.easy-application-upgrade.4-manage-apps", + "learn-dynamics.easy-application-upgrade.5-detect-breaking-changes", + "learn-dynamics.easy-application-upgrade.6-answers-about-updates", + "learn-dynamics.easy-application-upgrade.7-check", + "learn-dynamics.easy-application-upgrade.8-summary" + ] + }, + { + "uid": "learn-dynamics.events-triggers", + "title": "Use events and triggers in Dynamics 365 Business Central", + "summary": "Do you want to know how to access different table and page triggers? Do you also want to learn how to develop by using event-based architecture? If so, this module will explain how to use events and triggers in Microsoft Dynamics 365 Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 60, + "levels": [ + "intermediate" + ], + "subjects": [ + "architecture", + "data-visualization" + ], + "learningPathUids": "learn-dynamics.application-foundation-al-language", + "unitUids": [ + "learn-dynamics.events-triggers.1-introduction", + "learn-dynamics.events-triggers.2-table-field-triggers", + "learn-dynamics.events-triggers.3-page-control-action-triggers", + "learn-dynamics.events-triggers.4-event-based", + "learn-dynamics.events-triggers.5-publish-subscribe", + "learn-dynamics.events-triggers.6-exercise", + "learn-dynamics.events-triggers.7-check", + "learn-dynamics.events-triggers.8-summary" + ] + }, + { + "uid": "learn-dynamics.extend-functionality-using-events", + "title": "Extend functionality by using events in Dynamics 365 Business Central", + "summary": "Do you want to know how to add your business logic to existing functionality in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to add code and work with events.", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 44, + "levels": [ + "beginner" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.development-get-started-business-central", + "unitUids": [ + "learn-dynamics.extend-functionality-using-events.1-introduction", + "learn-dynamics.extend-functionality-using-events.2-programming-basics", + "learn-dynamics.extend-functionality-using-events.3-customer-rewards", + "learn-dynamics.extend-functionality-using-events.4-exercise", + "learn-dynamics.extend-functionality-using-events.5-check", + "learn-dynamics.extend-functionality-using-events.6-summary" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-pages", + "title": "Extend and modify an existing page in Dynamics 365 Business Central", + "summary": "Do you want to add extra fields or remove fields from an existing page? If so, this module will help you discover how to modify an existing page by using page extensions.", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages", + "lastModified": "2026-09-01T17:24:00Z", + "durationInMinutes": 45, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.foundation-customize-business-central", + "unitUids": [ + "learn-dynamics.extend-modify-existing-pages.1-introduction", + "learn-dynamics.extend-modify-existing-pages.2-get-started", + "learn-dynamics.extend-modify-existing-pages.3-properties", + "learn-dynamics.extend-modify-existing-pages.4-layout", + "learn-dynamics.extend-modify-existing-pages.5-check", + "learn-dynamics.extend-modify-existing-pages.6-summary" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-table", + "title": "Extend and modify an existing table in Dynamics 365 Business Central", + "summary": "Do you want to add extra fields to an existing table? This module discusses how you can modify an existing table by using table extensions.", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table", + "lastModified": "2026-08-17T23:09:00Z", + "durationInMinutes": 33, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.foundation-customize-business-central", + "unitUids": [ + "learn-dynamics.extend-modify-existing-table.1-introduction", + "learn-dynamics.extend-modify-existing-table.2-get-started", + "learn-dynamics.extend-modify-existing-table.3-properties", + "learn-dynamics.extend-modify-existing-table.add-field-group", + "learn-dynamics.extend-modify-existing-table.define-extension-objects", + "learn-dynamics.extend-modify-existing-table.4-check", + "learn-dynamics.extend-modify-existing-table.5-summary" + ] + }, + { + "uid": "learn-dynamics.extend-tables-page-objects", + "title": "Extend existing table and page objects in Dynamics 365 Business Central", + "summary": "Do you want to know how to modify existing tables and pages for Microsoft Dynamics 365 Business Central? In this module, you'll learn ways to create and customize table and page objects.", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 42, + "levels": [ + "beginner" + ], + "subjects": [ + "data-modeling" + ], + "learningPathUids": "learn-dynamics.development-get-started-business-central", + "unitUids": [ + "learn-dynamics.extend-tables-page-objects.1-introduction", + "learn-dynamics.extend-tables-page-objects.2-customer-table", + "learn-dynamics.extend-tables-page-objects.3-customer-card", + "learn-dynamics.extend-tables-page-objects.4-customer-list", + "learn-dynamics.extend-tables-page-objects.5-exercise", + "learn-dynamics.extend-tables-page-objects.6-exercise", + "learn-dynamics.extend-tables-page-objects.7-exercise", + "learn-dynamics.extend-tables-page-objects.8-check", + "learn-dynamics.extend-tables-page-objects.9-summary" + ] + }, + { + "uid": "learn-dynamics.handle-errors", + "title": "Handle errors by using application language in Dynamics 365 Business Central", + "summary": "Learn how to handle errors in Dynamics 365 Business Central by using application language (AL).", + "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 35, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.application-foundation-al-language", + "unitUids": [ + "learn-dynamics.handle-errors.introduction", + "learn-dynamics.handle-errors.handle-errors", + "learn-dynamics.handle-errors.collect-errors", + "learn-dynamics.handle-errors.progress-windows", + "learn-dynamics.handle-errors.check", + "learn-dynamics.handle-errors.summary" + ] + }, + { + "uid": "learn-dynamics.identify-table-types", + "title": "Identify functional table types and characteristics in Dynamics 365 Business Central", + "summary": "Do you want to learn about table types in Business Central? In this module, you'll learn about the different functional table types and their specific characteristics, such as special fields, properties, and linked pages.", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 68, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.essential-development-standards", + "unitUids": [ + "learn-dynamics.identify-table-types.1-introduction", + "learn-dynamics.identify-table-types.2-define-table-types", + "learn-dynamics.identify-table-types.3-create-master-tables", + "learn-dynamics.identify-table-types.4-create-supplemental-tables", + "learn-dynamics.identify-table-types.5-create-subsidiary-tables", + "learn-dynamics.identify-table-types.6-create-ledger-tables", + "learn-dynamics.identify-table-types.7-create-register-tables", + "learn-dynamics.identify-table-types.8-create-journal-tables", + "learn-dynamics.identify-table-types.9-create-document-tables", + "learn-dynamics.identify-table-types.10-create-setup-tables", + "learn-dynamics.identify-table-types.11-check", + "learn-dynamics.identify-table-types.12-summary" + ] + }, + { + "uid": "learn-dynamics.implement-data-process-model-business-central", + "title": "Implement the data process model in Dynamics 365 Business Central", + "summary": "Do you want to understand how data flows in Business Central? This module provides an overview of how data flows within the application so you can develop solutions efficiently.", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 37, + "levels": [ + "intermediate" + ], + "subjects": [ + "data-modeling", + "process-workflow" + ], + "learningPathUids": "learn-dynamics.essential-development-standards", + "unitUids": [ + "learn-dynamics.implement-data-process-model-business-central.1-introduction", + "learn-dynamics.implement-data-process-model-business-central.2-standard-data-model", + "learn-dynamics.implement-data-process-model-business-central.3-typical-data-triggers", + "learn-dynamics.implement-data-process-model-business-central.4-standard-data-flow", + "learn-dynamics.implement-data-process-model-business-central.5-check", + "learn-dynamics.implement-data-process-model-business-central.6-summary" + ] + }, + { + "uid": "learn-dynamics.implement-multilanguage-reports", + "title": "Implement multilingual reports in Dynamics 365 Business Central", + "summary": "Do you want to learn how to make your reports multilingual in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to generate a report in the language of the user or the recipient.", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 36, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.build-reports", + "unitUids": [ + "learn-dynamics.implement-multilanguage-reports.1-introduction", + "learn-dynamics.implement-multilanguage-reports.2-create-report-user-language", + "learn-dynamics.implement-multilanguage-reports.3-create-report-recipient-language", + "learn-dynamics.implement-multilanguage-reports.define-regional-settings", + "learn-dynamics.implement-multilanguage-reports.4-check", + "learn-dynamics.implement-multilanguage-reports.5-summary" + ] + }, + { + "uid": "learn-dynamics.implement-saas-techniques", + "title": "Implement SaaSification techniques in Business Central", + "summary": "Do you want to learn about how to build your user interface using SaaSification techniques? Learn about properly building a good user interface and first trial experience for the users of your application.", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 31, + "levels": [ + "intermediate" + ], + "subjects": [ + "custom-app-development" + ], + "learningPathUids": "learn-dynamics.use-application-development-business-central", + "unitUids": [ + "learn-dynamics.implement-saas-techniques.1-introduction", + "learn-dynamics.implement-saas-techniques.2-build-trial", + "learn-dynamics.implement-saas-techniques.3-implement-saasification", + "learn-dynamics.implement-saas-techniques.4-check", + "learn-dynamics.implement-saas-techniques.5-summary" + ] + }, + { + "uid": "learn-dynamics.initialize-data-extension-installation", + "title": "Initialize data during extension installation in Dynamics 365 Business Central", + "summary": "Do you want to know how to initialize data during extension installation for Microsoft Dynamics 365 Business Central? Initializing data when you're installing an extension automate many steps that you must otherwise perform manually. By automating this process, you can quickly set up an extension and it becomes more user-friendly.", + "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 32, + "levels": [ + "beginner" + ], + "subjects": [ + "data-engineering" + ], + "learningPathUids": "learn-dynamics.development-get-started-business-central", + "unitUids": [ + "learn-dynamics.initialize-data-extension-installation.1-introduction", + "learn-dynamics.initialize-data-extension-installation.2-install-logic", + "learn-dynamics.initialize-data-extension-installation.3-exercise", + "learn-dynamics.initialize-data-extension-installation.4-check", + "learn-dynamics.initialize-data-extension-installation.5-summary" + ] + }, + { + "uid": "learn-dynamics.interfaces", + "title": "Implement interfaces in Dynamics 365 Business Central", + "summary": "Do you want to know how to implement interfaces in AL? If so, this module will explain how to implement interfaces in AL for use with Microsoft Dynamics 365 Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 54, + "levels": [ + "intermediate" + ], + "subjects": [ + "process-workflow" + ], + "learningPathUids": "learn-dynamics.application-foundation-al-language", + "unitUids": [ + "learn-dynamics.business-central-interfaces.1-introduction", + "learn-dynamics.business-central-interfaces.2-snippet-support", + "learn-dynamics.business-central-interfaces.type-testing", + "learn-dynamics.business-central-interfaces.3-why-interfaces", + "learn-dynamics.business-central-interfaces.extending-interfaces", + "learn-dynamics.business-central-interfaces.4-exercise", + "learn-dynamics.business-central-interfaces.5-check", + "learn-dynamics.business-central-interfaces.6-summary" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming", + "title": "Introduction to the basics of AL programming in Dynamics 365 Business Central", + "summary": "Explore the basics of Application Language (AL) programming, including variables, data types, options, and enums.", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 88, + "levels": [ + "beginner" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.application-foundation-al-language", + "unitUids": [ + "learn-dynamics.intro-basics-al-programming.1-intro", + "learn-dynamics.intro-basics-al-programming.2-get-started", + "learn-dynamics.intro-basics-al-programming.3-variables", + "learn-dynamics.intro-basics-al-programming.4-data-types", + "learn-dynamics.intro-basics-al-programming.5-options-enums", + "learn-dynamics.intro-basics-al-programming.6-collections", + "learn-dynamics.intro-basics-al-programming.7-assignments", + "learn-dynamics.intro-basics-al-programming.8-expressions", + "learn-dynamics.intro-basics-al-programming.9-exercise", + "learn-dynamics.intro-basics-al-programming.10-exercise", + "learn-dynamics.intro-basics-al-programming.11-check", + "learn-dynamics.intro-basics-al-programming.12-summary" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment", + "title": "Introduction to the development environment for Dynamics 365 Business Central", + "summary": "Do you want to start developing for Microsoft Dynamics 365 Business Central? If so, this module is for you. Learn about the development environment that is used for Business Central development and how to configure that environment. You will also learn about the different objects in Microsoft Dynamics 365 Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 95, + "levels": [ + "beginner" + ], + "subjects": [ + "devops" + ], + "learningPathUids": "learn-dynamics.foundation-customize-business-central", + "unitUids": [ + "learn-dynamics.intro-development-environment.1-intro", + "learn-dynamics.intro-development-environment.2-get-started", + "learn-dynamics.intro-development-environment.3-install", + "learn-dynamics.intro-development-environment.4-logical-database", + "learn-dynamics.intro-development-environment.differentiate-apps", + "learn-dynamics.intro-development-environment.5-extension", + "learn-dynamics.intro-development-environment.6-json", + "learn-dynamics.intro-development-environment.7-al-language", + "learn-dynamics.intro-development-environment.8-multiple", + "learn-dynamics.intro-development-environment.segment-code-reduce-conflicts", + "learn-dynamics.intro-development-environment.open-visual-studio-code-web-client", + "learn-dynamics.intro-development-environment.9-exercise", + "learn-dynamics.intro-development-environment.10-check", + "learn-dynamics.intro-development-environment.11-summary" + ] + }, + { + "uid": "learn-dynamics.introduction-basics-file-management", + "title": "Introduction to the basics of file management in Dynamics 365 Business Central", + "summary": "Explore the file data type, File Management codeunit, Temp Blob codeunit, and stream functions for file manipulation in Dynamics 365 Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management", + "lastModified": "2026-08-18T17:07:00Z", + "durationInMinutes": 61, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications", + "data-management" + ], + "learningPathUids": "learn-dynamics.data-management-foundation-business-central", + "unitUids": [ + "learn-dynamics.introduction-basics-file-management.1-introduction", + "learn-dynamics.introduction-basics-file-management.2-file-object", + "learn-dynamics.introduction-basics-file-management.3-temp-blob-object", + "learn-dynamics.introduction-basics-file-management.4-read-write-stream", + "learn-dynamics.introduction-basics-file-management.uploads-drop-zones", + "learn-dynamics.introduction-basics-file-management.5-exercise", + "learn-dynamics.introduction-basics-file-management.6-check", + "learn-dynamics.introduction-basics-file-management.7-summary" + ] + }, + { + "uid": "learn-dynamics.introduction-development-process", + "title": "Introduction to the development process in Dynamics 365 Business Central", + "summary": "Do you want to know how to start creating an extension for Microsoft Dynamics 365 Business Central? If so, this module is for you. In this module, you'll learn about the different components that make up a typical extension. Then, the module will provide a brief description of the development environment and explain the different steps that are involved in getting started with an extension.", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 40, + "levels": [ + "beginner" + ], + "subjects": [ + "process-workflow" + ], + "learningPathUids": "learn-dynamics.development-get-started-business-central", + "unitUids": [ + "learn-dynamics.introduction-development-process.1-introduction", + "learn-dynamics.introduction-development-process.2-process", + "learn-dynamics.introduction-development-process.3-setup", + "learn-dynamics.introduction-development-process.4-rewards", + "learn-dynamics.introduction-development-process.5-exercise", + "learn-dynamics.introduction-development-process.6-exercise", + "learn-dynamics.introduction-development-process.7-check", + "learn-dynamics.introduction-development-process.8-summary" + ] + }, + { + "uid": "learn-dynamics.introduction-power-platform-business-central", + "title": "Introduction to Microsoft Power Platform and Business Central", + "summary": "Do you want to use Microsoft Power Platform with Business Central? This module focuses on the business value and product capabilities of Microsoft Power Platform when you use it with Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 53, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.power-apps-power-automate-business-central", + "unitUids": [ + "learn-dynamics.introduction-power-platform-business-central.1-introduction", + "learn-dynamics.introduction-power-platform-business-central.2-data-connectors", + "learn-dynamics.introduction-power-platform-business-central.3-business-central-connectors", + "learn-dynamics.introduction-power-platform-business-central.4-power-apps", + "learn-dynamics.introduction-power-platform-business-central.5-power-automate", + "learn-dynamics.introduction-power-platform-business-central.use-cases", + "learn-dynamics.introduction-power-platform-business-central.6-embed-power-platform", + "learn-dynamics.introduction-power-platform-business-central.7-check", + "learn-dynamics.introduction-power-platform-business-central.8-summary" + ] + }, + { + "uid": "learn-dynamics.introduction-report-components", + "title": "Introduction to report components in Dynamics 365 Business Central", + "summary": "Do you want to know how to start creating reports for Business Central? If so, this module will explain the different components that a typical report consists of.", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 37, + "levels": [ + "intermediate" + ], + "subjects": [ + "data-management" + ], + "learningPathUids": "learn-dynamics.build-reports", + "unitUids": [ + "learn-dynamics.introduction-report-components.1-introduction", + "learn-dynamics.introduction-report-components.2-learn-overview-report-description", + "learn-dynamics.introduction-report-components.3-report-properties-triggers", + "learn-dynamics.introduction-report-components.4-data-items-columns", + "learn-dynamics.introduction-report-components.5-request-page", + "learn-dynamics.introduction-report-components.6-work-report-labels", + "learn-dynamics.introduction-report-components.7-report-layouts", + "learn-dynamics.introduction-report-components.8-check", + "learn-dynamics.introduction-report-components.9-summary" + ] + }, + { + "uid": "learn-dynamics.introduction-test-automation", + "title": "Introduction to test automation in Business Central", + "summary": "Discover how to write test code in AL using Test Codeunits and run standard Business Central tests.", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 58, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.continuous-integration", + "unitUids": [ + "learn-dynamics.introduction-test-automation.1-introduction", + "learn-dynamics.introduction-test-automation.2-test-applications", + "learn-dynamics.introduction-test-automation.3-install-run-test-toolkit", + "learn-dynamics.introduction-test-automation.4-run-standard-tests", + "learn-dynamics.introduction-test-automation.5-create-test-code-units", + "learn-dynamics.introduction-test-automation.6-check", + "learn-dynamics.introduction-test-automation.7-summary" + ] + }, + { + "uid": "learn-dynamics.manage-multilanguage-development", + "title": "Manage multilanguage development in Dynamics 365 Business Central", + "summary": "Do you want to learn how to manage multiple languages in an AL extension? If so, this module explains how to create and use translation files.", + "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 43, + "levels": [ + "intermediate" + ], + "subjects": [ + "devops" + ], + "learningPathUids": "learn-dynamics.application-foundation-al-language", + "unitUids": [ + "learn-dynamics.manage-multilanguage-development.1-introduction", + "learn-dynamics.manage-multilanguage-development.2-translation-files", + "learn-dynamics.manage-multilanguage-development.3-automate", + "learn-dynamics.manage-multilanguage-development.4-exercise", + "learn-dynamics.manage-multilanguage-development.5-check", + "learn-dynamics.manage-multilanguage-development.6-summary" + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code", + "title": "Manipulate data by using code in Dynamics 365 Business Central", + "summary": "Do you want to know how to retrieve data from the database and manipulate that data or add new data? In this module, you learn about the different AL data manipulation statements.", + "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 77, + "levels": [ + "intermediate" + ], + "subjects": [ + "data-engineering", + "data-management" + ], + "learningPathUids": "learn-dynamics.application-foundation-al-language", + "unitUids": [ + "learn-dynamics.manipulate-data-via-code.1-introduction", + "learn-dynamics.manipulate-data-via-code.2-retrieve-data", + "learn-dynamics.manipulate-data-via-code.3-sort-filter", + "learn-dynamics.manipulate-data-via-code.4-add-modify-delete", + "learn-dynamics.manipulate-data-via-code.5-field-functions", + "learn-dynamics.manipulate-data-via-code.control-database-locking-behavior", + "learn-dynamics.manipulate-data-via-code.6-exercise", + "learn-dynamics.manipulate-data-via-code.7-exercise", + "learn-dynamics.manipulate-data-via-code.8-check", + "learn-dynamics.manipulate-data-via-code.9-summary" + ] + }, + { + "uid": "learn-dynamics.report-extensibility", + "title": "Extend reports in Dynamics 365 Business Central", + "summary": "Do you want to learn how to extend reports in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to extend reports.", + "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 53, + "levels": [ + "intermediate" + ], + "subjects": [ + "finance-accounting" + ], + "learningPathUids": "learn-dynamics.build-reports", + "unitUids": [ + "learn-dynamics.report-extensibility.1-introduction", + "learn-dynamics.report-extensibility.2-anatomy", + "learn-dynamics.report-extensibility.3-report-extension-example", + "learn-dynamics.report-extensibility.4-check", + "learn-dynamics.report-extensibility.5-summary" + ] + }, + { + "uid": "learn-dynamics.send-notifications", + "title": "Send notifications in Dynamics 365 Business Central", + "summary": "Discover how to create, send, and enhance user interactions through notifications in this comprehensive guide in Dynamics 365 Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 36, + "levels": [ + "intermediate" + ], + "subjects": [ + "collaboration", + "communication" + ], + "learningPathUids": "learn-dynamics.tailor-roles-design-ui", + "unitUids": [ + "learn-dynamics.send-notifications.1-introduction", + "learn-dynamics.send-notifications.2-notifications", + "learn-dynamics.send-notifications.3-create-send", + "learn-dynamics.send-notifications.4-exercise", + "learn-dynamics.send-notifications.5-check", + "learn-dynamics.send-notifications.6-summary" + ] + }, + { + "uid": "learn-dynamics.set-up-onboarding-experiences", + "title": "Set up onboarding experiences in Dynamics 365 Business Central", + "summary": "Learn how to set up onboarding experiences in Dynamics 365 Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 56, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.tailor-roles-design-ui", + "unitUids": [ + "learn-dynamics.set-up-onboarding-experiences.introduction", + "learn-dynamics.set-up-onboarding-experiences.onboard-new-users", + "learn-dynamics.set-up-onboarding-experiences.get-users-started-checklist", + "learn-dynamics.set-up-onboarding-experiences.implement-tips-tour", + "learn-dynamics.set-up-onboarding-experiences.check", + "learn-dynamics.set-up-onboarding-experiences.summary" + ] + }, + { + "uid": "learn-dynamics.test-automation", + "title": "Implement test automation techniques in Business Central", + "summary": "Do you want to implement test automation techniques? In this module, you learn about implementing test automation techniques and where to find useful guides and documentation.", + "url": "https://learn.microsoft.com/en-us/training/modules/test-automation", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 91, + "levels": [ + "intermediate" + ], + "subjects": [ + "process-workflow" + ], + "learningPathUids": "learn-dynamics.use-application-development-business-central", + "unitUids": [ + "learn-dynamics.test-automation.1-introduction", + "learn-dynamics.test-automation.2-test-automation-responsibilities", + "learn-dynamics.test-automation.3-documentation-examples", + "learn-dynamics.test-automation.page-scripting-tool", + "learn-dynamics.test-automation.4-answers-about-testing", + "learn-dynamics.test-automation.5-check", + "learn-dynamics.test-automation.6-summary" + ] + }, + { + "uid": "learn-dynamics.understand-report-design-process", + "title": "Use the report design process in Dynamics 365 Business Central", + "summary": "Do you want to learn about the report design process for Microsoft Dynamics 365 Business Central? If so, this module will explain the different steps that are involved in creating a report.", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 24, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.build-reports", + "unitUids": [ + "learn-dynamics.understand-report-design-process.1-introduction", + "learn-dynamics.understand-report-design-process.2-report-design-process", + "learn-dynamics.understand-report-design-process.3-use-snippet-create-report", + "learn-dynamics.understand-report-design-process.4-check", + "learn-dynamics.understand-report-design-process.5-summary" + ] + }, + { + "uid": "learn-dynamics.understand-report-triggers-functions", + "title": "Use report triggers, functions, and processing-only reports in Dynamics 365 Business Central", + "summary": "Do you want to learn how to work with report triggers and functions in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to create processing-only reports and how to use the report triggers and functions.", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions", + "lastModified": "2026-08-27T23:37:00Z", + "durationInMinutes": 56, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.build-reports", + "unitUids": [ + "learn-dynamics.understand-report-triggers-functions.1-introduction", + "learn-dynamics.understand-report-triggers-functions.2-understand-report-trigger-execution", + "learn-dynamics.understand-report-triggers-functions.3-use-report-functions", + "learn-dynamics.understand-report-triggers-functions.4-create-processing-only-report", + "learn-dynamics.understand-report-triggers-functions.4a-adding-barcodes", + "learn-dynamics.understand-report-triggers-functions.5-exercise", + "learn-dynamics.understand-report-triggers-functions.6-check", + "learn-dynamics.understand-report-triggers-functions.7-summary" + ] + }, + { + "uid": "learn-dynamics.use-al-controlled-barcode-scanning", + "title": "Use AL-controlled barcode scanning in Dynamics 365 Business Central", + "summary": "Learn how Business Central provides barcode scanning capabilities in the mobile application and AL language.", + "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning", + "lastModified": "2026-08-20T23:07:00Z", + "durationInMinutes": 35, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.interface-with-business-central", + "unitUids": [ + "learn-dynamics.use-al-controlled-barcode-scanning.introduction", + "learn-dynamics.use-al-controlled-barcode-scanning.scenarios-barcodes", + "learn-dynamics.use-al-controlled-barcode-scanning.add-button", + "learn-dynamics.use-al-controlled-barcode-scanning.invoke-scanning", + "learn-dynamics.use-al-controlled-barcode-scanning.integrate-scanners", + "learn-dynamics.use-al-controlled-barcode-scanning.check", + "learn-dynamics.use-al-controlled-barcode-scanning.summary" + ] + }, + { + "uid": "learn-dynamics.use-application-lifecycle", + "title": "Use Application Lifecycle Management for Business Central", + "summary": "Do you want to know how to start implementing a version control system to manage your Business Central applications source code? With Azure DevOps you can do that, and much more. You'll learn about the features of the application lifecycle management tool Azure DevOps.", + "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle", + "lastModified": "2026-08-28T23:11:00Z", + "durationInMinutes": 47, + "levels": [ + "intermediate" + ], + "subjects": [ + "devops" + ], + "learningPathUids": "learn-dynamics.continuous-integration", + "unitUids": [ + "learn-dynamics.use-application-lifecycle.1-introduction", + "learn-dynamics.use-application-lifecycle.2-create-azure-devops", + "learn-dynamics.use-application-lifecycle.3-manage-azure-devops", + "learn-dynamics.use-application-lifecycle.4-introduction-devops-services", + "learn-dynamics.use-application-lifecycle.5-connect-devops-tokens", + "learn-dynamics.use-application-lifecycle.6-introduction-github", + "learn-dynamics.use-application-lifecycle.7-exercise", + "learn-dynamics.use-application-lifecycle.8-check", + "learn-dynamics.use-application-lifecycle.9-summary" + ] + }, + { + "uid": "learn-dynamics.use-azure-functions", + "title": "Use Azure Functions with Dynamics 365 Business Central", + "summary": "Do you want to learn how to use Azure Functions with Business Central? This module will explain what an Azure function is and how to create one. You'll also learn how to integrate them with Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions", + "lastModified": "2026-08-17T18:23:00Z", + "durationInMinutes": 25, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.interface-with-business-central", + "unitUids": [ + "learn-dynamics.use-azure-functions.1-introduction", + "learn-dynamics.use-azure-functions.2-create-basic", + "learn-dynamics.use-azure-functions.3-create-existing", + "learn-dynamics.use-azure-functions.4-use", + "learn-dynamics.use-azure-functions.6-check", + "learn-dynamics.use-azure-functions.7-summary" + ] + }, + { + "uid": "learn-dynamics.use-azure-pipelines", + "title": "Use Azure Pipelines for CI/CD with Business Central", + "summary": "Learn how to configure and set up an automated build process, create pipelines using YAML files, and set up a release process in Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 46, + "levels": [ + "intermediate" + ], + "subjects": [ + "process-workflow" + ], + "learningPathUids": "learn-dynamics.continuous-integration", + "unitUids": [ + "learn-dynamics.use-azure-pipelines.1-introduction", + "learn-dynamics.use-azure-pipelines.2-what-continuous-integration", + "learn-dynamics.use-azure-pipelines.3-configure-build-agents", + "learn-dynamics.use-azure-pipelines.4-create-build-pipeline", + "learn-dynamics.use-azure-pipelines.5-work-yaml-files", + "learn-dynamics.use-azure-pipelines.6-create-release-pipeline", + "learn-dynamics.use-azure-pipelines.7-exercise", + "learn-dynamics.use-azure-pipelines.8-check", + "learn-dynamics.use-azure-pipelines.9-summary" + ] + }, + { + "uid": "learn-dynamics.use-branch-merge-git", + "title": "Use branching and merging with Git for Business Central", + "summary": "Do you want to work with different branches in Git to separate your development code from the code in production? In this module we'll discuss how you can work with branches in Git and merge your modifications.", + "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 64, + "levels": [ + "intermediate" + ], + "subjects": [ + "devops" + ], + "learningPathUids": "learn-dynamics.continuous-integration", + "unitUids": [ + "learn-dynamics.use-branch-merge-git.1-introduction", + "learn-dynamics.use-branch-merge-git.2-branch-commands", + "learn-dynamics.use-branch-merge-git.3-use-rebase", + "learn-dynamics.use-branch-merge-git.4-set-branch-policies", + "learn-dynamics.use-branch-merge-git.5-use-pull-requests", + "learn-dynamics.use-branch-merge-git.6-work-branch-strategies", + "learn-dynamics.use-branch-merge-git.7-exercise", + "learn-dynamics.use-branch-merge-git.8-check", + "learn-dynamics.use-branch-merge-git.9-summary" + ] + }, + { + "uid": "learn-dynamics.use-docker-business-central", + "title": "Use Docker in your Business Central development process", + "summary": "Do you want to know how you can benefit from working with Docker and Microsoft Dynamics 365 Business Central? Docker offers images for every released version of Business Central in every available country or region.", + "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central", + "lastModified": "2026-08-13T17:20:00Z", + "durationInMinutes": 68, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.continuous-integration", + "unitUids": [ + "learn-dynamics.use-docker-business-central.1-introduction", + "learn-dynamics.use-docker-business-central.2-docker-images-containers", + "learn-dynamics.use-docker-business-central.3-install-docker-windows", + "learn-dynamics.use-docker-business-central.4-process-isolation-hyper-v", + "learn-dynamics.use-docker-business-central.5-basic-docker-commands", + "learn-dynamics.use-docker-business-central.6-use-bc-container-helper", + "learn-dynamics.use-docker-business-central.7-exercise", + "learn-dynamics.use-docker-business-central.8-check", + "learn-dynamics.use-docker-business-central.9-summary" + ] + }, + { + "uid": "learn-dynamics.use-document-standards-business-central", + "title": "Use document standards in Dynamics 365 Business Central", + "summary": "Do you plan to develop solutions involving documents? In this module, you learn how to work with document data and about document standards in Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 57, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.essential-development-standards", + "unitUids": [ + "learn-dynamics.use-document-standards-business-central.1-introduction", + "learn-dynamics.use-document-standards-business-central.2-create-document-page", + "learn-dynamics.use-document-standards-business-central.3-use-initrecord-function", + "learn-dynamics.use-document-standards-business-central.4-use-testfield-function", + "learn-dynamics.use-document-standards-business-central.4a-use-round-function", + "learn-dynamics.use-document-standards-business-central.4b-exercise", + "learn-dynamics.use-document-standards-business-central.5-check", + "learn-dynamics.use-document-standards-business-central.6-summary" + ] + }, + { + "uid": "learn-dynamics.use-model-driven-apps-common-data-service", + "title": "Use model-driven apps and Dataverse with Business Central", + "summary": "This module focuses on the building blocks of Dataverse and model-driven apps as well as using them together with Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 83, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.power-apps-power-automate-business-central", + "unitUids": [ + "learn-dynamics.use-model-driven-apps-common-data-service.1-introduction", + "learn-dynamics.use-model-driven-apps-common-data-service.2-entities-fields-common-data-service", + "learn-dynamics.use-model-driven-apps-common-data-service.3-create-entity-import-data", + "learn-dynamics.use-model-driven-apps-common-data-service.4-model-driven-apps", + "learn-dynamics.use-model-driven-apps-common-data-service.5-user-interface-logic", + "learn-dynamics.use-model-driven-apps-common-data-service.6-create-model-driven-app", + "learn-dynamics.use-model-driven-apps-common-data-service.7-exercise", + "learn-dynamics.use-model-driven-apps-common-data-service.8-check", + "learn-dynamics.use-model-driven-apps-common-data-service.9-summary" + ] + }, + { + "uid": "learn-dynamics.use-power-automate", + "title": "Use Power Automate with Business Central", + "summary": "Discover the power of Power Automate in Business Central and explore the available actions and triggers.", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 128, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.power-apps-power-automate-business-central", + "unitUids": [ + "learn-dynamics.use-power-automate.1-introduction", + "learn-dynamics.use-power-automate.2-triggers", + "learn-dynamics.use-power-automate.3-create-power-automate", + "learn-dynamics.use-power-automate.4-item-approvals", + "learn-dynamics.use-power-automate.5-general-journal-approvals", + "learn-dynamics.use-power-automate.6-document-approvals", + "learn-dynamics.use-power-automate.7-business-scenarios", + "learn-dynamics.use-power-automate.8-create-button", + "learn-dynamics.use-power-automate.9-schedule", + "learn-dynamics.use-power-automate.10-troubleshoot", + "learn-dynamics.use-power-automate.11-exercise", + "learn-dynamics.use-power-automate.12-check", + "learn-dynamics.use-power-automate.13-summary" + ] + }, + { + "uid": "learn-dynamics.work-azure-boards", + "title": "Work with Azure Boards for project management in Business Central", + "summary": "Learn to manage development tasks, track project progress, assign work items and integrate Microsoft Teams with Azure DevOps in Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards", + "lastModified": "2026-08-11T23:09:00Z", + "durationInMinutes": 62, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.continuous-integration", + "unitUids": [ + "learn-dynamics.work-azure-boards.1-introduction", + "learn-dynamics.work-azure-boards.2-work-process-templates", + "learn-dynamics.work-azure-boards.3-configure-iterations", + "learn-dynamics.work-azure-boards.4-create-work-items", + "learn-dynamics.work-azure-boards.5-project-dashboards-wikis", + "learn-dynamics.work-azure-boards.6-teams-integrate-devops", + "learn-dynamics.work-azure-boards.7-exercise", + "learn-dynamics.work-azure-boards.8-check", + "learn-dynamics.work-azure-boards.9-summary" + ] + }, + { + "uid": "learn-dynamics.work-entitlements-permission-sets", + "title": "Work with entitlements and permission sets in Dynamics 365 Business Central", + "summary": "Learn about entitlements and permission sets and how to extend them in Microsoft Dynamics 365 Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets", + "lastModified": "2026-09-03T23:14:00Z", + "durationInMinutes": 43, + "levels": [ + "intermediate" + ], + "subjects": [ + "identity-access" + ], + "learningPathUids": "learn-dynamics.foundation-customize-business-central", + "unitUids": [ + "learn-dynamics.work-entitlements-permission-sets.1-introduction", + "learn-dynamics.work-entitlements-permission-sets.2-get-started", + "learn-dynamics.work-entitlements-permission-sets.3-create-entitlements-permission-sets", + "learn-dynamics.work-entitlements-permission-sets.override-entitlements", + "learn-dynamics.work-entitlements-permission-sets.4-check", + "learn-dynamics.work-entitlements-permission-sets.5-summary" + ] + }, + { + "uid": "learn-dynamics.work-power-bi", + "title": "Work with Power BI in Business Central", + "summary": "Do you want to know how to work with Microsoft Power BI in Dynamics 365 Business Central? If so, this module is for you. You'll learn how to deploy Business Central dashboards in Power BI and how to set up Power BI inside Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 65, + "levels": [ + "intermediate" + ], + "subjects": [ + "business-applications" + ], + "learningPathUids": "learn-dynamics.use-power-bi", + "unitUids": [ + "learn-dynamics.work-power-bi.1-introduction", + "learn-dynamics.work-power-bi.2-deploy-dashboards", + "learn-dynamics.work-power-bi.3-set-up-power-bi", + "learn-dynamics.work-power-bi.4-view-list-data", + "learn-dynamics.work-power-bi.add-report-parts", + "learn-dynamics.work-power-bi.5-exercise", + "learn-dynamics.work-power-bi.6-check", + "learn-dynamics.work-power-bi.7-summary" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git", + "title": "Work with source control using Git in Visual Studio Code for Business Central", + "summary": "Explore how to work with local and remote Git repositories, including configuring Git and creating new repositories in Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 94, + "levels": [ + "intermediate" + ], + "subjects": [ + "process-workflow" + ], + "learningPathUids": "learn-dynamics.continuous-integration", + "unitUids": [ + "learn-dynamics.work-source-control-git.1-introduction", + "learn-dynamics.work-source-control-git.2-configure-git", + "learn-dynamics.work-source-control-git.3-local-remote-repositories", + "learn-dynamics.work-source-control-git.4-create-local-git", + "learn-dynamics.work-source-control-git.5-use-git-ignore", + "learn-dynamics.work-source-control-git.6-add-remove-files", + "learn-dynamics.work-source-control-git.7-get-status-repository", + "learn-dynamics.work-source-control-git.8-use-remote-git", + "learn-dynamics.work-source-control-git.9-clone-remote-git", + "learn-dynamics.work-source-control-git.10-work-remote-git", + "learn-dynamics.work-source-control-git.11-exercise", + "learn-dynamics.work-source-control-git.12-check", + "learn-dynamics.work-source-control-git.13-summary" + ] + }, + { + "uid": "learn-dynamics.work-with-api", + "title": "Work with the API in Dynamics 365 Business Central", + "summary": "Learn how to work with the API in Dynamics Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 43, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.interface-with-business-central", + "unitUids": [ + "learn-dynamics.work-with-api.1-introduction", + "learn-dynamics.work-with-api.2-access", + "learn-dynamics.work-with-api.3-tips", + "learn-dynamics.work-with-api.4-limits", + "learn-dynamics.work-with-api.5-create-new", + "learn-dynamics.work-with-api.6-read", + "learn-dynamics.work-with-api.7-bound-actions", + "learn-dynamics.work-with-api.9-check", + "learn-dynamics.work-with-api.10-summary" + ] + }, + { + "uid": "learn-dynamics.work-with-codeunits", + "title": "Work with codeunits in Dynamics 365 Business Central", + "summary": "Do you want to create your own codeunit and learn how to access the functions within that codeunit? If so, this module is the ideal place to start.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 26, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.application-foundation-al-language", + "unitUids": [ + "learn-dynamics.work-with-codeunits.1-introduction", + "learn-dynamics.work-with-codeunits.2-create-codeunit", + "learn-dynamics.work-with-codeunits.keyword", + "learn-dynamics.work-with-codeunits.3-check", + "learn-dynamics.work-with-codeunits.4-summary" + ] + }, + { + "uid": "learn-dynamics.work-with-master-data-business-central", + "title": "Work with master data in Dynamics 365 Business Central", + "summary": "Learn to enter or import master data into Dynamics 365 Business Central, including creating tables, implementing design patterns, and formatting fields.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central", + "lastModified": "2026-08-18T23:35:00Z", + "durationInMinutes": 61, + "levels": [ + "intermediate" + ], + "subjects": [ + "data-engineering", + "data-management" + ], + "learningPathUids": "learn-dynamics.essential-development-standards", + "unitUids": [ + "learn-dynamics.work-with-master-data-business-central.1-introduction", + "learn-dynamics.work-with-master-data-business-central.2-setup-table-signature-design", + "learn-dynamics.work-with-master-data-business-central.3-master-table-page-standards", + "learn-dynamics.work-with-master-data-business-central.4-implement-series-pattern", + "learn-dynamics.work-with-master-data-business-central.5-use-blocked-field", + "learn-dynamics.work-with-master-data-business-central.6-frequently-used-table-properties", + "learn-dynamics.work-with-master-data-business-central.6a-exercise", + "learn-dynamics.work-with-master-data-business-central.7-check", + "learn-dynamics.work-with-master-data-business-central.8-summary" + ] + }, + { + "uid": "learn-dynamics.work-with-pages", + "title": "Work with pages in Dynamics 365 Business Central", + "summary": "Do you want to create new pages in Business Central that have the same appearance as existing pages? This module focuses on how to create new pages. It will also show you how to enable users to search for specific pages.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 113, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.foundation-customize-business-central", + "unitUids": [ + "learn-dynamics.work-with-pages.1-introduction", + "learn-dynamics.work-with-pages.2-get-started", + "learn-dynamics.work-with-pages.3-types", + "learn-dynamics.work-with-pages.4-properties", + "learn-dynamics.work-with-pages.5-layout", + "learn-dynamics.work-with-pages.6-link", + "learn-dynamics.work-with-pages.7-snippets", + "learn-dynamics.work-with-pages.8-controls", + "learn-dynamics.work-with-pages.use-rich-text-editor", + "learn-dynamics.work-with-pages.scan-barcodes", + "learn-dynamics.work-with-pages.9-search", + "learn-dynamics.work-with-pages.10-actions", + "learn-dynamics.work-with-pages.hidden-fields", + "learn-dynamics.work-with-pages.11-exercise", + "learn-dynamics.work-with-pages.12-exercise", + "learn-dynamics.work-with-pages.13-check", + "learn-dynamics.work-with-pages.14-summary" + ] + }, + { + "uid": "learn-dynamics.work-with-queries", + "title": "Work with queries in Dynamics 365 Business Central", + "summary": "Learn how to work with the Query object in Dynamics 365 Business Central to join, filter, and aggregate data.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 79, + "levels": [ + "intermediate" + ], + "subjects": [ + "data-analytics" + ], + "learningPathUids": "learn-dynamics.data-management-foundation-business-central", + "unitUids": [ + "learn-dynamics.work-with-queries.1-introduction", + "learn-dynamics.work-with-queries.2-get-started", + "learn-dynamics.work-with-queries.3-create", + "learn-dynamics.work-with-queries.4-join-data", + "learn-dynamics.work-with-queries.5-filter-data", + "learn-dynamics.work-with-queries.6-aggregate-data", + "learn-dynamics.work-with-queries.7-al-function", + "learn-dynamics.work-with-queries.8-advanced-concepts", + "learn-dynamics.work-with-queries.optimize-performance", + "learn-dynamics.work-with-queries.9-check", + "learn-dynamics.work-with-queries.10-summary" + ] + }, + { + "uid": "learn-dynamics.work-with-reports", + "title": "Work with reports in Dynamics 365 Business Central", + "summary": "Do you want to learn how to work with reports in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to work with reports and the difference between preview, run, email, and print.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports", + "lastModified": "2026-08-20T17:11:00Z", + "durationInMinutes": 59, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.build-reports", + "unitUids": [ + "learn-dynamics.work-with-reports.1-introduction", + "learn-dynamics.work-with-reports.2-preview-run", + "learn-dynamics.work-with-reports.set-layout", + "learn-dynamics.work-with-reports.3-printers", + "learn-dynamics.work-with-reports.4-check", + "learn-dynamics.work-with-reports.5-summary" + ] + }, + { + "uid": "learn-dynamics.work-with-tables", + "title": "Work with tables in Dynamics 365 Business Central", + "summary": "Do you want to know how to create new tables in Business Central? If so, this module is for you. This module focuses on the different table types in Business Central and show you how to create new tables. Additionally, you learn how to use Visual Studio Code snippets to create a table, fields, and keys.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 99, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.foundation-customize-business-central", + "unitUids": [ + "learn-dynamics.work-with-tables.1-introduction", + "learn-dynamics.work-with-tables.2-get-started", + "learn-dynamics.work-with-tables.3-types", + "learn-dynamics.work-with-tables.4-properties", + "learn-dynamics.work-with-tables.5-fields", + "learn-dynamics.work-with-tables.6-linking", + "learn-dynamics.work-with-tables.7-keys", + "learn-dynamics.work-with-tables.8-dropdown", + "learn-dynamics.work-with-tables.text-search", + "learn-dynamics.work-with-tables.9-calculated", + "learn-dynamics.work-with-tables.10-snippets", + "learn-dynamics.work-with-tables.11-exercise", + "learn-dynamics.work-with-tables.12-check", + "learn-dynamics.work-with-tables.13-summary" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services", + "title": "Work with web services in Dynamics 365 Business Central", + "summary": "Use SOAP and OData to read and update records, and handle UI interaction in Business Central.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 67, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.interface-with-business-central", + "unitUids": [ + "learn-dynamics.work-with-web-services.1-introduction", + "learn-dynamics.work-with-web-services.2-differences", + "learn-dynamics.work-with-web-services.3-authentication", + "learn-dynamics.work-with-web-services.4-enable-access", + "learn-dynamics.work-with-web-services.5-return-documents", + "learn-dynamics.work-with-web-services.6-request-records-odata", + "learn-dynamics.work-with-web-services.7-query-options", + "learn-dynamics.work-with-web-services.8-new-records", + "learn-dynamics.work-with-web-services.9-update-records", + "learn-dynamics.work-with-web-services.10-use-soap", + "learn-dynamics.work-with-web-services.11-request-records-soap", + "learn-dynamics.work-with-web-services.12-ui-interaction", + "learn-dynamics.work-with-web-services.14-check", + "learn-dynamics.work-with-web-services.15-summary" + ] + }, + { + "uid": "learn-dynamics.work-with-xmlports", + "title": "Work with XMLports in Dynamics 365 Business Central", + "summary": "Learn how to define and use XMLports in AL, understand different nodes and properties, and apply them in AL code.", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 65, + "levels": [ + "intermediate" + ], + "subjects": [], + "learningPathUids": "learn-dynamics.data-management-foundation-business-central", + "unitUids": [ + "learn-dynamics.work-with-xmlports.1-introduction", + "learn-dynamics.work-with-xmlports.2-get-started", + "learn-dynamics.work-with-xmlports.3-configure", + "learn-dynamics.work-with-xmlports.4-define-nodes", + "learn-dynamics.work-with-xmlports.5-design", + "learn-dynamics.work-with-xmlports.6-create", + "learn-dynamics.work-with-xmlports.7-al-functions", + "learn-dynamics.work-with-xmlports.8-exercise", + "learn-dynamics.work-with-xmlports.9-check", + "learn-dynamics.work-with-xmlports.10-summary" + ] + } + ], + "units": [ + { + "uid": "learn-dynamics.access-rest-services.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.access-rest-services" + ] + }, + { + "uid": "learn-dynamics.access-rest-services.2-http-classes", + "title": "Use the different HTTP classes and their functions", + "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/2-http-classes", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.access-rest-services" + ] + }, + { + "uid": "learn-dynamics.access-rest-services.3-get-send-data", + "title": "Get data from and send data to an external REST service", + "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/3-get-send-data", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.access-rest-services" + ] + }, + { + "uid": "learn-dynamics.access-rest-services.4-use-read-json", + "title": "Use and read JSON in Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/4-use-read-json", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.access-rest-services" + ] + }, + { + "uid": "learn-dynamics.access-rest-services.5-get-json-data", + "title": "Get JSON data from external REST services", + "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/5-get-json-data", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.access-rest-services" + ] + }, + { + "uid": "learn-dynamics.access-rest-services.6-exercise", + "title": "Exercise - Connect to external REST services", + "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/6-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.access-rest-services" + ] + }, + { + "uid": "learn-dynamics.access-rest-services.7-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/7-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.access-rest-services" + ] + }, + { + "uid": "learn-dynamics.access-rest-services.8-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/8-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.access-rest-services" + ] + }, + { + "uid": "learn-dynamics.add-layout-report.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.add-layout-report" + ] + }, + { + "uid": "learn-dynamics.add-layout-report.2-add-report-definition", + "title": "Add an RDLC layout to a report", + "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/2-add-report-definition", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.add-layout-report" + ] + }, + { + "uid": "learn-dynamics.add-layout-report.3-add-word-layout", + "title": "Add a Word layout to a report", + "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/3-add-word-layout", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.add-layout-report" + ] + }, + { + "uid": "learn-dynamics.add-layout-report.4-exercise", + "title": "Exercise - Create a basic report", + "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/4-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.add-layout-report" + ] + }, + { + "uid": "learn-dynamics.add-layout-report.5-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/5-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.add-layout-report" + ] + }, + { + "uid": "learn-dynamics.add-layout-report.6-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/6-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.add-layout-report" + ] + }, + { + "uid": "learn-dynamics.add-layout-report.add-excel-layout", + "title": "Add an Excel layout to a report", + "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/add-excel-layout", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.add-layout-report" + ] + }, + { + "uid": "learn-dynamics.add-layout-report.word-add-in", + "title": "Use the Word add-in for report layouts", + "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/word-add-in", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.add-layout-report" + ] + }, + { + "uid": "learn-dynamics.add-new-objects.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/1-introduction", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.add-new-objects" + ] + }, + { + "uid": "learn-dynamics.add-new-objects.10-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/10-summary", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.add-new-objects" + ] + }, + { + "uid": "learn-dynamics.add-new-objects.2-reward", + "title": "Create the reward level table object", + "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/2-reward", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.add-new-objects" + ] + }, + { + "uid": "learn-dynamics.add-new-objects.3-activation-code", + "title": "Create the activation code information table object", + "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/3-activation-code", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.add-new-objects" + ] + }, + { + "uid": "learn-dynamics.add-new-objects.4-customer-rewards", + "title": "Create the Customer Rewards Management Setup table object", + "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/4-customer-rewards", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.add-new-objects" + ] + }, + { + "uid": "learn-dynamics.add-new-objects.5-reward-list", + "title": "Create the Rewards Level List page object", + "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/5-reward-list", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.add-new-objects" + ] + }, + { + "uid": "learn-dynamics.add-new-objects.6-exercise", + "title": "Exercise - Create the Activation Code Information table object", + "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/6-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.add-new-objects" + ] + }, + { + "uid": "learn-dynamics.add-new-objects.7-exercise", + "title": "Exercise - Create the Customer Rewards Mgt Setup table object", + "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/7-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.add-new-objects" + ] + }, + { + "uid": "learn-dynamics.add-new-objects.8-exercise", + "title": "Exercise - Create the Rewards Level List page object", + "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/8-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.add-new-objects" + ] + }, + { + "uid": "learn-dynamics.add-new-objects.9-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/9-check", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.add-new-objects" + ] + }, + { + "uid": "learn-dynamics.add-reports-tell-me.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.add-reports-tell-me" + ] + }, + { + "uid": "learn-dynamics.add-reports-tell-me.2-add-reports-tell-me", + "title": "Add reports to Tell Me", + "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me/2-add-reports-tell-me", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.add-reports-tell-me" + ] + }, + { + "uid": "learn-dynamics.add-reports-tell-me.3-substitute-report", + "title": "Substitute a report", + "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me/3-substitute-report", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.add-reports-tell-me" + ] + }, + { + "uid": "learn-dynamics.add-reports-tell-me.4-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me/4-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.add-reports-tell-me" + ] + }, + { + "uid": "learn-dynamics.add-reports-tell-me.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me/5-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.add-reports-tell-me" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.10-exercise", + "title": "Exercise - Use built-in functions", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/10-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.11-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/11-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.12-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/12-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.2-interaction-functions", + "title": "Interaction methods", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/2-interaction-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.3-string-functions", + "title": "String functions", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/3-string-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.4-date-functions", + "title": "Date functions", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/4-date-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.5-numeric-functions", + "title": "Numeric functions", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/5-numeric-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.6-array-functions", + "title": "Array functions", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/6-array-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.7-list-functions", + "title": "List functions", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/7-list-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.8-system-functions", + "title": "System functions", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/8-system-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-built-in-functions.9-variable-functions", + "title": "Variable functions", + "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/9-variable-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.al-built-in-functions" + ] + }, + { + "uid": "learn-dynamics.al-statements.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.al-statements" + ] + }, + { + "uid": "learn-dynamics.al-statements.2-compound", + "title": "Work with comment and compound statements", + "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/2-compound", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.al-statements" + ] + }, + { + "uid": "learn-dynamics.al-statements.3-conditional", + "title": "Work with conditional statements", + "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/3-conditional", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.al-statements" + ] + }, + { + "uid": "learn-dynamics.al-statements.4-repetitive", + "title": "Use repetitive statements", + "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/4-repetitive", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 18, + "moduleUids": [ + "learn-dynamics.al-statements" + ] + }, + { + "uid": "learn-dynamics.al-statements.5-exercise", + "title": "Exercise - Use conditional and compound statements", + "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/5-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.al-statements" + ] + }, + { + "uid": "learn-dynamics.al-statements.6-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/6-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.al-statements" + ] + }, + { + "uid": "learn-dynamics.al-statements.7-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/7-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.al-statements" + ] + }, + { + "uid": "learn-dynamics.application-monetization.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.application-monetization" + ] + }, + { + "uid": "learn-dynamics.application-monetization.2-responsibilities", + "title": "Understand application monetization responsibilities", + "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization/2-responsibilities", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.application-monetization" + ] + }, + { + "uid": "learn-dynamics.application-monetization.3-documentation-examples", + "title": "Find application monetization documentation and examples", + "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization/3-documentation-examples", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.application-monetization" + ] + }, + { + "uid": "learn-dynamics.application-monetization.4-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization/4-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.application-monetization" + ] + }, + { + "uid": "learn-dynamics.application-monetization.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization/5-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.application-monetization" + ] + }, + { + "uid": "learn-dynamics.application-performance-monitor.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor/1-introduction", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.application-performance-monitor" + ] + }, + { + "uid": "learn-dynamics.application-performance-monitor.2-prepare-application", + "title": "Prepare your application for optimal performance", + "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor/2-prepare-application", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.application-performance-monitor" + ] + }, + { + "uid": "learn-dynamics.application-performance-monitor.3-get-insights", + "title": "Get essential Application Insights", + "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor/3-get-insights", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 20, + "moduleUids": [ + "learn-dynamics.application-performance-monitor" + ] + }, + { + "uid": "learn-dynamics.application-performance-monitor.4-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor/4-check", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.application-performance-monitor" + ] + }, + { + "uid": "learn-dynamics.application-performance-monitor.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor/5-summary", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.application-performance-monitor" + ] + }, + { + "uid": "learn-dynamics.application-types.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/application-types/1-introduction", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.application-types" + ] + }, + { + "uid": "learn-dynamics.application-types.2-connect-vs-add-on", + "title": "Understand the difference between connect and add-on applications", + "url": "https://learn.microsoft.com/en-us/training/modules/application-types/2-connect-vs-add-on", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.application-types" + ] + }, + { + "uid": "learn-dynamics.application-types.3-library-dependency-applications", + "title": "Understand library and dependency applications", + "url": "https://learn.microsoft.com/en-us/training/modules/application-types/3-library-dependency-applications", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.application-types" + ] + }, + { + "uid": "learn-dynamics.application-types.4-find-development-resources", + "title": "Find development resources for building applications", + "url": "https://learn.microsoft.com/en-us/training/modules/application-types/4-find-development-resources", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 11, + "moduleUids": [ + "learn-dynamics.application-types" + ] + }, + { + "uid": "learn-dynamics.application-types.5-set-up-code-cop-tools", + "title": "Learn how to properly set up code cop tools for app validation", + "url": "https://learn.microsoft.com/en-us/training/modules/application-types/5-set-up-code-cop-tools", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 20, + "moduleUids": [ + "learn-dynamics.application-types" + ] + }, + { + "uid": "learn-dynamics.application-types.6-check", + "title": "Module assessment", + "url": "https://learn.microsoft.com/en-us/training/modules/application-types/6-check", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.application-types" + ] + }, + { + "uid": "learn-dynamics.application-types.7-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/application-types/7-summary", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.application-types" + ] + }, + { + "uid": "learn-dynamics.assisted-setup-tooltips.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/1-introduction", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.assisted-setup-tooltips" + ] + }, + { + "uid": "learn-dynamics.assisted-setup-tooltips.2-rewards-wizard", + "title": "Create the Customer Rewards Wizard page object", + "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/2-rewards-wizard", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.assisted-setup-tooltips" + ] + }, + { + "uid": "learn-dynamics.assisted-setup-tooltips.3-assisted-setup", + "title": "Add the Customer Rewards Wizard page to the assisted setup", + "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/3-assisted-setup", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.assisted-setup-tooltips" + ] + }, + { + "uid": "learn-dynamics.assisted-setup-tooltips.4-tooltips", + "title": "Add tooltips to your extension", + "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/4-tooltips", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.assisted-setup-tooltips" + ] + }, + { + "uid": "learn-dynamics.assisted-setup-tooltips.5-exercise", + "title": "Exercise - Create the Customer Rewards Wizard page object", + "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/5-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.assisted-setup-tooltips" + ] + }, + { + "uid": "learn-dynamics.assisted-setup-tooltips.6-exercise", + "title": "Exercise - Add the Customer Rewards Wizard page to the assisted setup", + "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/6-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.assisted-setup-tooltips" + ] + }, + { + "uid": "learn-dynamics.assisted-setup-tooltips.7-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/7-check", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.assisted-setup-tooltips" + ] + }, + { + "uid": "learn-dynamics.assisted-setup-tooltips.8-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/8-summary", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.assisted-setup-tooltips" + ] + }, + { + "uid": "learn-dynamics.bring-app-appsource.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/1-introduction", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.bring-app-appsource" + ] + }, + { + "uid": "learn-dynamics.bring-app-appsource.2-get-accounts", + "title": "Get your accounts in place", + "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/2-get-accounts", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.bring-app-appsource" + ] + }, + { + "uid": "learn-dynamics.bring-app-appsource.3-marketing-validation", + "title": "Understand the marketing validation requirements", + "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/3-marketing-validation", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.bring-app-appsource" + ] + }, + { + "uid": "learn-dynamics.bring-app-appsource.4-technical-validation", + "title": "Understand the technical validation requirements", + "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/4-technical-validation", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.bring-app-appsource" + ] + }, + { + "uid": "learn-dynamics.bring-app-appsource.5-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/5-check", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.bring-app-appsource" + ] + }, + { + "uid": "learn-dynamics.bring-app-appsource.6-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/6-summary", + "lastModified": "2026-08-26T23:12:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.bring-app-appsource" + ] + }, + { + "uid": "learn-dynamics.build-assisted-setup.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.build-assisted-setup" + ] + }, + { + "uid": "learn-dynamics.build-assisted-setup.2-assisted-setup", + "title": "Use Assisted Setup", + "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/2-assisted-setup", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.build-assisted-setup" + ] + }, + { + "uid": "learn-dynamics.build-assisted-setup.3-wizard-pages", + "title": "Create a wizard page", + "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/3-wizard-pages", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 18, + "moduleUids": [ + "learn-dynamics.build-assisted-setup" + ] + }, + { + "uid": "learn-dynamics.build-assisted-setup.4-extensions", + "title": "Integrate with Assisted Setup in your extension", + "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/4-extensions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.build-assisted-setup" + ] + }, + { + "uid": "learn-dynamics.build-assisted-setup.5-exercise", + "title": "Exercise - Build a wizard page with Assisted Setup", + "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/5-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.build-assisted-setup" + ] + }, + { + "uid": "learn-dynamics.build-assisted-setup.6-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/6-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.build-assisted-setup" + ] + }, + { + "uid": "learn-dynamics.build-assisted-setup.7-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/7-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.build-assisted-setup" + ] + }, + { + "uid": "learn-dynamics.build-control-add-ins.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.build-control-add-ins" + ] + }, + { + "uid": "learn-dynamics.build-control-add-ins.2-apply-look-feel", + "title": "Apply the Business Central look and feel", + "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/2-apply-look-feel", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.build-control-add-ins" + ] + }, + { + "uid": "learn-dynamics.build-control-add-ins.3-create", + "title": "Build a control add-in for Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/3-create", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.build-control-add-ins" + ] + }, + { + "uid": "learn-dynamics.build-control-add-ins.4-asynchronous-considerations", + "title": "Asynchronous considerations for control add-ins", + "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/4-asynchronous-considerations", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.build-control-add-ins" + ] + }, + { + "uid": "learn-dynamics.build-control-add-ins.5-connect", + "title": "Connect control add-ins with Azure Functions", + "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/5-connect", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.build-control-add-ins" + ] + }, + { + "uid": "learn-dynamics.build-control-add-ins.6-exercise", + "title": "Exercise - Build a control add-in object", + "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/6-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.build-control-add-ins" + ] + }, + { + "uid": "learn-dynamics.build-control-add-ins.7-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/7-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.build-control-add-ins" + ] + }, + { + "uid": "learn-dynamics.build-control-add-ins.8-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/8-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.build-control-add-ins" + ] + }, + { + "uid": "learn-dynamics.build-power-bi-report.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/1-introduction", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.build-power-bi-report" + ] + }, + { + "uid": "learn-dynamics.build-power-bi-report.10-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/10-summary", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.build-power-bi-report" + ] + }, + { + "uid": "learn-dynamics.build-power-bi-report.2-build-data-model", + "title": "Build the data model for Power BI", + "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/2-build-data-model", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.build-power-bi-report" + ] + }, + { + "uid": "learn-dynamics.build-power-bi-report.3-add-data-dimension", + "title": "Add a date dimension", + "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/3-add-data-dimension", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.build-power-bi-report" + ] + }, + { + "uid": "learn-dynamics.build-power-bi-report.4-introduction-dax", + "title": "Introduction to DAX", + "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/4-introduction-dax", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.build-power-bi-report" + ] + }, + { + "uid": "learn-dynamics.build-power-bi-report.5-create-report", + "title": "Create a report", + "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/5-create-report", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.build-power-bi-report" + ] + }, + { + "uid": "learn-dynamics.build-power-bi-report.6-build-dashboard", + "title": "Build a dashboard", + "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/6-build-dashboard", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.build-power-bi-report" + ] + }, + { + "uid": "learn-dynamics.build-power-bi-report.7-exercise", + "title": "Exercise - Add a calculated column and measure in Power BI Desktop using DAX", + "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/7-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.build-power-bi-report" + ] + }, + { + "uid": "learn-dynamics.build-power-bi-report.8-exercise", + "title": "Exercise - Visualize your data in Power BI Desktop", + "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/8-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.build-power-bi-report" + ] + }, + { + "uid": "learn-dynamics.build-power-bi-report.9-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/9-check", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.build-power-bi-report" + ] + }, + { + "uid": "learn-dynamics.business-central-interfaces.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/1-introduction", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.interfaces" + ] + }, + { + "uid": "learn-dynamics.business-central-interfaces.2-snippet-support", + "title": "Snippet support", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/2-snippet-support", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.interfaces" + ] + }, + { + "uid": "learn-dynamics.business-central-interfaces.3-why-interfaces", + "title": "Importance of interfaces", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/3-why-interfaces", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.interfaces" + ] + }, + { + "uid": "learn-dynamics.business-central-interfaces.4-exercise", + "title": "Exercise - Create an interface", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/4-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.interfaces" + ] + }, + { + "uid": "learn-dynamics.business-central-interfaces.5-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/5-check", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.interfaces" + ] + }, + { + "uid": "learn-dynamics.business-central-interfaces.6-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/6-summary", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.interfaces" + ] + }, + { + "uid": "learn-dynamics.business-central-interfaces.extending-interfaces", + "title": "Extending interfaces in AL", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/extending-interfaces", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.interfaces" + ] + }, + { + "uid": "learn-dynamics.business-central-interfaces.type-testing", + "title": "Understand type testing and casting operators for interfaces", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/type-testing", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.interfaces" + ] + }, + { + "uid": "learn-dynamics.business-central-teams-integration.check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.business-central-teams-integration" + ] + }, + { + "uid": "learn-dynamics.business-central-teams-integration.create-tabs", + "title": "Create Business Central tabs for Teams", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/create-tabs", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.business-central-teams-integration" + ] + }, + { + "uid": "learn-dynamics.business-central-teams-integration.extend-teams-cards", + "title": "Extend Teams cards", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/extend-teams-cards", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 18, + "moduleUids": [ + "learn-dynamics.business-central-teams-integration" + ] + }, + { + "uid": "learn-dynamics.business-central-teams-integration.get-session-details", + "title": "Get relevant session and environment details for Teams integration", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/get-session-details", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.business-central-teams-integration" + ] + }, + { + "uid": "learn-dynamics.business-central-teams-integration.introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.business-central-teams-integration" + ] + }, + { + "uid": "learn-dynamics.business-central-teams-integration.manage-install-use", + "title": "Manage, install, and use the Business Central App for Teams", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/manage-install-use", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.business-central-teams-integration" + ] + }, + { + "uid": "learn-dynamics.business-central-teams-integration.summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.business-central-teams-integration" + ] + }, + { + "uid": "learn-dynamics.complete-report-request-page.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.complete-report-request-page" + ] + }, + { + "uid": "learn-dynamics.complete-report-request-page.2-design-request-page", + "title": "Design the request page", + "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page/2-design-request-page", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.complete-report-request-page" + ] + }, + { + "uid": "learn-dynamics.complete-report-request-page.3-design-request-options", + "title": "Design the request options page", + "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page/3-design-request-options", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.complete-report-request-page" + ] + }, + { + "uid": "learn-dynamics.complete-report-request-page.4-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page/4-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.complete-report-request-page" + ] + }, + { + "uid": "learn-dynamics.complete-report-request-page.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page/5-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.complete-report-request-page" + ] + }, + { + "uid": "learn-dynamics.create-canvas-app.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/1-introduction", + "lastModified": "2026-08-03T17:47:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.create-canvas-app" + ] + }, + { + "uid": "learn-dynamics.create-canvas-app.2-data-app", + "title": "Build an app using data", + "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/2-data-app", + "lastModified": "2026-08-03T17:47:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.create-canvas-app" + ] + }, + { + "uid": "learn-dynamics.create-canvas-app.3-blank-app", + "title": "Build an app from blank", + "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/3-blank-app", + "lastModified": "2026-08-03T17:47:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.create-canvas-app" + ] + }, + { + "uid": "learn-dynamics.create-canvas-app.4-customizations", + "title": "Make basic customizations in an app", + "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/4-customizations", + "lastModified": "2026-08-03T17:47:00Z", + "durationInMinutes": 14, + "moduleUids": [ + "learn-dynamics.create-canvas-app" + ] + }, + { + "uid": "learn-dynamics.create-canvas-app.5-save-publish-share", + "title": "Save, publish, and share an app", + "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/5-save-publish-share", + "lastModified": "2026-08-03T17:47:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.create-canvas-app" + ] + }, + { + "uid": "learn-dynamics.create-canvas-app.6-embed", + "title": "Embed an app", + "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/6-embed", + "lastModified": "2026-08-03T17:47:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.create-canvas-app" + ] + }, + { + "uid": "learn-dynamics.create-canvas-app.7-exercise", + "title": "Exercise - Create the customer financial details app in Power Apps", + "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/7-exercise", + "lastModified": "2026-08-03T17:47:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.create-canvas-app" + ] + }, + { + "uid": "learn-dynamics.create-canvas-app.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/8-check", + "lastModified": "2026-08-03T17:47:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.create-canvas-app" + ] + }, + { + "uid": "learn-dynamics.create-canvas-app.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/9-summary", + "lastModified": "2026-08-03T17:47:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.create-canvas-app" + ] + }, + { + "uid": "learn-dynamics.create-custom-connector-power-platform.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/1-introduction", + "lastModified": "2026-08-05T23:09:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.create-custom-connector-power-platform" + ] + }, + { + "uid": "learn-dynamics.create-custom-connector-power-platform.2-custom-connector", + "title": "Create a custom connector for Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/2-custom-connector", + "lastModified": "2026-08-05T23:09:00Z", + "durationInMinutes": 25, + "moduleUids": [ + "learn-dynamics.create-custom-connector-power-platform" + ] + }, + { + "uid": "learn-dynamics.create-custom-connector-power-platform.3-power-apps", + "title": "Use Business Central custom connector from Power Apps", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/3-power-apps", + "lastModified": "2026-08-05T23:09:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.create-custom-connector-power-platform" + ] + }, + { + "uid": "learn-dynamics.create-custom-connector-power-platform.4-power-automate", + "title": "Use a Business Central custom connector in Power Automate", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/4-power-automate", + "lastModified": "2026-08-05T23:09:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.create-custom-connector-power-platform" + ] + }, + { + "uid": "learn-dynamics.create-custom-connector-power-platform.5-share-custom-connectors", + "title": "Share Business Central custom connector across the organization", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/5-share-custom-connectors", + "lastModified": "2026-08-05T23:09:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.create-custom-connector-power-platform" + ] + }, + { + "uid": "learn-dynamics.create-custom-connector-power-platform.6-exercise", + "title": "Exercise - Create a testing process", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/6-exercise", + "lastModified": "2026-08-05T23:09:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.create-custom-connector-power-platform" + ] + }, + { + "uid": "learn-dynamics.create-custom-connector-power-platform.7-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/7-check", + "lastModified": "2026-08-05T23:09:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.create-custom-connector-power-platform" + ] + }, + { + "uid": "learn-dynamics.create-custom-connector-power-platform.8-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/8-summary", + "lastModified": "2026-08-05T23:09:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.create-custom-connector-power-platform" + ] + }, + { + "uid": "learn-dynamics.create-custom-functions.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.create-custom-functions" + ] + }, + { + "uid": "learn-dynamics.create-custom-functions.2-custom-function", + "title": "Custom procedures", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/2-custom-function", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.create-custom-functions" + ] + }, + { + "uid": "learn-dynamics.create-custom-functions.3-local-global-function", + "title": "Identify differences between local and global procedures", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/3-local-global-function", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.create-custom-functions" + ] + }, + { + "uid": "learn-dynamics.create-custom-functions.4-local-global-variable", + "title": "Identify differences between local and global variables", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/4-local-global-variable", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.create-custom-functions" + ] + }, + { + "uid": "learn-dynamics.create-custom-functions.5-pass", + "title": "Pass into a procedure", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/5-pass", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.create-custom-functions" + ] + }, + { + "uid": "learn-dynamics.create-custom-functions.6-parameter", + "title": "Identify differences between a parameter by value and by reference", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/6-parameter", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.create-custom-functions" + ] + }, + { + "uid": "learn-dynamics.create-custom-functions.7-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/7-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.create-custom-functions" + ] + }, + { + "uid": "learn-dynamics.create-custom-functions.8-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/8-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.create-custom-functions" + ] + }, + { + "uid": "learn-dynamics.create-data-set.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/1-introduction", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.10-exercise", + "title": "Exercise - Import data into Power BI Desktop", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/10-exercise", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.11-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/11-check", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.12-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/12-summary", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.2-install", + "title": "Install Power BI Desktop", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/2-install", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.3-use-data-source", + "title": "Use Business Central as a data source in Power BI", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/3-use-data-source", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.5-develop-query-page-objects", + "title": "Develop query and page objects for Power BI", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/5-develop-query-page-objects", + "lastModified": "2025-08-20T23:10:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.6-mind-performance-impact", + "title": "Weigh the performance impact", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/6-mind-performance-impact", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.7-exercise", + "title": "Exercise - Create a dataset for a Power BI report in Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/7-exercise", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.9-exercise", + "title": "Exercise - Publish and install the extension", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/9-exercise", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.create-power-bi-dataflow", + "title": "Create a Power BI dataflow with Business Central data", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/create-power-bi-dataflow", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.develop-api-query", + "title": "Develop API query and API page objects for Power BI", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/develop-api-query", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.create-data-set.use-dedicated-apis", + "title": "Use new dedicated APIs for financial reporting", + "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/use-dedicated-apis", + "lastModified": "2025-06-05T17:09:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.create-data-set" + ] + }, + { + "uid": "learn-dynamics.customize-ui-experience.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.customize-ui-experience" + ] + }, + { + "uid": "learn-dynamics.customize-ui-experience.2-objects", + "title": "Add objects to search", + "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/2-objects", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.customize-ui-experience" + ] + }, + { + "uid": "learn-dynamics.customize-ui-experience.3-customizations", + "title": "Create page customizations", + "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/3-customizations", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.customize-ui-experience" + ] + }, + { + "uid": "learn-dynamics.customize-ui-experience.4-profiles", + "title": "Create profiles in application language", + "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/4-profiles", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.customize-ui-experience" + ] + }, + { + "uid": "learn-dynamics.customize-ui-experience.5-views", + "title": "Use and create views in application language", + "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/5-views", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.customize-ui-experience" + ] + }, + { + "uid": "learn-dynamics.customize-ui-experience.6-application-areas", + "title": "Extend application areas", + "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/6-application-areas", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.customize-ui-experience" + ] + }, + { + "uid": "learn-dynamics.customize-ui-experience.7-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/7-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.customize-ui-experience" + ] + }, + { + "uid": "learn-dynamics.customize-ui-experience.8-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/8-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.customize-ui-experience" + ] + }, + { + "uid": "learn-dynamics.customize-ui-experience.extend-customize", + "title": "Extend and customize profiles", + "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/extend-customize", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.customize-ui-experience" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.2-debug", + "title": "Debug your extension with Visual Studio Code", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/2-debug", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 18, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.3-attach", + "title": "Use attach and debug with attach configurations", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/3-attach", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.3a-snapshot", + "title": "Implement snapshot debugging", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/3a-snapshot", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.4-rapid-application-development", + "title": "Work with Rapid Application Development", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/4-rapid-application-development", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.5-publish", + "title": "Publish and install an extension", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/5-publish", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.5a-performance-toolkit", + "title": "Work with the Business Central Performance Toolkit", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/5a-performance-toolkit", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.6-exercise", + "title": "Exercise - Create a table extension", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/6-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.7-exercise", + "title": "Exercise - Create a page extension", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/7-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/8-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/9-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.code-actions", + "title": "Use AL code actions", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/code-actions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.database-wait-statistics", + "title": "Use Database Wait Statistics", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/database-wait-statistics", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.generate-help", + "title": "Generate help with the ALDoc tool", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/generate-help", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.projects-workspaces", + "title": "Configure projects and workspaces", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/projects-workspaces", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.protect-sensitive-values", + "title": "Protect sensitive values", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/protect-sensitive-values", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.recovery-failures", + "title": "Recovery from publishing failures", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/recovery-failures", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.debug-deploy-extension.resource-policy-settings", + "title": "Set Resource Exposure Policy settings", + "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/resource-policy-settings", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.debug-deploy-extension" + ] + }, + { + "uid": "learn-dynamics.design-data-model-report.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.design-data-model-report" + ] + }, + { + "uid": "learn-dynamics.design-data-model-report.2-create-report-data-items", + "title": "Create report data items and their properties", + "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/2-create-report-data-items", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 18, + "moduleUids": [ + "learn-dynamics.design-data-model-report" + ] + }, + { + "uid": "learn-dynamics.design-data-model-report.3-add-columns-report-dataset", + "title": "Add columns to a report dataset", + "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/3-add-columns-report-dataset", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.design-data-model-report" + ] + }, + { + "uid": "learn-dynamics.design-data-model-report.4-link-order-indent-data-items", + "title": "Link, order, and indent data items", + "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/4-link-order-indent-data-items", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.design-data-model-report" + ] + }, + { + "uid": "learn-dynamics.design-data-model-report.5-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/5-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.design-data-model-report" + ] + }, + { + "uid": "learn-dynamics.design-data-model-report.6-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/6-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.design-data-model-report" + ] + }, + { + "uid": "learn-dynamics.design-data-model-report.use-query", + "title": "Use a query to create a report dataset", + "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/use-query", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.design-data-model-report" + ] + }, + { + "uid": "learn-dynamics.develop-role-center.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.develop-role-center" + ] + }, + { + "uid": "learn-dynamics.develop-role-center.2-create-page", + "title": "Create a Role Center page", + "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/2-create-page", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.develop-role-center" + ] + }, + { + "uid": "learn-dynamics.develop-role-center.3-activity-parts", + "title": "Create activity page parts", + "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/3-activity-parts", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.develop-role-center" + ] + }, + { + "uid": "learn-dynamics.develop-role-center.4-cue-setup", + "title": "Integrate with the Cue Setup", + "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/4-cue-setup", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.develop-role-center" + ] + }, + { + "uid": "learn-dynamics.develop-role-center.5-role-center-lists", + "title": "Create Role Center lists", + "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/5-role-center-lists", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.develop-role-center" + ] + }, + { + "uid": "learn-dynamics.develop-role-center.6-headline-parts", + "title": "Implement headline parts", + "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/6-headline-parts", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.develop-role-center" + ] + }, + { + "uid": "learn-dynamics.develop-role-center.7-exercise", + "title": "Exercise - Create a Role Center page with an Activity page", + "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/7-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.develop-role-center" + ] + }, + { + "uid": "learn-dynamics.develop-role-center.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/8-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.develop-role-center" + ] + }, + { + "uid": "learn-dynamics.develop-role-center.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/9-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.develop-role-center" + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.easy-application-upgrade" + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.2-upgrade-responsibilities", + "title": "Understand upgrade responsibilities and best practices", + "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/2-upgrade-responsibilities", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.easy-application-upgrade" + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.3-installation-upgrade-codeunits", + "title": "Create proper installation and upgrade codeunits", + "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/3-installation-upgrade-codeunits", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 25, + "moduleUids": [ + "learn-dynamics.easy-application-upgrade" + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.4-manage-apps", + "title": "Manage apps", + "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/4-manage-apps", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.easy-application-upgrade" + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.5-detect-breaking-changes", + "title": "Detect and avoid breaking changes", + "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/5-detect-breaking-changes", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.easy-application-upgrade" + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.6-answers-about-updates", + "title": "Frequently asked questions about updating your Business Central App", + "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/6-answers-about-updates", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.easy-application-upgrade" + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.7-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/7-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.easy-application-upgrade" + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.8-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/8-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.easy-application-upgrade" + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.consider-update-lifecycle", + "title": "Consider the update lifecycle", + "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/consider-update-lifecycle", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.easy-application-upgrade" + ] + }, + { + "uid": "learn-dynamics.easy-application-upgrade.transfer-data", + "title": "Transfer data between tables using DataTransfer", + "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/transfer-data", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.easy-application-upgrade" + ] + }, + { + "uid": "learn-dynamics.events-triggers.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.events-triggers" + ] + }, + { + "uid": "learn-dynamics.events-triggers.2-table-field-triggers", + "title": "Identify different table and field triggers", + "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/2-table-field-triggers", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.events-triggers" + ] + }, + { + "uid": "learn-dynamics.events-triggers.3-page-control-action-triggers", + "title": "Identify different page, control, and action triggers", + "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/3-page-control-action-triggers", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.events-triggers" + ] + }, + { + "uid": "learn-dynamics.events-triggers.4-event-based", + "title": "Event-based architecture", + "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/4-event-based", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.events-triggers" + ] + }, + { + "uid": "learn-dynamics.events-triggers.5-publish-subscribe", + "title": "Publish and subscribe to events", + "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/5-publish-subscribe", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.events-triggers" + ] + }, + { + "uid": "learn-dynamics.events-triggers.6-exercise", + "title": "Exercise - Events and triggers", + "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/6-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.events-triggers" + ] + }, + { + "uid": "learn-dynamics.events-triggers.7-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/7-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.events-triggers" + ] + }, + { + "uid": "learn-dynamics.events-triggers.8-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/8-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.events-triggers" + ] + }, + { + "uid": "learn-dynamics.extend-functionality-using-events.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/1-introduction", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.extend-functionality-using-events" + ] + }, + { + "uid": "learn-dynamics.extend-functionality-using-events.2-programming-basics", + "title": "Learn the basics of AL programming language", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/2-programming-basics", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.extend-functionality-using-events" + ] + }, + { + "uid": "learn-dynamics.extend-functionality-using-events.3-customer-rewards", + "title": "Create the Customer Rewards Extension Management codeunit object", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/3-customer-rewards", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.extend-functionality-using-events" + ] + }, + { + "uid": "learn-dynamics.extend-functionality-using-events.4-exercise", + "title": "Exercise - Create the Customer Rewards Extension Management codeunit object", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/4-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.extend-functionality-using-events" + ] + }, + { + "uid": "learn-dynamics.extend-functionality-using-events.5-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/5-check", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.extend-functionality-using-events" + ] + }, + { + "uid": "learn-dynamics.extend-functionality-using-events.6-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/6-summary", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.extend-functionality-using-events" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-pages.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/1-introduction", + "lastModified": "2026-09-01T17:24:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-pages" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-pages.2-get-started", + "title": "Get started with page extensions", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/2-get-started", + "lastModified": "2026-09-01T17:24:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-pages" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-pages.3-properties", + "title": "Compare page extension properties with page properties", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/3-properties", + "lastModified": "2026-09-01T17:24:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-pages" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-pages.4-layout", + "title": "Personalize pages", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/4-layout", + "lastModified": "2026-09-01T17:24:00Z", + "durationInMinutes": 20, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-pages" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-pages.5-check", + "title": "Module assessment", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/5-check", + "lastModified": "2026-09-01T17:24:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-pages" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-pages.6-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/6-summary", + "lastModified": "2026-09-01T17:24:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-pages" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-table.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/1-introduction", + "lastModified": "2026-08-17T23:09:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-table" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-table.2-get-started", + "title": "Get started with table extensions", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/2-get-started", + "lastModified": "2026-08-17T23:09:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-table" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-table.3-properties", + "title": "Compare table extension properties with table properties", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/3-properties", + "lastModified": "2026-08-17T23:09:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-table" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-table.4-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/4-check", + "lastModified": "2026-08-17T23:09:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-table" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-table.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/5-summary", + "lastModified": "2026-08-17T23:09:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-table" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-table.add-field-group", + "title": "Add a new FieldGroup to an existing table", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/add-field-group", + "lastModified": "2026-08-17T23:09:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-table" + ] + }, + { + "uid": "learn-dynamics.extend-modify-existing-table.define-extension-objects", + "title": "Define extension objects in same app as their base object", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/define-extension-objects", + "lastModified": "2026-08-17T23:09:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.extend-modify-existing-table" + ] + }, + { + "uid": "learn-dynamics.extend-tables-page-objects.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/1-introduction", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.extend-tables-page-objects" + ] + }, + { + "uid": "learn-dynamics.extend-tables-page-objects.2-customer-table", + "title": "Create the customer table extension object", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/2-customer-table", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.extend-tables-page-objects" + ] + }, + { + "uid": "learn-dynamics.extend-tables-page-objects.3-customer-card", + "title": "Create the customer card page extension object", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/3-customer-card", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.extend-tables-page-objects" + ] + }, + { + "uid": "learn-dynamics.extend-tables-page-objects.4-customer-list", + "title": "Create the customer list page extension object", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/4-customer-list", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.extend-tables-page-objects" + ] + }, + { + "uid": "learn-dynamics.extend-tables-page-objects.5-exercise", + "title": "Exercise - Create the Customer table extension object", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/5-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.extend-tables-page-objects" + ] + }, + { + "uid": "learn-dynamics.extend-tables-page-objects.6-exercise", + "title": "Exercise - Create the Customer Card page extension object", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/6-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.extend-tables-page-objects" + ] + }, + { + "uid": "learn-dynamics.extend-tables-page-objects.7-exercise", + "title": "Exercise - Create the Customer List page extension object", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/7-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.extend-tables-page-objects" + ] + }, + { + "uid": "learn-dynamics.extend-tables-page-objects.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/8-check", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.extend-tables-page-objects" + ] + }, + { + "uid": "learn-dynamics.extend-tables-page-objects.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/9-summary", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.extend-tables-page-objects" + ] + }, + { + "uid": "learn-dynamics.handle-errors.check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.handle-errors" + ] + }, + { + "uid": "learn-dynamics.handle-errors.collect-errors", + "title": "Collect errors", + "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/collect-errors", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.handle-errors" + ] + }, + { + "uid": "learn-dynamics.handle-errors.handle-errors", + "title": "Handle errors by using try methods", + "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/handle-errors", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.handle-errors" + ] + }, + { + "uid": "learn-dynamics.handle-errors.introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.handle-errors" + ] + }, + { + "uid": "learn-dynamics.handle-errors.progress-windows", + "title": "Progress windows and the message, error, and confirm methods", + "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/progress-windows", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.handle-errors" + ] + }, + { + "uid": "learn-dynamics.handle-errors.summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 1, + "moduleUids": [ + "learn-dynamics.handle-errors" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.10-create-setup-tables", + "title": "Create setup tables", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/10-create-setup-tables", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.11-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/11-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.12-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/12-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.2-define-table-types", + "title": "Define the different functional table types and characteristics", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/2-define-table-types", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.3-create-master-tables", + "title": "Create master tables", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/3-create-master-tables", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.4-create-supplemental-tables", + "title": "Create supplemental tables", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/4-create-supplemental-tables", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.5-create-subsidiary-tables", + "title": "Create subsidiary tables", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/5-create-subsidiary-tables", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.6-create-ledger-tables", + "title": "Create ledger tables", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/6-create-ledger-tables", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.7-create-register-tables", + "title": "Create register tables", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/7-create-register-tables", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.8-create-journal-tables", + "title": "Create journal tables", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/8-create-journal-tables", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.identify-table-types.9-create-document-tables", + "title": "Create document tables", + "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/9-create-document-tables", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.identify-table-types" + ] + }, + { + "uid": "learn-dynamics.implement-data-process-model-business-central.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.implement-data-process-model-business-central" + ] + }, + { + "uid": "learn-dynamics.implement-data-process-model-business-central.2-standard-data-model", + "title": "Work with the standard data model", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/2-standard-data-model", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.implement-data-process-model-business-central" + ] + }, + { + "uid": "learn-dynamics.implement-data-process-model-business-central.3-typical-data-triggers", + "title": "Use typical data triggers", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/3-typical-data-triggers", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.implement-data-process-model-business-central" + ] + }, + { + "uid": "learn-dynamics.implement-data-process-model-business-central.4-standard-data-flow", + "title": "Adopt the standard data flow", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/4-standard-data-flow", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.implement-data-process-model-business-central" + ] + }, + { + "uid": "learn-dynamics.implement-data-process-model-business-central.5-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/5-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.implement-data-process-model-business-central" + ] + }, + { + "uid": "learn-dynamics.implement-data-process-model-business-central.6-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/6-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.implement-data-process-model-business-central" + ] + }, + { + "uid": "learn-dynamics.implement-multilanguage-reports.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.implement-multilanguage-reports" + ] + }, + { + "uid": "learn-dynamics.implement-multilanguage-reports.2-create-report-user-language", + "title": "Create a report in the language of the user", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/2-create-report-user-language", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 11, + "moduleUids": [ + "learn-dynamics.implement-multilanguage-reports" + ] + }, + { + "uid": "learn-dynamics.implement-multilanguage-reports.3-create-report-recipient-language", + "title": "Create a report in the language of the recipient", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/3-create-report-recipient-language", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.implement-multilanguage-reports" + ] + }, + { + "uid": "learn-dynamics.implement-multilanguage-reports.4-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/4-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.implement-multilanguage-reports" + ] + }, + { + "uid": "learn-dynamics.implement-multilanguage-reports.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/5-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.implement-multilanguage-reports" + ] + }, + { + "uid": "learn-dynamics.implement-multilanguage-reports.define-regional-settings", + "title": "Define regional settings in reports", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/define-regional-settings", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.implement-multilanguage-reports" + ] + }, + { + "uid": "learn-dynamics.implement-saas-techniques.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques/1-introduction", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.implement-saas-techniques" + ] + }, + { + "uid": "learn-dynamics.implement-saas-techniques.2-build-trial", + "title": "Build a first trial experience", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques/2-build-trial", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 11, + "moduleUids": [ + "learn-dynamics.implement-saas-techniques" + ] + }, + { + "uid": "learn-dynamics.implement-saas-techniques.3-implement-saasification", + "title": "Implement SaaSification techniques when building your user interface", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques/3-implement-saasification", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.implement-saas-techniques" + ] + }, + { + "uid": "learn-dynamics.implement-saas-techniques.4-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques/4-check", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.implement-saas-techniques" + ] + }, + { + "uid": "learn-dynamics.implement-saas-techniques.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques/5-summary", + "lastModified": "2026-08-27T17:36:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.implement-saas-techniques" + ] + }, + { + "uid": "learn-dynamics.initialize-data-extension-installation.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation/1-introduction", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.initialize-data-extension-installation" + ] + }, + { + "uid": "learn-dynamics.initialize-data-extension-installation.2-install-logic", + "title": "Create the Customer Rewards Install Logic codeunit object", + "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation/2-install-logic", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.initialize-data-extension-installation" + ] + }, + { + "uid": "learn-dynamics.initialize-data-extension-installation.3-exercise", + "title": "Exercise - Create the Customer Rewards Install Logic codeunit object", + "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation/3-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.initialize-data-extension-installation" + ] + }, + { + "uid": "learn-dynamics.initialize-data-extension-installation.4-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation/4-check", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.initialize-data-extension-installation" + ] + }, + { + "uid": "learn-dynamics.initialize-data-extension-installation.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation/5-summary", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.initialize-data-extension-installation" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.1-intro", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/1-intro", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.10-exercise", + "title": "Exercise - Use logical and relational expressions", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/10-exercise", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.11-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/11-check", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.12-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/12-summary", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.2-get-started", + "title": "Get started with application language", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/2-get-started", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.3-variables", + "title": "Work with identifiers, syntax, and variables", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/3-variables", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.4-data-types", + "title": "Discover intrinsic data types", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/4-data-types", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.5-options-enums", + "title": "Options and enums", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/5-options-enums", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.6-collections", + "title": "Work with collections", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/6-collections", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.7-assignments", + "title": "Use assignments and type conversions", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/7-assignments", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.8-expressions", + "title": "Define different expression types", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/8-expressions", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-basics-al-programming.9-exercise", + "title": "Exercise - Discover the intrinsic data types", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/9-exercise", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.intro-basics-al-programming" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.1-intro", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/1-intro", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.10-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/10-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.11-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/11-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.2-get-started", + "title": "Get started with Visual Studio Code", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/2-get-started", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.3-install", + "title": "Install the AL language extension in Visual Studio Code", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/3-install", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.4-logical-database", + "title": "Discover the logical database and its objects", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/4-logical-database", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.5-extension", + "title": "Create a new Business Central extension in Visual Studio Code", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/5-extension", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.6-json", + "title": "Work with JSON configuration files", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/6-json", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.7-al-language", + "title": "Configure the AL language extension", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/7-al-language", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.8-multiple", + "title": "Manage multiple AL extensions in one workspace", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/8-multiple", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.9-exercise", + "title": "Exercise - Create and run an AL extension", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/9-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.differentiate-apps", + "title": "Differentiate the base app and the system app", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/differentiate-apps", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.open-visual-studio-code-web-client", + "title": "Open Visual Studio Code from the web client", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/open-visual-studio-code-web-client", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.intro-development-environment.segment-code-reduce-conflicts", + "title": "Segment AL code and reduce naming conflicts with namespaces", + "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/segment-code-reduce-conflicts", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.intro-development-environment" + ] + }, + { + "uid": "learn-dynamics.introduction-basics-file-management.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/1-introduction", + "lastModified": "2026-08-18T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.introduction-basics-file-management" + ] + }, + { + "uid": "learn-dynamics.introduction-basics-file-management.2-file-object", + "title": "Work with the File object", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/2-file-object", + "lastModified": "2026-08-18T17:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.introduction-basics-file-management" + ] + }, + { + "uid": "learn-dynamics.introduction-basics-file-management.3-temp-blob-object", + "title": "Work with the Temp Blob object", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/3-temp-blob-object", + "lastModified": "2026-08-18T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.introduction-basics-file-management" + ] + }, + { + "uid": "learn-dynamics.introduction-basics-file-management.4-read-write-stream", + "title": "Read and write files by using streaming in Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/4-read-write-stream", + "lastModified": "2026-08-18T17:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.introduction-basics-file-management" + ] + }, + { + "uid": "learn-dynamics.introduction-basics-file-management.5-exercise", + "title": "Exercise - Read and write files", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/5-exercise", + "lastModified": "2026-08-18T17:07:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.introduction-basics-file-management" + ] + }, + { + "uid": "learn-dynamics.introduction-basics-file-management.6-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/6-check", + "lastModified": "2026-08-18T17:07:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.introduction-basics-file-management" + ] + }, + { + "uid": "learn-dynamics.introduction-basics-file-management.7-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/7-summary", + "lastModified": "2026-08-18T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.introduction-basics-file-management" + ] + }, + { + "uid": "learn-dynamics.introduction-basics-file-management.uploads-drop-zones", + "title": "Handle multiple file uploads and file drop zones", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/uploads-drop-zones", + "lastModified": "2026-08-18T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.introduction-basics-file-management" + ] + }, + { + "uid": "learn-dynamics.introduction-development-process.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/1-introduction", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.introduction-development-process" + ] + }, + { + "uid": "learn-dynamics.introduction-development-process.2-process", + "title": "Introduction to the development process", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/2-process", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.introduction-development-process" + ] + }, + { + "uid": "learn-dynamics.introduction-development-process.3-setup", + "title": "Set up the development environment", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/3-setup", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.introduction-development-process" + ] + }, + { + "uid": "learn-dynamics.introduction-development-process.4-rewards", + "title": "Build a customer rewards extension", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/4-rewards", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.introduction-development-process" + ] + }, + { + "uid": "learn-dynamics.introduction-development-process.5-exercise", + "title": "Exercise - Set up the development environment", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/5-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.introduction-development-process" + ] + }, + { + "uid": "learn-dynamics.introduction-development-process.6-exercise", + "title": "Exercise - Create the reward level table object", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/6-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.introduction-development-process" + ] + }, + { + "uid": "learn-dynamics.introduction-development-process.7-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/7-check", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.introduction-development-process" + ] + }, + { + "uid": "learn-dynamics.introduction-development-process.8-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/8-summary", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.introduction-development-process" + ] + }, + { + "uid": "learn-dynamics.introduction-power-platform-business-central.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.introduction-power-platform-business-central" + ] + }, + { + "uid": "learn-dynamics.introduction-power-platform-business-central.2-data-connectors", + "title": "Explore data connectors", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/2-data-connectors", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.introduction-power-platform-business-central" + ] + }, + { + "uid": "learn-dynamics.introduction-power-platform-business-central.3-business-central-connectors", + "title": "Explore Business Central connectors", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/3-business-central-connectors", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.introduction-power-platform-business-central" + ] + }, + { + "uid": "learn-dynamics.introduction-power-platform-business-central.4-power-apps", + "title": "Get started with Power Apps", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/4-power-apps", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.introduction-power-platform-business-central" + ] + }, + { + "uid": "learn-dynamics.introduction-power-platform-business-central.5-power-automate", + "title": "Get started with Power Automate", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/5-power-automate", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.introduction-power-platform-business-central" + ] + }, + { + "uid": "learn-dynamics.introduction-power-platform-business-central.6-embed-power-platform", + "title": "Embed Microsoft Power Platform in Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/6-embed-power-platform", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.introduction-power-platform-business-central" + ] + }, + { + "uid": "learn-dynamics.introduction-power-platform-business-central.7-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/7-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.introduction-power-platform-business-central" + ] + }, + { + "uid": "learn-dynamics.introduction-power-platform-business-central.8-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/8-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.introduction-power-platform-business-central" + ] + }, + { + "uid": "learn-dynamics.introduction-power-platform-business-central.use-cases", + "title": "Power Platform integration use cases", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/use-cases", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.introduction-power-platform-business-central" + ] + }, + { + "uid": "learn-dynamics.introduction-report-components.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.introduction-report-components" + ] + }, + { + "uid": "learn-dynamics.introduction-report-components.2-learn-overview-report-description", + "title": "Report descriptions", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/2-learn-overview-report-description", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.introduction-report-components" + ] + }, + { + "uid": "learn-dynamics.introduction-report-components.3-report-properties-triggers", + "title": "Report properties and triggers", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/3-report-properties-triggers", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.introduction-report-components" + ] + }, + { + "uid": "learn-dynamics.introduction-report-components.4-data-items-columns", + "title": "Data items and columns", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/4-data-items-columns", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.introduction-report-components" + ] + }, + { + "uid": "learn-dynamics.introduction-report-components.5-request-page", + "title": "Request page", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/5-request-page", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.introduction-report-components" + ] + }, + { + "uid": "learn-dynamics.introduction-report-components.6-work-report-labels", + "title": "Work with report labels", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/6-work-report-labels", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.introduction-report-components" + ] + }, + { + "uid": "learn-dynamics.introduction-report-components.7-report-layouts", + "title": "Report layouts", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/7-report-layouts", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.introduction-report-components" + ] + }, + { + "uid": "learn-dynamics.introduction-report-components.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/8-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.introduction-report-components" + ] + }, + { + "uid": "learn-dynamics.introduction-report-components.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/9-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.introduction-report-components" + ] + }, + { + "uid": "learn-dynamics.introduction-test-automation.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/1-introduction", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.introduction-test-automation" + ] + }, + { + "uid": "learn-dynamics.introduction-test-automation.2-test-applications", + "title": "Test applications in Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/2-test-applications", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 18, + "moduleUids": [ + "learn-dynamics.introduction-test-automation" + ] + }, + { + "uid": "learn-dynamics.introduction-test-automation.3-install-run-test-toolkit", + "title": "Install and run the Test Toolkit", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/3-install-run-test-toolkit", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.introduction-test-automation" + ] + }, + { + "uid": "learn-dynamics.introduction-test-automation.4-run-standard-tests", + "title": "Run the standard tests", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/4-run-standard-tests", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.introduction-test-automation" + ] + }, + { + "uid": "learn-dynamics.introduction-test-automation.5-create-test-code-units", + "title": "Create your own test codeunits with a test app", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/5-create-test-code-units", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.introduction-test-automation" + ] + }, + { + "uid": "learn-dynamics.introduction-test-automation.6-check", + "title": "Module assessment", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/6-check", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.introduction-test-automation" + ] + }, + { + "uid": "learn-dynamics.introduction-test-automation.7-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/7-summary", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.introduction-test-automation" + ] + }, + { + "uid": "learn-dynamics.manage-multilanguage-development.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.manage-multilanguage-development" + ] + }, + { + "uid": "learn-dynamics.manage-multilanguage-development.2-translation-files", + "title": "Create and use translation files", + "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/2-translation-files", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.manage-multilanguage-development" + ] + }, + { + "uid": "learn-dynamics.manage-multilanguage-development.3-automate", + "title": "Use Azure Translator Text to automate translation", + "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/3-automate", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.manage-multilanguage-development" + ] + }, + { + "uid": "learn-dynamics.manage-multilanguage-development.4-exercise", + "title": "Exercise - Multilanguage development", + "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/4-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.manage-multilanguage-development" + ] + }, + { + "uid": "learn-dynamics.manage-multilanguage-development.5-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/5-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.manage-multilanguage-development" + ] + }, + { + "uid": "learn-dynamics.manage-multilanguage-development.6-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/6-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.manage-multilanguage-development" + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.manipulate-data-via-code" + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code.2-retrieve-data", + "title": "Retrieve data from the database with AL statements", + "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/2-retrieve-data", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.manipulate-data-via-code" + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code.3-sort-filter", + "title": "Sort and filter data in code", + "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/3-sort-filter", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.manipulate-data-via-code" + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code.4-add-modify-delete", + "title": "Add, modify, or delete data with AL", + "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/4-add-modify-delete", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.manipulate-data-via-code" + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code.5-field-functions", + "title": "Use different field functions in AL", + "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/5-field-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.manipulate-data-via-code" + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code.6-exercise", + "title": "Exercise - Use data manipulation statements", + "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/6-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.manipulate-data-via-code" + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code.7-exercise", + "title": "Exercise - Custom functions", + "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/7-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.manipulate-data-via-code" + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/8-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.manipulate-data-via-code" + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/9-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.manipulate-data-via-code" + ] + }, + { + "uid": "learn-dynamics.manipulate-data-via-code.control-database-locking-behavior", + "title": "Control database locking behavior", + "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/control-database-locking-behavior", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.manipulate-data-via-code" + ] + }, + { + "uid": "learn-dynamics.report-extensibility.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.report-extensibility" + ] + }, + { + "uid": "learn-dynamics.report-extensibility.2-anatomy", + "title": "Anatomy of a report extension", + "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility/2-anatomy", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 20, + "moduleUids": [ + "learn-dynamics.report-extensibility" + ] + }, + { + "uid": "learn-dynamics.report-extensibility.3-report-extension-example", + "title": "Examples of report extensions", + "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility/3-report-extension-example", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 24, + "moduleUids": [ + "learn-dynamics.report-extensibility" + ] + }, + { + "uid": "learn-dynamics.report-extensibility.4-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility/4-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.report-extensibility" + ] + }, + { + "uid": "learn-dynamics.report-extensibility.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility/5-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.report-extensibility" + ] + }, + { + "uid": "learn-dynamics.send-notifications.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.send-notifications" + ] + }, + { + "uid": "learn-dynamics.send-notifications.2-notifications", + "title": "Work with notifications", + "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/2-notifications", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.send-notifications" + ] + }, + { + "uid": "learn-dynamics.send-notifications.3-create-send", + "title": "Create and send notifications", + "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/3-create-send", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.send-notifications" + ] + }, + { + "uid": "learn-dynamics.send-notifications.4-exercise", + "title": "Exercise - Create a notification", + "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/4-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.send-notifications" + ] + }, + { + "uid": "learn-dynamics.send-notifications.5-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/5-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.send-notifications" + ] + }, + { + "uid": "learn-dynamics.send-notifications.6-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/6-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.send-notifications" + ] + }, + { + "uid": "learn-dynamics.set-up-onboarding-experiences.check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.set-up-onboarding-experiences" + ] + }, + { + "uid": "learn-dynamics.set-up-onboarding-experiences.get-users-started-checklist", + "title": "Get users started with the checklist", + "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/get-users-started-checklist", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.set-up-onboarding-experiences" + ] + }, + { + "uid": "learn-dynamics.set-up-onboarding-experiences.implement-tips-tour", + "title": "Implement teaching tips and in-app tours", + "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/implement-tips-tour", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 20, + "moduleUids": [ + "learn-dynamics.set-up-onboarding-experiences" + ] + }, + { + "uid": "learn-dynamics.set-up-onboarding-experiences.introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.set-up-onboarding-experiences" + ] + }, + { + "uid": "learn-dynamics.set-up-onboarding-experiences.onboard-new-users", + "title": "Greet new users with the welcome banner", + "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/onboard-new-users", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.set-up-onboarding-experiences" + ] + }, + { + "uid": "learn-dynamics.set-up-onboarding-experiences.summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.set-up-onboarding-experiences" + ] + }, + { + "uid": "learn-dynamics.test-automation.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/1-introduction", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.test-automation" + ] + }, + { + "uid": "learn-dynamics.test-automation.2-test-automation-responsibilities", + "title": "Understand test automation responsibilities", + "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/2-test-automation-responsibilities", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.test-automation" + ] + }, + { + "uid": "learn-dynamics.test-automation.3-documentation-examples", + "title": "Find test automation documentation and examples", + "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/3-documentation-examples", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.test-automation" + ] + }, + { + "uid": "learn-dynamics.test-automation.4-answers-about-testing", + "title": "Frequently asked questions about testing your Business Central App", + "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/4-answers-about-testing", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.test-automation" + ] + }, + { + "uid": "learn-dynamics.test-automation.5-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/5-check", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.test-automation" + ] + }, + { + "uid": "learn-dynamics.test-automation.6-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/6-summary", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.test-automation" + ] + }, + { + "uid": "learn-dynamics.test-automation.page-scripting-tool", + "title": "Use the in-client page scripting tool for acceptance testing", + "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/page-scripting-tool", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 50, + "moduleUids": [ + "learn-dynamics.test-automation" + ] + }, + { + "uid": "learn-dynamics.understand-report-design-process.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.understand-report-design-process" + ] + }, + { + "uid": "learn-dynamics.understand-report-design-process.2-report-design-process", + "title": "Report design process", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process/2-report-design-process", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.understand-report-design-process" + ] + }, + { + "uid": "learn-dynamics.understand-report-design-process.3-use-snippet-create-report", + "title": "Use a snippet to create a report object", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process/3-use-snippet-create-report", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.understand-report-design-process" + ] + }, + { + "uid": "learn-dynamics.understand-report-design-process.4-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process/4-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.understand-report-design-process" + ] + }, + { + "uid": "learn-dynamics.understand-report-design-process.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process/5-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.understand-report-design-process" + ] + }, + { + "uid": "learn-dynamics.understand-report-triggers-functions.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/1-introduction", + "lastModified": "2026-08-27T23:37:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.understand-report-triggers-functions" + ] + }, + { + "uid": "learn-dynamics.understand-report-triggers-functions.2-understand-report-trigger-execution", + "title": "Report trigger implementation flow", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/2-understand-report-trigger-execution", + "lastModified": "2026-08-27T23:37:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.understand-report-triggers-functions" + ] + }, + { + "uid": "learn-dynamics.understand-report-triggers-functions.3-use-report-functions", + "title": "Use report functions", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/3-use-report-functions", + "lastModified": "2026-08-27T23:37:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.understand-report-triggers-functions" + ] + }, + { + "uid": "learn-dynamics.understand-report-triggers-functions.4-create-processing-only-report", + "title": "Create a processing-only report", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/4-create-processing-only-report", + "lastModified": "2026-08-27T23:37:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.understand-report-triggers-functions" + ] + }, + { + "uid": "learn-dynamics.understand-report-triggers-functions.4a-adding-barcodes", + "title": "Adding barcodes to a report", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/4a-adding-barcodes", + "lastModified": "2026-08-27T23:37:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.understand-report-triggers-functions" + ] + }, + { + "uid": "learn-dynamics.understand-report-triggers-functions.5-exercise", + "title": "Exercise - Create a processing-only report", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/5-exercise", + "lastModified": "2026-08-27T23:37:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.understand-report-triggers-functions" + ] + }, + { + "uid": "learn-dynamics.understand-report-triggers-functions.6-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/6-check", + "lastModified": "2026-08-27T23:37:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.understand-report-triggers-functions" + ] + }, + { + "uid": "learn-dynamics.understand-report-triggers-functions.7-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/7-summary", + "lastModified": "2026-08-27T23:37:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.understand-report-triggers-functions" + ] + }, + { + "uid": "learn-dynamics.use-al-controlled-barcode-scanning.add-button", + "title": "Add a barcode-scanning UI button on a field", + "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/add-button", + "lastModified": "2026-08-20T23:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-al-controlled-barcode-scanning" + ] + }, + { + "uid": "learn-dynamics.use-al-controlled-barcode-scanning.check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/check", + "lastModified": "2026-08-20T23:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.use-al-controlled-barcode-scanning" + ] + }, + { + "uid": "learn-dynamics.use-al-controlled-barcode-scanning.integrate-scanners", + "title": "Integrate dedicated barcode scanners", + "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/integrate-scanners", + "lastModified": "2026-08-20T23:07:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.use-al-controlled-barcode-scanning" + ] + }, + { + "uid": "learn-dynamics.use-al-controlled-barcode-scanning.introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/introduction", + "lastModified": "2026-08-20T23:07:00Z", + "durationInMinutes": 1, + "moduleUids": [ + "learn-dynamics.use-al-controlled-barcode-scanning" + ] + }, + { + "uid": "learn-dynamics.use-al-controlled-barcode-scanning.invoke-scanning", + "title": "Invoke barcode scanning programmatically from AL", + "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/invoke-scanning", + "lastModified": "2026-08-20T23:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.use-al-controlled-barcode-scanning" + ] + }, + { + "uid": "learn-dynamics.use-al-controlled-barcode-scanning.scenarios-barcodes", + "title": "Supported scenarios and barcodes", + "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/scenarios-barcodes", + "lastModified": "2026-08-20T23:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.use-al-controlled-barcode-scanning" + ] + }, + { + "uid": "learn-dynamics.use-al-controlled-barcode-scanning.summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/summary", + "lastModified": "2026-08-20T23:07:00Z", + "durationInMinutes": 1, + "moduleUids": [ + "learn-dynamics.use-al-controlled-barcode-scanning" + ] + }, + { + "uid": "learn-dynamics.use-application-lifecycle.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/1-introduction", + "lastModified": "2026-08-28T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.use-application-lifecycle" + ] + }, + { + "uid": "learn-dynamics.use-application-lifecycle.2-create-azure-devops", + "title": "Create an Azure DevOps organization", + "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/2-create-azure-devops", + "lastModified": "2026-08-28T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.use-application-lifecycle" + ] + }, + { + "uid": "learn-dynamics.use-application-lifecycle.3-manage-azure-devops", + "title": "Manage Azure DevOps Projects", + "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/3-manage-azure-devops", + "lastModified": "2026-08-28T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.use-application-lifecycle" + ] + }, + { + "uid": "learn-dynamics.use-application-lifecycle.4-introduction-devops-services", + "title": "Introduction to Azure DevOps Services", + "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/4-introduction-devops-services", + "lastModified": "2026-08-28T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.use-application-lifecycle" + ] + }, + { + "uid": "learn-dynamics.use-application-lifecycle.5-connect-devops-tokens", + "title": "Connect to Azure DevOps via Personal Access Tokens", + "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/5-connect-devops-tokens", + "lastModified": "2026-08-28T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-application-lifecycle" + ] + }, + { + "uid": "learn-dynamics.use-application-lifecycle.6-introduction-github", + "title": "Introduction to GitHub", + "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/6-introduction-github", + "lastModified": "2026-08-28T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-application-lifecycle" + ] + }, + { + "uid": "learn-dynamics.use-application-lifecycle.7-exercise", + "title": "Exercise - Create an Azure DevOps organization and project", + "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/7-exercise", + "lastModified": "2026-08-28T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.use-application-lifecycle" + ] + }, + { + "uid": "learn-dynamics.use-application-lifecycle.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/8-check", + "lastModified": "2026-08-28T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-application-lifecycle" + ] + }, + { + "uid": "learn-dynamics.use-application-lifecycle.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/9-summary", + "lastModified": "2026-08-28T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-application-lifecycle" + ] + }, + { + "uid": "learn-dynamics.use-azure-functions.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/1-introduction", + "lastModified": "2026-08-17T18:23:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-azure-functions" + ] + }, + { + "uid": "learn-dynamics.use-azure-functions.2-create-basic", + "title": "Create basic Azure functions", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/2-create-basic", + "lastModified": "2026-08-17T18:23:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.use-azure-functions" + ] + }, + { + "uid": "learn-dynamics.use-azure-functions.3-create-existing", + "title": "Create an Azure function", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/3-create-existing", + "lastModified": "2026-08-17T18:23:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.use-azure-functions" + ] + }, + { + "uid": "learn-dynamics.use-azure-functions.4-use", + "title": "Use an Azure function in Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/4-use", + "lastModified": "2026-08-17T18:23:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.use-azure-functions" + ] + }, + { + "uid": "learn-dynamics.use-azure-functions.6-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/6-check", + "lastModified": "2026-08-17T18:23:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.use-azure-functions" + ] + }, + { + "uid": "learn-dynamics.use-azure-functions.7-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/7-summary", + "lastModified": "2026-08-17T18:23:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-azure-functions" + ] + }, + { + "uid": "learn-dynamics.use-azure-pipelines.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/1-introduction", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.use-azure-pipelines" + ] + }, + { + "uid": "learn-dynamics.use-azure-pipelines.2-what-continuous-integration", + "title": "What is CI/CD?", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/2-what-continuous-integration", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-azure-pipelines" + ] + }, + { + "uid": "learn-dynamics.use-azure-pipelines.3-configure-build-agents", + "title": "Configure build agents", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/3-configure-build-agents", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.use-azure-pipelines" + ] + }, + { + "uid": "learn-dynamics.use-azure-pipelines.4-create-build-pipeline", + "title": "Create a build pipeline", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/4-create-build-pipeline", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.use-azure-pipelines" + ] + }, + { + "uid": "learn-dynamics.use-azure-pipelines.5-work-yaml-files", + "title": "Work with YAML files", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/5-work-yaml-files", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.use-azure-pipelines" + ] + }, + { + "uid": "learn-dynamics.use-azure-pipelines.6-create-release-pipeline", + "title": "Create a release pipeline", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/6-create-release-pipeline", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.use-azure-pipelines" + ] + }, + { + "uid": "learn-dynamics.use-azure-pipelines.7-exercise", + "title": "Exercise - Use Azure Pipelines for CI/CD with Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/7-exercise", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.use-azure-pipelines" + ] + }, + { + "uid": "learn-dynamics.use-azure-pipelines.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/8-check", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.use-azure-pipelines" + ] + }, + { + "uid": "learn-dynamics.use-azure-pipelines.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/9-summary", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-azure-pipelines" + ] + }, + { + "uid": "learn-dynamics.use-branch-merge-git.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/1-introduction", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-branch-merge-git" + ] + }, + { + "uid": "learn-dynamics.use-branch-merge-git.2-branch-commands", + "title": "Branch commands in Git", + "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/2-branch-commands", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 18, + "moduleUids": [ + "learn-dynamics.use-branch-merge-git" + ] + }, + { + "uid": "learn-dynamics.use-branch-merge-git.3-use-rebase", + "title": "Use Rebase in Git", + "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/3-use-rebase", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.use-branch-merge-git" + ] + }, + { + "uid": "learn-dynamics.use-branch-merge-git.4-set-branch-policies", + "title": "Set branch policies in Azure DevOps", + "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/4-set-branch-policies", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 14, + "moduleUids": [ + "learn-dynamics.use-branch-merge-git" + ] + }, + { + "uid": "learn-dynamics.use-branch-merge-git.5-use-pull-requests", + "title": "Use pull requests in Git with Azure DevOps", + "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/5-use-pull-requests", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.use-branch-merge-git" + ] + }, + { + "uid": "learn-dynamics.use-branch-merge-git.6-work-branch-strategies", + "title": "Work with different branching strategies", + "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/6-work-branch-strategies", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.use-branch-merge-git" + ] + }, + { + "uid": "learn-dynamics.use-branch-merge-git.7-exercise", + "title": "Exercise - Use branching and merging with Git", + "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/7-exercise", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.use-branch-merge-git" + ] + }, + { + "uid": "learn-dynamics.use-branch-merge-git.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/8-check", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.use-branch-merge-git" + ] + }, + { + "uid": "learn-dynamics.use-branch-merge-git.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/9-summary", + "lastModified": "2026-08-28T17:09:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.use-branch-merge-git" + ] + }, + { + "uid": "learn-dynamics.use-docker-business-central.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/1-introduction", + "lastModified": "2026-08-13T17:20:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-docker-business-central" + ] + }, + { + "uid": "learn-dynamics.use-docker-business-central.2-docker-images-containers", + "title": "Work with Docker images and containers", + "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/2-docker-images-containers", + "lastModified": "2026-08-13T17:20:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.use-docker-business-central" + ] + }, + { + "uid": "learn-dynamics.use-docker-business-central.3-install-docker-windows", + "title": "Install Docker on Windows", + "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/3-install-docker-windows", + "lastModified": "2026-08-13T17:20:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.use-docker-business-central" + ] + }, + { + "uid": "learn-dynamics.use-docker-business-central.4-process-isolation-hyper-v", + "title": "Process Isolation vs. Hyper-V Isolation", + "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/4-process-isolation-hyper-v", + "lastModified": "2026-08-13T17:20:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.use-docker-business-central" + ] + }, + { + "uid": "learn-dynamics.use-docker-business-central.5-basic-docker-commands", + "title": "Basic Docker commands", + "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/5-basic-docker-commands", + "lastModified": "2026-08-13T17:20:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.use-docker-business-central" + ] + }, + { + "uid": "learn-dynamics.use-docker-business-central.6-use-bc-container-helper", + "title": "Use the BcContainerHelper", + "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/6-use-bc-container-helper", + "lastModified": "2026-08-13T17:20:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.use-docker-business-central" + ] + }, + { + "uid": "learn-dynamics.use-docker-business-central.7-exercise", + "title": "Exercise - Use Docker to test the latest Business Central version", + "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/7-exercise", + "lastModified": "2026-08-13T17:20:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.use-docker-business-central" + ] + }, + { + "uid": "learn-dynamics.use-docker-business-central.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/8-check", + "lastModified": "2026-08-13T17:20:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.use-docker-business-central" + ] + }, + { + "uid": "learn-dynamics.use-docker-business-central.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/9-summary", + "lastModified": "2026-08-13T17:20:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-docker-business-central" + ] + }, + { + "uid": "learn-dynamics.use-document-standards-business-central.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.use-document-standards-business-central" + ] + }, + { + "uid": "learn-dynamics.use-document-standards-business-central.2-create-document-page", + "title": "Create a document page", + "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/2-create-document-page", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.use-document-standards-business-central" + ] + }, + { + "uid": "learn-dynamics.use-document-standards-business-central.3-use-initrecord-function", + "title": "Use the InitRecord function", + "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/3-use-initrecord-function", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.use-document-standards-business-central" + ] + }, + { + "uid": "learn-dynamics.use-document-standards-business-central.4-use-testfield-function", + "title": "Use the TestField function", + "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/4-use-testfield-function", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.use-document-standards-business-central" + ] + }, + { + "uid": "learn-dynamics.use-document-standards-business-central.4a-use-round-function", + "title": "Use the Round function", + "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/4a-use-round-function", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.use-document-standards-business-central" + ] + }, + { + "uid": "learn-dynamics.use-document-standards-business-central.4b-exercise", + "title": "Exercise - Create example documents", + "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/4b-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.use-document-standards-business-central" + ] + }, + { + "uid": "learn-dynamics.use-document-standards-business-central.5-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/5-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.use-document-standards-business-central" + ] + }, + { + "uid": "learn-dynamics.use-document-standards-business-central.6-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/6-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.use-document-standards-business-central" + ] + }, + { + "uid": "learn-dynamics.use-model-driven-apps-common-data-service.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/1-introduction", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.use-model-driven-apps-common-data-service" + ] + }, + { + "uid": "learn-dynamics.use-model-driven-apps-common-data-service.2-entities-fields-common-data-service", + "title": "Identify tables and columns in Microsoft Dataverse", + "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/2-entities-fields-common-data-service", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.use-model-driven-apps-common-data-service" + ] + }, + { + "uid": "learn-dynamics.use-model-driven-apps-common-data-service.3-create-entity-import-data", + "title": "Create a table and connection to import data from Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/3-create-entity-import-data", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 25, + "moduleUids": [ + "learn-dynamics.use-model-driven-apps-common-data-service" + ] + }, + { + "uid": "learn-dynamics.use-model-driven-apps-common-data-service.4-model-driven-apps", + "title": "Get started with model-driven apps", + "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/4-model-driven-apps", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.use-model-driven-apps-common-data-service" + ] + }, + { + "uid": "learn-dynamics.use-model-driven-apps-common-data-service.5-user-interface-logic", + "title": "Explore the user interface and logic in model-driven apps", + "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/5-user-interface-logic", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.use-model-driven-apps-common-data-service" + ] + }, + { + "uid": "learn-dynamics.use-model-driven-apps-common-data-service.6-create-model-driven-app", + "title": "Create a model-driven app based on Business Central data", + "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/6-create-model-driven-app", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 17, + "moduleUids": [ + "learn-dynamics.use-model-driven-apps-common-data-service" + ] + }, + { + "uid": "learn-dynamics.use-model-driven-apps-common-data-service.7-exercise", + "title": "Exercise - Create a table in Microsoft Dataverse", + "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/7-exercise", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.use-model-driven-apps-common-data-service" + ] + }, + { + "uid": "learn-dynamics.use-model-driven-apps-common-data-service.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/8-check", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.use-model-driven-apps-common-data-service" + ] + }, + { + "uid": "learn-dynamics.use-model-driven-apps-common-data-service.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/9-summary", + "lastModified": "2026-08-14T23:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.use-model-driven-apps-common-data-service" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/1-introduction", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.10-troubleshoot", + "title": "Troubleshoot and monitor flows", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/10-troubleshoot", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.11-exercise", + "title": "Exercise - Create a flow for sending emails when a new item is created in Power Automate", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/11-exercise", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.12-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/12-check", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.13-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/13-summary", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.2-triggers", + "title": "Explore Business Central triggers in Power Automate", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/2-triggers", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.3-create-power-automate", + "title": "Create a Power Automate flow from scratch", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/3-create-power-automate", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.4-item-approvals", + "title": "Create customer, vendor, and item approvals in Power Automate", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/4-item-approvals", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 20, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.5-general-journal-approvals", + "title": "Create approvals in a general journal with Power Automate", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/5-general-journal-approvals", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.6-document-approvals", + "title": "Create documents approvals with Power Automate", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/6-document-approvals", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 9, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.7-business-scenarios", + "title": "Create Power Automate flows for different business scenarios", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/7-business-scenarios", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 22, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.8-create-button", + "title": "Create a button or instant flow in Power Automate", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/8-create-button", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.use-power-automate.9-schedule", + "title": "Run a Power Automate flow on a schedule", + "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/9-schedule", + "lastModified": "2026-08-10T18:36:00Z", + "durationInMinutes": 9, + "moduleUids": [ + "learn-dynamics.use-power-automate" + ] + }, + { + "uid": "learn-dynamics.work-azure-boards.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/1-introduction", + "lastModified": "2026-08-11T23:09:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-azure-boards" + ] + }, + { + "uid": "learn-dynamics.work-azure-boards.2-work-process-templates", + "title": "Work with the process templates", + "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/2-work-process-templates", + "lastModified": "2026-08-11T23:09:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.work-azure-boards" + ] + }, + { + "uid": "learn-dynamics.work-azure-boards.3-configure-iterations", + "title": "Configure iterations in Azure Boards", + "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/3-configure-iterations", + "lastModified": "2026-08-11T23:09:00Z", + "durationInMinutes": 13, + "moduleUids": [ + "learn-dynamics.work-azure-boards" + ] + }, + { + "uid": "learn-dynamics.work-azure-boards.4-create-work-items", + "title": "Create work items and link to code changes", + "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/4-create-work-items", + "lastModified": "2026-08-11T23:09:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-azure-boards" + ] + }, + { + "uid": "learn-dynamics.work-azure-boards.5-project-dashboards-wikis", + "title": "Work with project dashboards and wikis", + "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/5-project-dashboards-wikis", + "lastModified": "2026-08-11T23:09:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-azure-boards" + ] + }, + { + "uid": "learn-dynamics.work-azure-boards.6-teams-integrate-devops", + "title": "Use Microsoft Teams to integrate with Azure DevOps", + "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/6-teams-integrate-devops", + "lastModified": "2026-08-11T23:09:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.work-azure-boards" + ] + }, + { + "uid": "learn-dynamics.work-azure-boards.7-exercise", + "title": "Exercise - Work with Azure Boards", + "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/7-exercise", + "lastModified": "2026-08-11T23:09:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-azure-boards" + ] + }, + { + "uid": "learn-dynamics.work-azure-boards.8-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/8-check", + "lastModified": "2026-08-11T23:09:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.work-azure-boards" + ] + }, + { + "uid": "learn-dynamics.work-azure-boards.9-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/9-summary", + "lastModified": "2026-08-11T23:09:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-azure-boards" + ] + }, + { + "uid": "learn-dynamics.work-entitlements-permission-sets.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/1-introduction", + "lastModified": "2026-09-03T23:14:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.work-entitlements-permission-sets" + ] + }, + { + "uid": "learn-dynamics.work-entitlements-permission-sets.2-get-started", + "title": "Get started with entitlements and permissions", + "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/2-get-started", + "lastModified": "2026-09-03T23:14:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-entitlements-permission-sets" + ] + }, + { + "uid": "learn-dynamics.work-entitlements-permission-sets.3-create-entitlements-permission-sets", + "title": "Create entitlements and permission sets", + "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/3-create-entitlements-permission-sets", + "lastModified": "2026-09-03T23:14:00Z", + "durationInMinutes": 18, + "moduleUids": [ + "learn-dynamics.work-entitlements-permission-sets" + ] + }, + { + "uid": "learn-dynamics.work-entitlements-permission-sets.4-check", + "title": "Module assessment", + "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/4-check", + "lastModified": "2026-09-03T23:14:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-entitlements-permission-sets" + ] + }, + { + "uid": "learn-dynamics.work-entitlements-permission-sets.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/5-summary", + "lastModified": "2026-09-03T23:14:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-entitlements-permission-sets" + ] + }, + { + "uid": "learn-dynamics.work-entitlements-permission-sets.override-entitlements", + "title": "Override entitlements with inherent permissions", + "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/override-entitlements", + "lastModified": "2026-09-03T23:14:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-entitlements-permission-sets" + ] + }, + { + "uid": "learn-dynamics.work-power-bi.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/1-introduction", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-power-bi" + ] + }, + { + "uid": "learn-dynamics.work-power-bi.2-deploy-dashboards", + "title": "Deploy Business Central dashboards in Power BI", + "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/2-deploy-dashboards", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-power-bi" + ] + }, + { + "uid": "learn-dynamics.work-power-bi.3-set-up-power-bi", + "title": "Set up Power BI in Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/3-set-up-power-bi", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.work-power-bi" + ] + }, + { + "uid": "learn-dynamics.work-power-bi.4-view-list-data", + "title": "View list data in Power BI reports in Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/4-view-list-data", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-power-bi" + ] + }, + { + "uid": "learn-dynamics.work-power-bi.5-exercise", + "title": "Exercise - Embed your report in Business Central", + "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/5-exercise", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-power-bi" + ] + }, + { + "uid": "learn-dynamics.work-power-bi.6-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/6-check", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-power-bi" + ] + }, + { + "uid": "learn-dynamics.work-power-bi.7-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/7-summary", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-power-bi" + ] + }, + { + "uid": "learn-dynamics.work-power-bi.add-report-parts", + "title": "Adding Power BI Report Parts to pages", + "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/add-report-parts", + "lastModified": "2026-09-02T17:07:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.work-power-bi" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/1-introduction", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.10-work-remote-git", + "title": "Work with a remote Git Repository", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/10-work-remote-git", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.11-exercise", + "title": "Exercise - Source control with Git", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/11-exercise", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.12-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/12-check", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.13-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/13-summary", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.2-configure-git", + "title": "Configure Git", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/2-configure-git", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.3-local-remote-repositories", + "title": "Compare and contrast local and remote repositories", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/3-local-remote-repositories", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.4-create-local-git", + "title": "Create a local Git repository", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/4-create-local-git", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.5-use-git-ignore", + "title": "Use the .gitignore file", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/5-use-git-ignore", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.6-add-remove-files", + "title": "Add and remove files in Git", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/6-add-remove-files", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.7-get-status-repository", + "title": "Get the status of your Git repository", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/7-get-status-repository", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.8-use-remote-git", + "title": "Use a remote Git repository", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/8-use-remote-git", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-source-control-git.9-clone-remote-git", + "title": "Clone a remote Git repository", + "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/9-clone-remote-git", + "lastModified": "2026-08-24T17:14:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.work-source-control-git" + ] + }, + { + "uid": "learn-dynamics.work-with-api.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-api" + ] + }, + { + "uid": "learn-dynamics.work-with-api.10-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/10-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-api" + ] + }, + { + "uid": "learn-dynamics.work-with-api.2-access", + "title": "Access Business Central API", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/2-access", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-api" + ] + }, + { + "uid": "learn-dynamics.work-with-api.3-tips", + "title": "Tips for working with APIs", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/3-tips", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-api" + ] + }, + { + "uid": "learn-dynamics.work-with-api.4-limits", + "title": "Work with API limits", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/4-limits", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-api" + ] + }, + { + "uid": "learn-dynamics.work-with-api.5-create-new", + "title": "Create new APIs", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/5-create-new", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-with-api" + ] + }, + { + "uid": "learn-dynamics.work-with-api.6-read", + "title": "Read, create, and update by using API", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/6-read", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-api" + ] + }, + { + "uid": "learn-dynamics.work-with-api.7-bound-actions", + "title": "API and OData bound actions", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/7-bound-actions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-api" + ] + }, + { + "uid": "learn-dynamics.work-with-api.9-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/9-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-with-api" + ] + }, + { + "uid": "learn-dynamics.work-with-codeunits.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-codeunits" + ] + }, + { + "uid": "learn-dynamics.work-with-codeunits.2-create-codeunit", + "title": "Create and access a codeunit", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits/2-create-codeunit", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.work-with-codeunits" + ] + }, + { + "uid": "learn-dynamics.work-with-codeunits.3-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits/3-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-codeunits" + ] + }, + { + "uid": "learn-dynamics.work-with-codeunits.4-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits/4-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-codeunits" + ] + }, + { + "uid": "learn-dynamics.work-with-codeunits.keyword", + "title": "Use keywords for codeunit self-reference", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits/keyword", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-codeunits" + ] + }, + { + "uid": "learn-dynamics.work-with-master-data-business-central.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/1-introduction", + "lastModified": "2026-08-18T23:35:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-master-data-business-central" + ] + }, + { + "uid": "learn-dynamics.work-with-master-data-business-central.2-setup-table-signature-design", + "title": "Use the setup table and Singleton Design Pattern", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/2-setup-table-signature-design", + "lastModified": "2026-08-18T23:35:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.work-with-master-data-business-central" + ] + }, + { + "uid": "learn-dynamics.work-with-master-data-business-central.3-master-table-page-standards", + "title": "Use master table and page standards", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/3-master-table-page-standards", + "lastModified": "2026-08-18T23:35:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.work-with-master-data-business-central" + ] + }, + { + "uid": "learn-dynamics.work-with-master-data-business-central.4-implement-series-pattern", + "title": "Implement the number series pattern", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/4-implement-series-pattern", + "lastModified": "2026-08-18T23:35:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-master-data-business-central" + ] + }, + { + "uid": "learn-dynamics.work-with-master-data-business-central.5-use-blocked-field", + "title": "Use the Blocked field and the Blocked Entity Design Pattern", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/5-use-blocked-field", + "lastModified": "2026-08-18T23:35:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-with-master-data-business-central" + ] + }, + { + "uid": "learn-dynamics.work-with-master-data-business-central.6-frequently-used-table-properties", + "title": "Implement frequently used table properties", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/6-frequently-used-table-properties", + "lastModified": "2026-08-18T23:35:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.work-with-master-data-business-central" + ] + }, + { + "uid": "learn-dynamics.work-with-master-data-business-central.6a-exercise", + "title": "Exercise - Add tables and pages for master data", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/6a-exercise", + "lastModified": "2026-08-18T23:35:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.work-with-master-data-business-central" + ] + }, + { + "uid": "learn-dynamics.work-with-master-data-business-central.7-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/7-check", + "lastModified": "2026-08-18T23:35:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-with-master-data-business-central" + ] + }, + { + "uid": "learn-dynamics.work-with-master-data-business-central.8-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/8-summary", + "lastModified": "2026-08-18T23:35:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-master-data-business-central" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.10-actions", + "title": "Define actions on a page and its properties", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/10-actions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 18, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.11-exercise", + "title": "Exercise - Create a Card page", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/11-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.12-exercise", + "title": "Exercise - Create a List page", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/12-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.13-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/13-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.14-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/14-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.2-get-started", + "title": "Get started with pages", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/2-get-started", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.3-types", + "title": "Identify different page types", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/3-types", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.4-properties", + "title": "Configure the different page properties", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/4-properties", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.5-layout", + "title": "Build a page layout by using controls", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/5-layout", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.6-link", + "title": "Link pages with page parts", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/6-link", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.7-snippets", + "title": "Use snippets in Visual Studio Code to create pages", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/7-snippets", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.8-controls", + "title": "Set different control properties", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/8-controls", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.9-search", + "title": "Enable users to search for a page", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/9-search", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.hidden-fields", + "title": "Add existing hidden fields to pages", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/hidden-fields", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.scan-barcodes", + "title": "Scan barcodes in the mobile app", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/scan-barcodes", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-pages.use-rich-text-editor", + "title": "Use the built-in rich text editor to enter data", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/use-rich-text-editor", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-pages" + ] + }, + { + "uid": "learn-dynamics.work-with-queries.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-queries" + ] + }, + { + "uid": "learn-dynamics.work-with-queries.10-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/10-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-queries" + ] + }, + { + "uid": "learn-dynamics.work-with-queries.2-get-started", + "title": "Get started with queries", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/2-get-started", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-queries" + ] + }, + { + "uid": "learn-dynamics.work-with-queries.3-create", + "title": "Create a query in Visual Studio Code", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/3-create", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-with-queries" + ] + }, + { + "uid": "learn-dynamics.work-with-queries.4-join-data", + "title": "Join data in queries", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/4-join-data", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-with-queries" + ] + }, + { + "uid": "learn-dynamics.work-with-queries.5-filter-data", + "title": "Filter data in queries", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/5-filter-data", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-with-queries" + ] + }, + { + "uid": "learn-dynamics.work-with-queries.6-aggregate-data", + "title": "Aggregate and sort data in a query", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/6-aggregate-data", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-queries" + ] + }, + { + "uid": "learn-dynamics.work-with-queries.7-al-function", + "title": "Access queries from AL", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/7-al-function", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.work-with-queries" + ] + }, + { + "uid": "learn-dynamics.work-with-queries.8-advanced-concepts", + "title": "Advanced query concepts", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/8-advanced-concepts", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-queries" + ] + }, + { + "uid": "learn-dynamics.work-with-queries.9-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/9-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-with-queries" + ] + }, + { + "uid": "learn-dynamics.work-with-queries.optimize-performance", + "title": "Optimize performance in Query objects", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/optimize-performance", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-queries" + ] + }, + { + "uid": "learn-dynamics.work-with-reports.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/1-introduction", + "lastModified": "2026-08-20T17:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-reports" + ] + }, + { + "uid": "learn-dynamics.work-with-reports.2-preview-run", + "title": "Preview and run reports", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/2-preview-run", + "lastModified": "2026-08-20T17:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.work-with-reports" + ] + }, + { + "uid": "learn-dynamics.work-with-reports.3-printers", + "title": "Set up printers", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/3-printers", + "lastModified": "2026-08-20T17:11:00Z", + "durationInMinutes": 16, + "moduleUids": [ + "learn-dynamics.work-with-reports" + ] + }, + { + "uid": "learn-dynamics.work-with-reports.4-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/4-check", + "lastModified": "2026-08-20T17:11:00Z", + "durationInMinutes": 4, + "moduleUids": [ + "learn-dynamics.work-with-reports" + ] + }, + { + "uid": "learn-dynamics.work-with-reports.5-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/5-summary", + "lastModified": "2026-08-20T17:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-reports" + ] + }, + { + "uid": "learn-dynamics.work-with-reports.set-layout", + "title": "Set the layout used by a report", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/set-layout", + "lastModified": "2026-08-20T17:11:00Z", + "durationInMinutes": 20, + "moduleUids": [ + "learn-dynamics.work-with-reports" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.10-snippets", + "title": "Create a table in Visual Studio Code with snippets", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/10-snippets", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.11-exercise", + "title": "Exercise - Create a table", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/11-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.12-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/12-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.13-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/13-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.2-get-started", + "title": "Get started with tables", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/2-get-started", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 2, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.3-types", + "title": "Identify the different table types", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/3-types", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.4-properties", + "title": "Configure the different table properties", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/4-properties", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 12, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.5-fields", + "title": "Define fields and their properties", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/5-fields", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.6-linking", + "title": "Link different tables with table relations", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/6-linking", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.7-keys", + "title": "Create primary and secondary keys on a table", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/7-keys", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 15, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.8-dropdown", + "title": "Work with DropDown and Brick field groups", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/8-dropdown", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.9-calculated", + "title": "Create and use calculated fields", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/9-calculated", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-tables.text-search", + "title": "Enable optimized text search on table fields", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/text-search", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-tables" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/1-introduction", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.10-use-soap", + "title": "Use SOAP web services", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/10-use-soap", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.11-request-records-soap", + "title": "Request records with SOAP", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/11-request-records-soap", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.12-ui-interaction", + "title": "Handle UI interaction when working with web services", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/12-ui-interaction", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.14-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/14-check", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.15-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/15-summary", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.2-differences", + "title": "Define differences between SOAP, OData, REST, and API", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/2-differences", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.3-authentication", + "title": "Web service authentication", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/3-authentication", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.4-enable-access", + "title": "Enable access to OData web services", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/4-enable-access", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.5-return-documents", + "title": "Work with different OData return documents", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/5-return-documents", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.6-request-records-odata", + "title": "Request records with OData", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/6-request-records-odata", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.7-query-options", + "title": "Work with OData query options", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/7-query-options", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.8-new-records", + "title": "Create new records with OData", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/8-new-records", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-web-services.9-update-records", + "title": "Update records with OData", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/9-update-records", + "lastModified": "2026-08-19T17:07:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-web-services" + ] + }, + { + "uid": "learn-dynamics.work-with-xmlports.1-introduction", + "title": "Introduction", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/1-introduction", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-xmlports" + ] + }, + { + "uid": "learn-dynamics.work-with-xmlports.10-summary", + "title": "Summary", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/10-summary", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-xmlports" + ] + }, + { + "uid": "learn-dynamics.work-with-xmlports.2-get-started", + "title": "Get started with XMLports", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/2-get-started", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 3, + "moduleUids": [ + "learn-dynamics.work-with-xmlports" + ] + }, + { + "uid": "learn-dynamics.work-with-xmlports.3-configure", + "title": "Configure different XMLport properties", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/3-configure", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 8, + "moduleUids": [ + "learn-dynamics.work-with-xmlports" + ] + }, + { + "uid": "learn-dynamics.work-with-xmlports.4-define-nodes", + "title": "Define nodes and their properties", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/4-define-nodes", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 6, + "moduleUids": [ + "learn-dynamics.work-with-xmlports" + ] + }, + { + "uid": "learn-dynamics.work-with-xmlports.5-design", + "title": "Design XMLports", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/5-design", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 7, + "moduleUids": [ + "learn-dynamics.work-with-xmlports" + ] + }, + { + "uid": "learn-dynamics.work-with-xmlports.6-create", + "title": "Create an XMLport by using snippets in Visual Studio Code", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/6-create", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-with-xmlports" + ] + }, + { + "uid": "learn-dynamics.work-with-xmlports.7-al-functions", + "title": "Use XMLports in AL code", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/7-al-functions", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 5, + "moduleUids": [ + "learn-dynamics.work-with-xmlports" + ] + }, + { + "uid": "learn-dynamics.work-with-xmlports.8-exercise", + "title": "Exercise - Create an XMLport to export XML data", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/8-exercise", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-with-xmlports" + ] + }, + { + "uid": "learn-dynamics.work-with-xmlports.9-check", + "title": "Check your knowledge", + "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/9-check", + "lastModified": "2026-06-11T23:11:00Z", + "durationInMinutes": 10, + "moduleUids": [ + "learn-dynamics.work-with-xmlports" + ] + } + ] +} diff --git a/evaluation/README.md b/evaluation/README.md index 7063bafa..40954eba 100644 --- a/evaluation/README.md +++ b/evaluation/README.md @@ -54,3 +54,50 @@ This credential-free check proves every selected leaf maps to a same-named knowl For a single combined stress-test result, use `-ResultsPath` instead. The committed gate requires full expected recall, the exact convention-derived article ID, and no findings on clean controls. + +## AL development evaluation + +`development-fixtures.json` defines end-to-end development requests rather than +prewritten good/bad snippets. Each case declares its execution mode, the +Business Central capabilities it exercises, the knowledge that should shape +the implementation, acceptance criteria, and the real checks an external +runner must perform. + +Validate the fixture and capability manifests and prepare opaque requests: + +```powershell +pwsh ./tools/Test-DevelopmentFixtures.ps1 -Root . -PrepareDirectory ./.development-evaluation +``` + +Each request runs `al-development` in a fresh writable AL repository. Cases +may exercise feature, bug, refactor, upgrade, or maintenance mode. +The runner compiles the generated project, runs its tests, invokes the review +quality gate, and stores the resulting implementation report using the opaque +`caseId` from its request, for example `result-case-a1b2c3d4.json`. It keeps the +generated repository available at the wrapper's `workspaceRoot` so scoring can +verify reported changed paths. Score all results with: + +```powershell +pwsh ./tools/Test-DevelopmentFixtures.ps1 -Root . -ResultsDirectory ./.development-evaluation +``` + +The initial fixtures cover setup-backed master data, document header/line +workflows, versioned API integrations, and surgical diagnosis and repair of a +batch-processing bug. The broader capability roadmap lives in +`coverage/development-capabilities.json`. + +### Read-only plan guidance + +`development-guidance-fixtures.json` evaluates the planning interface used by +specialized orchestrators. It supplies an existing development plan and expects +a referenced set of implementation constraints without any target-repository +changes. + +```powershell +pwsh ./tools/Test-DevelopmentGuidanceFixtures.ps1 -Root . -PrepareDirectory ./.development-guidance-evaluation +``` + +An external runner stores `result-.json` beside the generated request +and retains the clean fixture repository at `workspaceRoot`. Score the result +with `-ResultsDirectory`; the scorer verifies knowledge recall and precision +and fails if the planning pass changed the repository. diff --git a/evaluation/development-fixtures.json b/evaluation/development-fixtures.json new file mode 100644 index 00000000..dcedde4e --- /dev/null +++ b/evaluation/development-fixtures.json @@ -0,0 +1,188 @@ +{ + "version": 1, + "skill": "microsoft/skills/development/al-development.md", + "minimumKnowledgeRecall": 1.0, + "minimumKnowledgePrecision": 0.5, + "cases": [ + { + "id": "setup-backed-master-data", + "title": "Build setup-backed loyalty member master data", + "capabilities": [ + "setup-and-master-data" + ], + "expectedKind": "feature", + "development-request": { + "kind": "feature", + "description": "Add a Loyalty Member feature to an existing Business Central AL app. Administrators configure the member number series on a singleton setup card. Users create members through list and card pages, the table assigns numbers, and blocked members cannot be selected by consuming records. Include least-privilege permission sets and automated tests.", + "acceptance-criteria": [ + "The setup is a blank-key singleton surfaced by a Card page.", + "Member numbers use the current No. Series codeunit and support manual numbers according to setup.", + "Blocked validation occurs where a member is consumed, not only on the member table.", + "The feature includes assignable least-privilege permissions and automated tests." + ] + }, + "context": { + "technologies": [ + "al" + ], + "countries": [ + "w1" + ], + "application-area": [ + "all" + ] + }, + "requiredKnowledge": [ + "microsoft/knowledge/data-modeling/setup-table-is-a-singleton.md", + "microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.md", + "microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.md", + "microsoft/knowledge/security/permission-set-avoid-wildcard-grants.md", + "microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md" + ], + "optionalKnowledge": [ + "microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.md", + "microsoft/knowledge/security/compose-permission-sets-with-included-sets.md", + "microsoft/knowledge/style/applicationarea-required-on-page-controls.md", + "microsoft/knowledge/style/tooltip-required-on-page-fields.md", + "microsoft/knowledge/ui/showmandatory-on-code-required-page-fields.md" + ], + "requiredChecks": [ + "compile", + "tests", + "review" + ] + }, + { + "id": "document-header-and-lines", + "title": "Build a document header and lines workflow", + "capabilities": [ + "document-workflows" + ], + "expectedKind": "feature", + "development-request": { + "kind": "feature", + "description": "Implement a Service Quote feature with a header, lines, document page, number series, posting and document dates, calculated totals, and tests. Line edits must refresh the total shown on the header. Structure initialization so API, UI, and test creation paths behave consistently.", + "acceptance-criteria": [ + "The header assigns its number before InitRecord establishes document defaults.", + "The document page links lines correctly and refreshes parent totals after edits.", + "Tests exercise creation outside the UI as well as the document-page behavior.", + "The implementation contains no obsolete NoSeriesManagement dependency." + ] + }, + "context": { + "technologies": [ + "al" + ], + "countries": [ + "w1" + ], + "application-area": [ + "service" + ] + }, + "requiredKnowledge": [ + "microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md", + "microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.md", + "microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md", + "microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md" + ], + "optionalKnowledge": [ + "microsoft/knowledge/events/publish-thin-onbefore-onafter-integration-events.md", + "microsoft/knowledge/style/applicationarea-required-on-page-controls.md", + "microsoft/knowledge/style/tooltip-required-on-page-fields.md" + ], + "requiredChecks": [ + "compile", + "tests", + "review" + ] + }, + { + "id": "versioned-master-data-api", + "title": "Expose master data through a versioned API", + "capabilities": [ + "api-integrations" + ], + "expectedKind": "feature", + "development-request": { + "kind": "auto", + "plan": "Expose Loyalty Member master data through a Business Central API page. Use a stable v1.0 contract, address records by SystemId, support insert and update, use conventional entity naming, and include permissions and automated API-oriented tests.", + "acceptance-criteria": [ + "The API declares all routing properties and a stable APIVersion.", + "ODataKeyFields uses SystemId and the exposed SystemId field is not editable.", + "EntityName is singular, EntitySetName is plural, and both are lower camel case.", + "The API is covered by least-privilege permissions and automated tests." + ] + }, + "context": { + "technologies": [ + "al" + ], + "countries": [ + "w1" + ], + "application-area": [ + "all" + ] + }, + "requiredKnowledge": [ + "microsoft/knowledge/web-services/set-required-api-page-properties.md", + "microsoft/knowledge/web-services/expose-systemid-as-the-api-key.md", + "microsoft/knowledge/style/api-page-delayedinsert-true.md", + "microsoft/knowledge/style/api-page-entity-naming-singular-plural.md", + "microsoft/knowledge/security/permission-set-avoid-wildcard-grants.md" + ], + "optionalKnowledge": [ + "microsoft/knowledge/style/api-page-camelcase-properties.md", + "microsoft/knowledge/web-services/version-apis-by-adding-not-mutating-published-versions.md" + ], + "requiredChecks": [ + "compile", + "tests", + "review" + ] + }, + { + "id": "fix-filtered-batch-processing", + "title": "Fix a batch routine that processes only one record", + "capabilities": [ + "bug-diagnosis-and-fix" + ], + "expectedKind": "bug", + "development-request": { + "kind": "bug", + "description": "Users report that an existing filtered batch routine updates only the first matching record. Reproduce the defect, identify why iteration stops, make the smallest safe correction, and add a regression test that selects multiple records and proves every selected record is processed.", + "acceptance-criteria": [ + "The defect is reproduced or demonstrated by a failing regression test before the fix.", + "The root cause is corrected without widening the supplied record filters.", + "The routine processes every selected record with the appropriate update locking behavior.", + "A regression test covers more than one selected record." + ] + }, + "context": { + "technologies": [ + "al" + ], + "countries": [ + "w1" + ], + "application-area": [ + "all" + ] + }, + "requiredKnowledge": [ + "microsoft/knowledge/performance/pair-findset-with-next-loop.md", + "microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md" + ], + "optionalKnowledge": [ + "microsoft/knowledge/performance/findset-true-applies-updlock-on-read.md", + "microsoft/knowledge/performance/pass-var-record-to-preserve-partial-load-enumerator.md" + ], + "requiredChecks": [ + "compile", + "tests", + "review" + ] + } + ] +} diff --git a/evaluation/development-guidance-fixtures.json b/evaluation/development-guidance-fixtures.json new file mode 100644 index 00000000..9da4d0af --- /dev/null +++ b/evaluation/development-guidance-fixtures.json @@ -0,0 +1,50 @@ +{ + "version": 1, + "skill": "microsoft/skills/development/al-development-plan.md", + "minimumKnowledgeRecall": 1.0, + "minimumKnowledgePrecision": 0.5, + "cases": [ + { + "id": "bcapps-filtered-batch-bug-plan", + "title": "Select guidance for a filtered batch bug fix", + "development-plan": { + "kind": "bug", + "request": "Fix a filtered batch routine that updates only the first matching record.", + "root-cause": "The routine calls FindFirst and updates the current record without entering an enumerator loop.", + "affected-files": [ + "src/Batch/UpdateSelectedEntries.Codeunit.al", + "test/Batch/UpdateSelectedEntries.Codeunit.al" + ], + "proposed-changes": [ + "Iterate the supplied filtered record set and update every selected entry.", + "Add a regression test with multiple selected entries." + ], + "test-strategy": "Establish a red test where only one of several selected records is updated, then require all selected records to be updated after the fix.", + "acceptance-criteria": [ + "The supplied filters are preserved.", + "Every selected record is updated.", + "The regression test demonstrates red-to-green behavior." + ] + }, + "context": { + "technologies": [ + "al" + ], + "countries": [ + "w1" + ], + "application-area": [ + "all" + ] + }, + "requiredKnowledge": [ + "microsoft/knowledge/performance/pair-findset-with-next-loop.md", + "microsoft/knowledge/performance/findset-true-applies-updlock-on-read.md", + "microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md" + ], + "optionalKnowledge": [ + "microsoft/knowledge/performance/pass-var-record-to-preserve-partial-load-enumerator.md" + ] + } + ] +} diff --git a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.bad.al b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.bad.al new file mode 100644 index 00000000..0c87bb2a --- /dev/null +++ b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.bad.al @@ -0,0 +1,28 @@ +table 50603 "Sample Order Header Bad" +{ + fields + { + field(1; "No."; Code[20]) + { + DataClassification = CustomerContent; + } + field(2; "Document Date"; Date) + { + DataClassification = CustomerContent; + } + } + + trigger OnInsert() + var + SalesSetup: Record "Sales & Receivables Setup"; + NoSeries: Codeunit "No. Series"; + begin + "Document Date" := WorkDate(); + + if "No." = '' then begin + SalesSetup.Get(); + SalesSetup.TestField("Order Nos."); + "No." := NoSeries.GetNextNo(SalesSetup."Order Nos."); + end; + end; +} diff --git a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.good.al b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.good.al new file mode 100644 index 00000000..355d3d54 --- /dev/null +++ b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.good.al @@ -0,0 +1,45 @@ +table 50602 "Sample Order Header Good" +{ + fields + { + field(1; "No."; Code[20]) + { + DataClassification = CustomerContent; + } + field(2; "Document Date"; Date) + { + DataClassification = CustomerContent; + } + } + + trigger OnInsert() + var + SalesSetup: Record "Sales & Receivables Setup"; + NoSeries: Codeunit "No. Series"; + begin + if "No." = '' then begin + SalesSetup.Get(); + SalesSetup.TestField("Order Nos."); + "No." := NoSeries.GetNextNo(SalesSetup."Order Nos."); + end; + + InitRecord(); + end; + + procedure InitRecord() + begin + OnBeforeInitRecord(Rec); + "Document Date" := WorkDate(); + OnAfterInitRecord(Rec); + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeInitRecord(var SampleOrderHeader: Record "Sample Order Header Good") + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnAfterInitRecord(var SampleOrderHeader: Record "Sample Order Header Good") + begin + end; +} diff --git a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md new file mode 100644 index 00000000..fbdec6e4 --- /dev/null +++ b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md @@ -0,0 +1,30 @@ +--- +bc-version: [all] +domain: data-modeling +keywords: [document-header, initrecord, number-series, default-values, oninsert, initialization] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Initialize document defaults in `InitRecord` after assigning the number + +## Description + +Business Central document headers assign their number series first and then call an `InitRecord` procedure that owns the remaining business defaults, such as posting and document dates. Keeping that sequence and extensibility point makes initialization consistent for every creation path and lets extensions subscribe around one documented operation. Defaults scattered across page triggers or unrelated helpers can differ between UI, API, test, and background creation. + +## Best Practice + +In the document table's insert path, assign the document number and then call `InitRecord`. Keep the default assignments in that procedure and expose narrow before/after events when other extensions must participate. + +See sample: `initialize-document-defaults-in-initrecord.good.al`. + +## Anti Pattern + +Assigning document defaults in a page trigger, or scattering them directly through `OnInsert` with no `InitRecord` boundary. Non-page creation paths can then miss the defaults, and extensions have no stable initialization hook. + +See sample: `initialize-document-defaults-in-initrecord.bad.al`. + +## Reference + +[Use the InitRecord function](https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/3-use-initrecord-function) diff --git a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.bad.al b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.bad.al new file mode 100644 index 00000000..f8e11cca --- /dev/null +++ b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.bad.al @@ -0,0 +1,8 @@ +codeunit 50601 "Directed Rounding Bad" +{ + procedure FloorAmount(Value: Decimal; Precision: Decimal): Decimal + begin + // For negative values, '<' rounds toward zero rather than toward negative infinity. + exit(Round(Value, Precision, '<')); + end; +} diff --git a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.good.al b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.good.al new file mode 100644 index 00000000..cd8a80fc --- /dev/null +++ b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.good.al @@ -0,0 +1,10 @@ +codeunit 50600 "Directed Rounding Good" +{ + procedure RoundAmount(Value: Decimal; Precision: Decimal; IncreaseMagnitude: Boolean): Decimal + begin + if IncreaseMagnitude then + exit(Round(Value, Precision, '>')); + + exit(Round(Value, Precision, '<')); + end; +} diff --git a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md new file mode 100644 index 00000000..9fb0ba37 --- /dev/null +++ b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md @@ -0,0 +1,30 @@ +--- +bc-version: [all] +domain: data-modeling +keywords: [round, rounding, direction, precision, negative-decimal, amount] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# `Round` direction symbols follow magnitude, not mathematical ordering + +## Description + +AL's `Round(Number, Precision, Direction)` uses `'>'` to round away from zero and `'<'` to round toward zero. For a negative value this reverses mathematical ordering: `Round(-1234.56789, 0.001, '<')` returns `-1234.567`, while direction `'>'` returns `-1234.568`. Code that treats the symbols as mathematical ceiling and floor produces sign-dependent amount errors, commonly on credit documents and negative adjustments. + +## Best Practice + +Choose the direction from the business meaning: `'>'` increases absolute magnitude and `'<'` decreases absolute magnitude for both positive and negative values. Include positive and negative cases whenever a directed rounding rule is tested. + +See sample: `round-direction-symbols-use-magnitude.good.al`. + +## Anti Pattern + +Using `'<'` as a mathematical floor or `'>'` as a mathematical ceiling. The result looks correct for positive amounts but moves in the opposite mathematical direction for negative amounts. + +See sample: `round-direction-symbols-use-magnitude.bad.al`. + +## Reference + +[Use the Round function](https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/4a-use-round-function) diff --git a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.bad.al b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.bad.al new file mode 100644 index 00000000..4fd98be0 --- /dev/null +++ b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.bad.al @@ -0,0 +1,20 @@ +interface "I Quote Amount Bad" +{ + procedure GetAmount(): Decimal; +} + +interface "I Quote Date Bad" +{ + procedure GetDate(): Date; +} + +codeunit 50611 "Quote Reader Bad" +{ + procedure GetDate(Quote: Interface "I Quote Amount Bad"): Date + var + DatedQuote: Interface "I Quote Date Bad"; + begin + DatedQuote := Quote as "I Quote Date Bad"; + exit(DatedQuote.GetDate()); + end; +} diff --git a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.good.al b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.good.al new file mode 100644 index 00000000..f58a19d8 --- /dev/null +++ b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.good.al @@ -0,0 +1,24 @@ +interface "I Quote Amount Good" +{ + procedure GetAmount(): Decimal; +} + +interface "I Quote Date Good" +{ + procedure GetDate(): Date; +} + +codeunit 50610 "Quote Reader Good" +{ + procedure TryGetDate(Quote: Interface "I Quote Amount Good"; var QuoteDate: Date): Boolean + var + DatedQuote: Interface "I Quote Date Good"; + begin + if not (Quote is "I Quote Date Good") then + exit(false); + + DatedQuote := Quote as "I Quote Date Good"; + QuoteDate := DatedQuote.GetDate(); + exit(true); + end; +} diff --git a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.md b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.md new file mode 100644 index 00000000..1962e69e --- /dev/null +++ b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.md @@ -0,0 +1,30 @@ +--- +bc-version: [25..] +domain: interfaces +keywords: [interface, is-operator, as-operator, type-test, cast, variant, runtime-error] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Guard optional interface casts with `is` + +## Description + +From runtime 14.0, AL can type-test an interface or `Variant` with `is` and cast it to another interface with `as`. The test is non-throwing, but `as` raises a runtime error when the underlying codeunit does not implement the target interface. This matters when an extended capability is optional or implementations can come from other extensions. + +## Best Practice + +Use `is` to establish that the value supports the target interface before using `as`. Cast directly only where the target implementation is an invariant guaranteed by the surrounding contract. + +See sample: `guard-interface-casts-with-is.good.al`. + +## Anti Pattern + +Using `as` unconditionally for an optional extended interface. An otherwise valid implementation of the base interface then fails at runtime merely because it does not implement the additional contract. + +See sample: `guard-interface-casts-with-is.bad.al`. + +## Reference + +[Understand type testing and casting operators for interfaces](https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/type-testing) diff --git a/microsoft/knowledge/style/applicationarea-required-on-page-controls.bad.al b/microsoft/knowledge/style/applicationarea-required-on-page-controls.bad.al index 8da7777d..0e26a4bf 100644 --- a/microsoft/knowledge/style/applicationarea-required-on-page-controls.bad.al +++ b/microsoft/knowledge/style/applicationarea-required-on-page-controls.bad.al @@ -8,9 +8,6 @@ page 50375 "Sample App Area Bad" { group(General) { - // Anti-pattern: no ApplicationArea. AS0062 flags this control, - // and it is silently hidden in the Web client for profiles whose - // enabled areas do not already cover it. field("No."; Rec."No.") { ToolTip = 'Specifies the number that identifies the customer.'; @@ -23,3 +20,18 @@ page 50375 "Sample App Area Bad" } } } + +pageextension 50377 "Customer App Area Bad" extends "Customer Card" +{ + layout + { + addlast(General) + { + // Extension controls do not inherit ApplicationArea from the base page. + field("Language Code Sample"; Rec."Language Code") + { + ToolTip = 'Specifies the language used for the customer.'; + } + } + } +} diff --git a/microsoft/knowledge/style/applicationarea-required-on-page-controls.good.al b/microsoft/knowledge/style/applicationarea-required-on-page-controls.good.al index d3443371..76c48cff 100644 --- a/microsoft/knowledge/style/applicationarea-required-on-page-controls.good.al +++ b/microsoft/knowledge/style/applicationarea-required-on-page-controls.good.al @@ -2,6 +2,8 @@ page 50374 "Sample App Area Good" { PageType = Card; SourceTable = Customer; + ApplicationArea = All; + layout { area(Content) @@ -10,12 +12,10 @@ page 50374 "Sample App Area Good" { field("No."; Rec."No.") { - ApplicationArea = All; ToolTip = 'Specifies the number that identifies the customer.'; } field(Name; Rec.Name) { - ApplicationArea = All; ToolTip = 'Specifies the customer''s name.'; } } @@ -27,7 +27,6 @@ page 50374 "Sample App Area Good" { action(Refresh) { - ApplicationArea = All; ToolTip = 'Reloads the current record.'; trigger OnAction() @@ -38,3 +37,18 @@ page 50374 "Sample App Area Good" } } } + +pageextension 50376 "Customer App Area Good" extends "Customer Card" +{ + layout + { + addlast(General) + { + field("Language Code Sample"; Rec."Language Code") + { + ApplicationArea = All; + ToolTip = 'Specifies the language used for the customer.'; + } + } + } +} diff --git a/microsoft/knowledge/style/applicationarea-required-on-page-controls.md b/microsoft/knowledge/style/applicationarea-required-on-page-controls.md index 606e08a0..3a74a05d 100644 --- a/microsoft/knowledge/style/applicationarea-required-on-page-controls.md +++ b/microsoft/knowledge/style/applicationarea-required-on-page-controls.md @@ -1,28 +1,32 @@ --- bc-version: [all] domain: style -keywords: [application-area, page-control, as0062, appsourcecop, hidden-control, web-client] +keywords: [application-area, page-control, inheritance, as0062, appsourcecop, web-client] technologies: [al] countries: [w1] application-area: [all] --- -# Every page control needs an `ApplicationArea` (AppSourceCop AS0062) +# Page-level `ApplicationArea` inheritance does not apply to extensions ## Description -A field control on a page or pageextension that has no `ApplicationArea` property is silently hidden in the Web client for every profile whose enabled application areas do not cover it. There is no error and no warning at runtime — the field simply does not appear, which reads as data loss to the user. AppSourceCop AS0062 flags any page control or action that is missing the `ApplicationArea` property, and AppSource technical validation rejects the app until it is set. +A page control or action needs an effective `ApplicationArea` to appear in cloud experiences. From runtime 10.0, controls on a page object inherit the page-level value, so repeating it on every child is unnecessary when the parent defines a suitable default. This inheritance does not apply to controls added or modified by page and report extensions: extension controls must still set the property explicitly. -Set the property to an area the app actually enables. `All` makes the control visible under every profile and is the common default; if the app declares narrower areas in `app.json`, use one of those. The property applies to field controls and to actions. This is a sibling concern to `caption-required-on-page-fields.md` and `tooltip-required-on-page-fields.md`; note that the ToolTip requirement is the separate CodeCop rule AA0218, not AS0062. +For targets before runtime 10.0, child controls do not inherit and must also set the property. AppSourceCop AS0062 and PTE0008 account for page-level inheritance on runtime 10.0 and later but continue to require explicit values in extensions. ## Best Practice -Every field control and action carries `ApplicationArea = All;` (or a declared area of the app). The value is set once per control and keeps the control visible in the Web client. +On runtime 10.0 or later, set a suitable page-level default and override only controls that belong to a narrower area. Set `ApplicationArea` explicitly on every control or action introduced by a page or report extension. See sample: `applicationarea-required-on-page-controls.good.al`. ## Anti Pattern -A field control with no `ApplicationArea`. AS0062 flags it, and the control is invisible in the Web client for any profile that does not already enable a matching area. +A page object that defines neither a parent nor child value, or an extension control that assumes it inherits from the base page. The control has no effective application area and can be hidden or rejected by analyzer validation. See sample: `applicationarea-required-on-page-controls.bad.al`. + +## Reference + +[Set different control properties](https://learn.microsoft.com/en-us/training/modules/work-with-pages/8-controls) diff --git a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.bad.al b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.bad.al new file mode 100644 index 00000000..78602b4d --- /dev/null +++ b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.bad.al @@ -0,0 +1,9 @@ +tableextension 50622 "Ship-to Dropdown Bad" extends "Ship-to Address" +{ + fieldgroups + { + addlast(DropDown; "Address 2") + { + } + } +} diff --git a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.good.al b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.good.al new file mode 100644 index 00000000..c8a8c28c --- /dev/null +++ b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.good.al @@ -0,0 +1,20 @@ +tableextension 50620 "Ship-to Dropdown Good" extends "Ship-to Address" +{ + fieldgroups + { + addlast(DropDown; "Address 2") + { + } + } +} + +pageextension 50621 "Ship-to Lookup Good" extends "Ship-to Address List" +{ + layout + { + modify("Address 2") + { + Visible = true; + } + } +} diff --git a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md new file mode 100644 index 00000000..656d1a2b --- /dev/null +++ b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md @@ -0,0 +1,30 @@ +--- +bc-version: [all] +domain: ui +keywords: [fieldgroup, dropdown, addlast, lookup-page, visible, tableextension, pageextension] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# A `DropDown` field remains hidden when its lookup-page control is hidden + +## Description + +A tableextension can append a field to the `DropDown` field group with `addlast`, but the client still omits that field when its control on the underlying lookup page has `Visible = false`. Changing only the table field group therefore compiles while producing no visible UI change. The field-group name is case-sensitive and must be written as `DropDown`. + +## Best Practice + +When adding a hidden field to a `DropDown` field group, also extend the page used for the lookup and make that field control visible. Verify the actual lookup page rather than assuming the table definition alone controls the drop-down. + +See sample: `dropdown-fieldgroup-respects-lookup-page-visibility.good.al`. + +## Anti Pattern + +Adding the field with `addlast(DropDown; ...)` while leaving its lookup-page control hidden, then expecting the field to appear in the drop-down. + +See sample: `dropdown-fieldgroup-respects-lookup-page-visibility.bad.al`. + +## Reference + +[Add a new FieldGroup to an existing table](https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/add-field-group) diff --git a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.bad.al b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.bad.al new file mode 100644 index 00000000..f5d2b2be --- /dev/null +++ b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.bad.al @@ -0,0 +1,26 @@ +page 50631 "Sample Order Bad" +{ + PageType = Document; + SourceTable = "Sales Header"; + + layout + { + area(Content) + { + group(General) + { + field(Amount; Rec.Amount) + { + ApplicationArea = All; + ToolTip = 'Specifies the total amount of the order.'; + } + } + part(Lines; "Sales Order Subform") + { + ApplicationArea = All; + SubPageLink = "Document Type" = field("Document Type"), + "Document No." = field("No."); + } + } + } +} diff --git a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.good.al b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.good.al new file mode 100644 index 00000000..34620981 --- /dev/null +++ b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.good.al @@ -0,0 +1,27 @@ +page 50630 "Sample Order Good" +{ + PageType = Document; + SourceTable = "Sales Header"; + + layout + { + area(Content) + { + group(General) + { + field(Amount; Rec.Amount) + { + ApplicationArea = All; + ToolTip = 'Specifies the total amount of the order.'; + } + } + part(Lines; "Sales Order Subform") + { + ApplicationArea = All; + SubPageLink = "Document Type" = field("Document Type"), + "Document No." = field("No."); + UpdatePropagation = Both; + } + } + } +} diff --git a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md new file mode 100644 index 00000000..55324bdc --- /dev/null +++ b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md @@ -0,0 +1,30 @@ +--- +bc-version: [all] +domain: ui +keywords: [updatepropagation, page-part, subpage, main-page, refresh, flowfield, document-lines] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Use `UpdatePropagation = Both` when line edits must refresh the main page + +## Description + +A page part does not automatically refresh its parent page when the subpage changes. `UpdatePropagation = Subpage` updates only the part; `Both` also refreshes the main page. Without `Both`, header totals, FlowFields, and FactBoxes that depend on edited lines can remain stale until another user action refreshes the page. + +## Best Practice + +Set `UpdatePropagation = Both` on a part when edits in that subpage must immediately update values rendered by the main page. Leave propagation at `Subpage` when the parent has no dependent presentation to avoid unnecessary refreshes. + +See sample: `updatepropagation-both-refreshes-main-page.good.al`. + +## Anti Pattern + +Displaying a line-dependent total on the main page while the editable lines part updates only itself. The persisted values can be correct while the parent page continues to show an old total. + +See sample: `updatepropagation-both-refreshes-main-page.bad.al`. + +## Reference + +[Set different control properties](https://learn.microsoft.com/en-us/training/modules/work-with-pages/8-controls) diff --git a/microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md b/microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md new file mode 100644 index 00000000..61e74226 --- /dev/null +++ b/microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md @@ -0,0 +1,26 @@ +--- +bc-version: [all] +domain: upgrade +keywords: [appversion, dataversion, moduleinfo, install-codeunit, upgrade-codeunit, version-context] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# `ModuleInfo.AppVersion` changes meaning with execution context + +## Description + +`ModuleInfo.AppVersion()` is the installed version during normal operation, the version being installed inside install code, and the target version inside upgrade code. It is therefore not the source data version during an upgrade. In upgrade code, `DataVersion()` describes the version of the existing data, whether from the currently installed app or the version most recently uninstalled. + +## Best Practice + +Interpret `AppVersion()` as the code package entering the context and `DataVersion()` as the existing data state. Prefer upgrade tags for controlling individual migration steps; when version information is needed for diagnostics or preconditions, name variables so target app version and source data version cannot be confused. + +## Anti Pattern + +Reading `AppVersion()` from an upgrade codeunit and treating it as the version being upgraded from. The comparison actually observes the target package and can skip or misroute migration logic. + +## Reference + +[Create proper installation and upgrade codeunits](https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/3-installation-upgrade-codeunits) diff --git a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.bad.al b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.bad.al new file mode 100644 index 00000000..2a92d112 --- /dev/null +++ b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.bad.al @@ -0,0 +1,28 @@ +codeunit 50641 "Sample Upgrade Part One" +{ + Subtype = Upgrade; + + trigger OnUpgradePerCompany() + begin + CreateUpgradeState(); + end; + + local procedure CreateUpgradeState() + begin + end; +} + +codeunit 50642 "Sample Upgrade Part Two" +{ + Subtype = Upgrade; + + trigger OnUpgradePerCompany() + begin + // This can run before Part One; object IDs do not sequence upgrade codeunits. + MigrateDataThatRequiresUpgradeState(); + end; + + local procedure MigrateDataThatRequiresUpgradeState() + begin + end; +} diff --git a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.good.al b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.good.al new file mode 100644 index 00000000..42346a88 --- /dev/null +++ b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.good.al @@ -0,0 +1,18 @@ +codeunit 50640 "Sample Upgrade Good" +{ + Subtype = Upgrade; + + trigger OnUpgradePerCompany() + begin + CreateUpgradeState(); + MigrateDependentData(); + end; + + local procedure CreateUpgradeState() + begin + end; + + local procedure MigrateDependentData() + begin + end; +} diff --git a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md new file mode 100644 index 00000000..2271b2a4 --- /dev/null +++ b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md @@ -0,0 +1,30 @@ +--- +bc-version: [all] +domain: upgrade +keywords: [install-codeunit, upgrade-codeunit, execution-order, subtype-install, subtype-upgrade, sequencing] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Separate install or upgrade codeunits have no execution order + +## Description + +An extension can contain multiple `Install` or `Upgrade` codeunits, but Business Central does not guarantee the order in which codeunits of the same subtype execute. Upgrade trigger phases are ordered globally, yet one codeunit's `OnUpgradePerCompany` must not assume another codeunit's same-phase trigger already ran. Object ID and source-file order do not provide sequencing. + +## Best Practice + +Keep separate install or upgrade codeunits independent. When two steps have a real dependency, coordinate them from one owning trigger in the required order; use upgrade tags to make each completed step idempotent. + +See sample: `install-and-upgrade-codeunits-have-no-order.good.al`. + +## Anti Pattern + +Splitting dependent steps into separate codeunits and relying on names, object IDs, or declaration order. The dependent codeunit can run first and fail or observe partially migrated data. + +See sample: `install-and-upgrade-codeunits-have-no-order.bad.al`. + +## Reference + +[Create proper installation and upgrade codeunits](https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/3-installation-upgrade-codeunits) diff --git a/microsoft/skills/development/al-development-plan.md b/microsoft/skills/development/al-development-plan.md new file mode 100644 index 00000000..f824b683 --- /dev/null +++ b/microsoft/skills/development/al-development-plan.md @@ -0,0 +1,68 @@ +--- +kind: action-skill +id: al-development-plan +version: 1 +title: AL development plan guidance +description: Produces a read-only BCQuality knowledge bundle for an existing Business Central AL development plan. +inputs: [development-plan, repository] +outputs: [development-guidance-report] +bc-version: [all] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# AL development plan guidance + +Selects the BCQuality knowledge that should constrain an existing AL development plan. It does not implement, edit, stage, commit, or publish anything in the target repository. Repository-specific orchestrators can consume this skill before their own test and implementation phases while retaining ownership of workflow, tooling, and delivery. + +Both a readable `repository` and a non-empty `development-plan` are required. The plan may be structured data or text, but it must identify the intended change. Return `not-applicable` without changing files when either input is absent or the repository is not an AL project. + +## Source + +Read the BCQuality knowledge index once. Use entries from every enabled layer and domain. The index supplies candidate paths, applicability dimensions, keywords, titles, and descriptions; it never substitutes for opening selected articles in full. + +Inspect the target repository read-only for `app.json`, affected files and symbols named by the plan, relevant tests, permission sets, dependencies, target/runtime versions, countries, application areas, and repository conventions. Do not create scratch or generated files inside the target repository. + +## Relevance + +Apply READ's matching semantics using: + +- `bc-version` from the plan, target application, or supplied context; for upgrades, distinguish source and target versions. +- `technologies` from the affected files, beginning with `[al]`. +- `countries` from the plan, `app.json`, or workspace configuration. +- `application-area` from the plan and affected objects. + +When a dimension cannot be resolved, retain conditionally applicable candidates only when they can materially constrain the plan. Record the dimension in `context.unknown` and explain it in `unresolved`; do not silently treat it as a match. + +## Worklist + +1. Normalize the plan into: request summary, development kind, assumptions, root cause or design intent, affected files and symbols, proposed changes, test strategy, and acceptance criteria. When the plan has no normalized kind, apply the same categories as `al-development`: new or expanded behavior is `feature`, a defect correction is `bug`, behavior-preserving restructuring is `refactor`, migration is `upgrade`, and other bounded work is `maintenance`. A repository-specific additive event or extensibility request maps to `feature`; retain its original work-item type in the request summary. Do not redesign the repository-specific workflow. +2. Build retrieval vocabulary from the plan and confirmed repository symbols. Give exact object types, properties, methods, analyzers, errors, and affected domains more weight than broad business nouns. +3. Search the index in separate passes: + - data ownership, keys, setup, numbering, validation, transactions, and upgrade; + - behavior, events, interfaces, errors, permissions, privacy, and telemetry; + - pages, reports, APIs, integrations, localization, and accessibility; + - tests, analyzers, packaging, and deployment constraints. +4. Add an article when its keywords or indexed topic match a concrete planned change, affected symbol, acceptance criterion, or validation obligation. Applicability alone is not enough. +5. Open every selected article in full. Read any referenced `.good.*` and `.bad.*` sibling needed to make the constraint concrete. Never cite an index row that was not opened. +6. Resolve contradictory normative guidance with READ's layer precedence and record losing candidates in `suppressed`. +7. Check the resulting worklist across the whole plan. A bug fix may require testing, data, performance, and upgrade guidance at once; a feature plan may require security and lifecycle constraints that are not named in its title. + +Keep the worklist focused. Do not include generic engineering advice, an entire domain, or an article that would not change implementation or validation. + +## Action + +For each worklist article: + +1. Copy its exact path and optional commit SHA. +2. State `used-for` as the concrete plan decision or affected surface. +3. Translate its normative Best Practice and Anti Pattern into short implementation constraints without adding facts or weakening conditions. +4. Include only opened, existing sibling samples in `sample-paths`. +5. Derive validation considerations only where the plan or selected knowledge requires observable evidence. Describe the evidence to obtain; do not claim it already exists or passed. + +Do not change the target repository. Before emitting, verify every knowledge and sample path exists in the live BCQuality checkout and was opened during this run. If reference integrity cannot be established, return `failed` rather than fabricating guidance. + +## Output + +Return one `development-guidance-report` conforming to DO. `completed` requires that every selected article was opened and faithfully converted into constraints. `no-knowledge` is valid when the plan is applicable but BCQuality contains no relevant article. `partial` names every unevaluated candidate or unresolved applicability gap. diff --git a/microsoft/skills/development/al-development.md b/microsoft/skills/development/al-development.md new file mode 100644 index 00000000..c4eefd74 --- /dev/null +++ b/microsoft/skills/development/al-development.md @@ -0,0 +1,91 @@ +--- +kind: action-skill +id: al-development +version: 1 +title: AL development +description: Implements Business Central AL features, bug fixes, refactors, upgrades, and maintenance changes using BCQuality knowledge. +inputs: [development-request, repository] +outputs: [implementation-report] +bc-version: [all] +technologies: [al] +countries: [w1] +application-area: [all] +guidance-skill: microsoft/skills/development/al-development-plan.md +quality-skill: microsoft/skills/review/al-code-review.md +--- + +# AL development + +Implements a Business Central change in an existing AL repository. Feature work, bug fixing, refactoring, upgrades, and maintenance share one public contract and one quality pipeline; their different investigation disciplines are execution modes within this skill. + +Both a writable `repository` and a `development-request` are required. A structured request has this shape: + +```yaml +development-request: + kind: auto # feature | bug | refactor | upgrade | maintenance + description: string # optional when plan states the requested outcome + plan: string # optional + acceptance-criteria: [string] # optional +``` + +A plain-text request is normalized to `kind: auto` with the text as `description`. A plan-only request is valid when the plan states the requested outcome. Return `not-applicable` without changing files when either input is absent, both description and plan are empty, or the repository is not an AL project. + +## Source + +Read the frontmatter `guidance-skill`; it owns BCQuality discovery and returns the knowledge constraints for the implementation plan. Inspect the target repository for `app.json`, existing objects, tests, permission sets, analyzers, build scripts, naming and object-ID conventions, dependencies, target/runtime versions, localization layout, and uncommitted user changes. For bugs, refactors, and upgrades, inspect enough history and surrounding code to establish the behavior being changed. + +## Relevance + +Resolve and pass this context to the guidance-skill: + +- `bc-version` from the target application's platform/application/runtime settings or supplied context. For an upgrade, distinguish source and target versions. +- `technologies: [al]`, plus any additional technology actually required by the request. +- `countries` from `app.json`, workspace configuration, or supplied context. +- `application-area` from the request and affected objects. + +Record unresolved dimensions in the development plan rather than silently substituting broad values. The guidance-skill applies READ's matching semantics and returns any conditional applicability in its report. + +## Worklist + +1. Normalize the request, deriving a concise description from a plan-only input, and classify `kind: auto` as: + - `feature` for new or intentionally expanded behavior; + - `bug` for observed behavior that contradicts an expected result; + - `refactor` for structural change with no intended behavior change; + - `upgrade` for schema, data, dependency, runtime, or application-version migration; + - `maintenance` for bounded development work that fits none of the above. + Preserve an explicit valid kind. When repository evidence conflicts with it, record the mismatch and ask for clarification before changing files rather than silently switching disciplines. +2. Establish the mode-specific implementation contract: + - **Feature:** define user-visible behavior and cover data lifecycle, UI/API, permissions, extensibility, upgrade impact, telemetry, and tests where applicable. + - **Bug:** state expected versus actual behavior, reproduce or otherwise prove the defect, trace the root cause, and define a regression test that fails for that cause. + - **Refactor:** identify the behavior and public contracts that must remain invariant, plus the checks that establish a before/after baseline. + - **Upgrade:** identify source and target states, data migration, compatibility, idempotency, and validation requirements. + - **Maintenance:** define the bounded outcome and the behavior that must not change. +3. Treat a supplied plan as an input constraint, not as proof. Reconcile it with repository reality and BCQuality; preserve its intent, correct unsafe assumptions, and record consequential deviations. +4. Discover existing implementation patterns and reusable objects before proposing new ones. Preserve repository conventions and current user changes. +5. Materialize a `development-plan` containing the classified kind, request, assumptions, affected files and symbols, design or root cause, proposed changes, validation strategy, and acceptance criteria. +6. Invoke the frontmatter `guidance-skill` with that plan, the repository, and the resolved context. It performs Source, Relevance, and knowledge worklisting independently and read-only. +7. Require a complete guidance result before editing product code: + - `completed` — use every returned constraint and validation consideration. + - `no-knowledge` — return `no-knowledge` without implementing a Business Central-specific change. + - `not-applicable`, `partial`, or `failed` — return the corresponding non-completed outcome without editing product code; preserve its reason in `remaining`. +8. Copy the guidance report's selected paths into the eventual implementation report only when the corresponding constraint materially shaped the implementation. Carry its suppression records forward. + +## Action + +1. Record the starting working-tree state so unrelated changes are preserved and excluded from `changes`. +2. Apply the execution mode: + - **Feature:** implement the smallest complete vertical slice; do not leave placeholder surfaces. + - **Bug:** reproduce first when feasible, fix the root cause rather than the symptom, keep the patch surgical, and add a regression test. + - **Refactor:** capture a behavioral baseline, avoid unrelated behavior changes, and prove the declared invariants afterward. + - **Upgrade:** make migrations rerunnable where required, preserve data and compatibility, and validate both upgraded and fresh-install paths when applicable. + - **Maintenance:** make only the bounded requested change and preserve surrounding behavior. +3. Produce a coherent design that satisfies the implementation contract and every constraint returned by the guidance-skill. Reuse existing abstractions and object ranges. Do not hard-code a Business Central fact in this skill or invent a rule absent from both the repository and reliable platform knowledge. +4. Implement the request end to end. Include all surfaces required by the mode, acceptance criteria, and repository conventions. Do not create success-shaped stubs. +5. Treat the guidance report as design constraints throughout implementation. Adapt its referenced companion samples to the target codebase; never copy demonstration IDs or names blindly. +6. Run the smallest existing build, analyzer, and test commands that cover the change. Fix failures caused by the implementation. Record every command and real outcome in `validation`; unavailable checks are `not-run`, never `passed`. +7. Invoke the frontmatter `quality-skill` against the final implementation diff. Fix all justified knowledge-backed `blocker` and `major` findings and concrete defects introduced by this work, then rerun affected validation and review. Preserve the last findings-report in `review` and add a `validation` entry with `id: "review"`. If review is disabled or unavailable, record `not-run` and return `partial`. +8. Verify the persisted files against the implementation contract, acceptance criteria, and mode-specific evidence. If behavior, validation, or review remains incomplete, return `partial` and list the exact gap in `remaining`. + +## Output + +Return one `implementation-report` conforming to DO. Set `plan.kind` to the classified execution mode. `knowledge` lists only articles opened in full and materially used. `changes` lists only files changed by this skill. `completed` requires a persisted implementation, passing required validation, and no unresolved `blocker` or `major` finding in `review`. diff --git a/microsoft/skills/review/al-data-modeling-review.md b/microsoft/skills/review/al-data-modeling-review.md index 2386613a..234589ec 100644 --- a/microsoft/skills/review/al-data-modeling-review.md +++ b/microsoft/skills/review/al-data-modeling-review.md @@ -39,7 +39,7 @@ Narrow the relevant files to the subset that applies to the changes under review - The changed AL object names and types — especially `* Setup` singleton tables and Card pages, custom master tables, tableextensions that add master-data fields, and document or journal lines that reference a master. - The changed fields, keys, triggers, and procedures, weighted toward `Primary Key`, `No.`, `No. Series`, `Blocked`, `Last Date Modified`, `OnInsert`, `OnModify`, `OnRename`, reference-field `OnValidate`, and posting validation. -- Tokens extracted from the diff that relate to data modeling (`setup`, `master`, `Primary Key`, `Code[10]`, `Code[20]`, `AutoIncrement`, `SystemId`, `No.`, `No. Series`, `NoSeriesManagement`, `Codeunit "No. Series"`, `GetNextNo`, `IsManual`, `TestManual`, `Blocked`, `TestField`, `Last Date Modified`, `Today`, `WorkDate`, `InsertAllowed`, `DeleteAllowed`, `PageType = Card`, `OnOpenPage`, `GetRecordOnce`, `OnInsert`, `OnModify`, `OnRename`, `TableRelation`, `tableextension`, `enumextension`, `Media`, `MediaSet`, `Item`, `Count`). +- Tokens extracted from the diff that relate to data modeling (`setup`, `master`, `Primary Key`, `Code[10]`, `Code[20]`, `AutoIncrement`, `SystemId`, `No.`, `No. Series`, `NoSeriesManagement`, `Codeunit "No. Series"`, `GetNextNo`, `IsManual`, `TestManual`, `Blocked`, `TestField`, `Last Date Modified`, `Today`, `WorkDate`, `InsertAllowed`, `DeleteAllowed`, `PageType = Card`, `OnOpenPage`, `GetRecordOnce`, `OnInsert`, `OnModify`, `OnRename`, `InitRecord`, `Round`, `Precision`, `Direction`, `TableRelation`, `tableextension`, `enumextension`, `Media`, `MediaSet`, `Item`, `Count`). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. When the diff contains no data-modeling changes by any of the above signals, return `outcome: "not-applicable"` without evaluating files. @@ -52,6 +52,8 @@ The following targeted checks cover every current `data-modeling` article. Treat - A master table adds or changes `Last Date Modified`, `OnModify`, or `OnRename`, but the non-editable field is not assigned `Today()` in both triggers — `set-last-date-modified-in-onmodify-and-onrename`. - A `tableextension` appends a conditional `TableRelation` as if it overrides an earlier unconditional relation, or relation branches are otherwise designed without accounting for additive top-down evaluation — `table-relation-extensions-are-additive-and-top-down`. - A `Media` or `MediaSet` field is assigned directly between different table types or different field IDs instead of registering each shared item with `MediaSet.Insert` — `share-mediaset-items-with-insert-not-field-assignment`. +- A custom document header assigns defaults outside an `InitRecord` boundary, calls `InitRecord` before assigning its number, or places UI-independent defaults only in a page trigger — `initialize-document-defaults-in-initrecord`. +- Directed `Round` calls use `'<'` as mathematical floor or `'>'` as mathematical ceiling, especially where negative amounts are possible — `round-direction-symbols-use-magnitude`. Once the candidate worklist is known, resolve layer-precedence conflicts per READ. Drop lower-precedence files whose normative guidance (`## Best Practice` or `## Anti Pattern`) directly contradicts a higher-precedence candidate, and record each dropped file in `suppressed` with `reason: "layer-precedence"`. Files that would have been candidates but are hidden because their layer is disabled in consumer configuration are recorded with `reason: "configuration"`. Files that never became candidates are NOT recorded in `suppressed`. diff --git a/microsoft/skills/review/al-interfaces-review.md b/microsoft/skills/review/al-interfaces-review.md index 0031e8f9..59c93332 100644 --- a/microsoft/skills/review/al-interfaces-review.md +++ b/microsoft/skills/review/al-interfaces-review.md @@ -39,7 +39,7 @@ Narrow the relevant files to the subset that applies to the changes under review - The changed AL object names and types — especially `interface` objects, codeunits and enums declared with the `implements` keyword, and consumers that declare or assign an `Interface` variable. - The changed procedures and triggers, weighted toward factory or dispatch routines that resolve a variant to behaviour, setter-injection procedures that take an `Interface` parameter, and `case`-over-enum blocks that select between strategies. -- Tokens extracted from the diff that relate to interfaces and enum-backed implementation (`interface`, `extends`, `implements`, `Implementation`, `DefaultImplementation`, `UnknownValueImplementation`, `enum`, `Extensible`, `Interface`, `case`, and the `case of` anti-pattern signal — a `case` over an enum value whose branches choose between variant computations). +- Tokens extracted from the diff that relate to interfaces and enum-backed implementation (`interface`, `extends`, `implements`, `Implementation`, `DefaultImplementation`, `UnknownValueImplementation`, `enum`, `Extensible`, `Interface`, `Variant`, `is`, `as`, `case`, and the `case of` anti-pattern signal — a `case` over an enum value whose branches choose between variant computations). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. @@ -54,6 +54,7 @@ The following targeted checks map diff signals to specific `interfaces` articles - `DefaultImplementation` used as the only fallback where a persisted ordinal may no longer match any declared enum value, or a persisted enum lacks `UnknownValueImplementation` on BC18 or later — `handle-unknown-enum-ordinals-with-unknownvalueimplementation`. - A method added directly to an interface that exists in the baseline, instead of adding a BC25+ interface that `extends` it or a versioned sibling for older targets — `extend-published-interfaces-dont-edit-them`. - A declared enum value with no `Implementation` and no enum-level `DefaultImplementation` — `set-defaultimplementation-on-enum`. +- An `Interface` or `Variant` is cast with `as` to an optional extended interface without first establishing support with `is` — `guard-interface-casts-with-is`. For `set-defaultimplementation-on-enum`, inspect the complete containing enum before emitting. An enum-level `DefaultImplementation = = ;` conclusively covers every declared value that omits its own `Implementation`; do not flag such a value and do not replace the intentional fallback with a per-value mapping. diff --git a/microsoft/skills/review/al-style-review.md b/microsoft/skills/review/al-style-review.md index bffef4de..d89d74e1 100644 --- a/microsoft/skills/review/al-style-review.md +++ b/microsoft/skills/review/al-style-review.md @@ -41,7 +41,7 @@ Narrow the relevant files to the subset that applies to the changes under review - Changed AL objects — especially API pages (`PageType = API`), tables and pages declaring Labels/TextConsts, codeunits issuing `Error`/`Message`/`Confirm`, and any file whose name violates the `..al` convention. - Changed declarations, weighted toward `: Label '...'`, `: TextConst '...'`, temporary record variables, option fields, error-handling call sites, and codeunit-internal method calls. -- Tokens extracted from the diff (`Label`, `TextConst`, `Locked`, `Comment`, `MaxLength`, `temporary`, `OptionMembers`, `OptionCaption`, `APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `DelayedInsert`, `FieldCaption`, `TableCaption`, `FieldName`, `TableName`, `Page.RunModal`, `Report.Run`, `this.`, `StrSubstNo`). +- Tokens extracted from the diff (`Label`, `TextConst`, `Locked`, `Comment`, `MaxLength`, `temporary`, `OptionMembers`, `OptionCaption`, `ApplicationArea`, `APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `DelayedInsert`, `FieldCaption`, `TableCaption`, `FieldName`, `TableName`, `Page.RunModal`, `Report.Run`, `this.`, `StrSubstNo`). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object or declaration. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. @@ -51,6 +51,7 @@ Apply these high-signal mappings before fuzzy topic ranking: - A `Label` or `TextConst` contains multiple or ambiguous placeholders but has no `Comment`, or its Comment does not explain every placeholder — `label-comment-explains-placeholders.md`. A single placeholder whose meaning is explicit in the text, such as `Customer %1`, is allowed without a Comment and must not be flagged. - `function-call-parentheses-required.md` applies only to a zero-argument invocation written without `()`. Never worklist it from an invocation that already has parentheses or supplies arguments, including `Error(Label, Arg1, Arg2)`. +- On runtime 10.0 or later, a page child may inherit `ApplicationArea` from its page object; do not flag that shape. A control added by a page or report extension still requires an explicit value — `applicationarea-required-on-page-controls.md`. Once the candidate worklist is known, resolve layer-precedence conflicts per READ and record suppressions. diff --git a/microsoft/skills/review/al-ui-review.md b/microsoft/skills/review/al-ui-review.md index c0af5af8..f26cf480 100644 --- a/microsoft/skills/review/al-ui-review.md +++ b/microsoft/skills/review/al-ui-review.md @@ -41,10 +41,15 @@ Narrow the relevant files to the subset that applies to the changes under review - **UI-file filter.** UI review applies to files declaring `page`, `pageextension`, or `pagecustomization`, and to JavaScript/CSS/HTML that implements a control add-in's rendering or Business Central communication. When the diff contains no such files, return `outcome: "not-applicable"` without evaluating knowledge files. - For each relevant knowledge file, compute overlap against changed page declarations and control add-in files, weighted toward `Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `OptionCaption`, `ShowCaption`, `InstructionalText`, `GridLayout`, `Style`, `StyleExpr`, promoted action definitions, field importance, page background tasks, DOM creation, ARIA attributes, keyboard/focus handlers, packaged-resource AJAX, and calls from JavaScript into AL. -- Tokens extracted from the diff (`Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `PageType`, `ShowCaption`, `InstructionalText`, `grid`, `fixed`, `GridLayout`, `Style`, `StyleExpr`, `Importance`, `Promoted`, `Additional`, `area(Promoted)`, `actionref`, `PromotedCategory`, `PromotedOnly`, `PromotedIsBig`, `ShowAs`, `SplitButton`, `EnqueueBackgroundTask`, `OnAfterGetCurrRecord`, `OnAfterGetRecord`, `OnPageBackgroundTaskCompleted`, `OnPageBackgroundTaskError`, `RunPageBackgroundTask`, `Favorable`, `Unfavorable`, `Ambiguous`, `cuegroup`, `controladdin`, `control-add-in`, `usercontrol`, `aria-`, `tabindex`, `keydown`, `focus`, `innerHTML`, `createElement`, `packaged-resource`, `ajax`, `$.get`, `$.ajax`, `XMLHttpRequest`, `xhrFields`, `withCredentials`, `withcredentials`, `InvokeExtensibilityMethod`, `invokeextensibilitymethod`, `skipIfBusy`, `successCallback`, `success-callback`, `errorCallback`, `setInterval`, `JSON.stringify`, `payload`, `throttling`, `reduced-functionality`, `ClientServicesMaxUploadSize`, `&`, `Specifies`, `Message(`, `Confirm(`, `Error(` in a page context, `Disabled`, `Invalid`, `Whitelist`, `Blacklist`, trailing punctuation patterns on captions). +- Tokens extracted from the diff (`Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `PageType`, `ShowCaption`, `InstructionalText`, `grid`, `fixed`, `GridLayout`, `Style`, `StyleExpr`, `Importance`, `Promoted`, `Additional`, `area(Promoted)`, `actionref`, `PromotedCategory`, `PromotedOnly`, `PromotedIsBig`, `ShowAs`, `SplitButton`, `fieldgroups`, `DropDown`, `UpdatePropagation`, `EnqueueBackgroundTask`, `OnAfterGetCurrRecord`, `OnAfterGetRecord`, `OnPageBackgroundTaskCompleted`, `OnPageBackgroundTaskError`, `RunPageBackgroundTask`, `Favorable`, `Unfavorable`, `Ambiguous`, `cuegroup`, `controladdin`, `control-add-in`, `usercontrol`, `aria-`, `tabindex`, `keydown`, `focus`, `innerHTML`, `createElement`, `packaged-resource`, `ajax`, `$.get`, `$.ajax`, `XMLHttpRequest`, `xhrFields`, `withCredentials`, `withcredentials`, `InvokeExtensibilityMethod`, `invokeextensibilitymethod`, `skipIfBusy`, `successCallback`, `success-callback`, `errorCallback`, `setInterval`, `JSON.stringify`, `payload`, `throttling`, `reduced-functionality`, `ClientServicesMaxUploadSize`, `&`, `Specifies`, `Message(`, `Confirm(`, `Error(` in a page context, `Disabled`, `Invalid`, `Whitelist`, `Blacklist`, trailing punctuation patterns on captions). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed page element. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. +Apply these high-signal mappings before fuzzy topic ranking: + +- A tableextension adds a field to `DropDown` while the corresponding lookup-page control remains `Visible = false` — `dropdown-fieldgroup-respects-lookup-page-visibility`. +- An editable page part affects a total, FlowField, or FactBox on the parent but does not set `UpdatePropagation = Both` — `updatepropagation-both-refreshes-main-page`. + Once the candidate worklist is known, resolve layer-precedence conflicts per READ and record suppressions. When the post-conflict worklist is empty because no applicable UI knowledge exists, or because configuration suppressed every candidate, emit `outcome: "no-knowledge"`. When the worklist is empty because no applicable UI knowledge matched the page changes, emit `outcome: "completed"` with an empty `findings` array. diff --git a/microsoft/skills/review/al-upgrade-review.md b/microsoft/skills/review/al-upgrade-review.md index 667ea32e..4d691185 100644 --- a/microsoft/skills/review/al-upgrade-review.md +++ b/microsoft/skills/review/al-upgrade-review.md @@ -39,10 +39,12 @@ Narrow the relevant files to the subset that applies to the changes under review - The changed AL object names and types — especially codeunits with `Subtype = Upgrade` or `Subtype = Install`, tables and tableextensions adding or changing fields, enums and enumextensions, and objects under `Hybrid*`/`Migration`/`Upgrade` namespaces. - The changed triggers and procedures, weighted toward `OnCheckPreconditionsPerCompany`/`PerDatabase`, `OnUpgradePerCompany`/`PerDatabase`, `OnValidateUpgradePerCompany`/`PerDatabase`, `OnInstallAppPerCompany`/`PerDatabase`, the `OnGetPerCompanyUpgradeTags`/`OnGetPerDatabaseUpgradeTags` subscribers, and helper procedures transitively reachable from those entry points. -- Tokens extracted from the diff that relate to upgrade concerns (`Subtype = Upgrade`, `Subtype = Install`, `Upgrade Tag`, `HasUpgradeTag`, `SetUpgradeTag`, `OnCheckPreconditions`, `OnUpgrade`, `OnValidateUpgrade`, `OnInstallApp`, `DataTransfer`, `CopyFields`, `Insert`, `Modify`, `Delete`, `Rename`, `InitValue`, `ObsoleteState`, `ObsoleteReason`, `ObsoleteTag`, `DataVersion`, `ExecutionContext`, `PrimaryKey`, `key(`, `field(`, `value(`, `enum`, `enumextension`, `HybridSL`, `HybridGP`, `HybridBC`, `HybridBaseDeployment`). +- Tokens extracted from the diff that relate to upgrade concerns (`Subtype = Upgrade`, `Subtype = Install`, `Upgrade Tag`, `HasUpgradeTag`, `SetUpgradeTag`, `OnCheckPreconditions`, `OnUpgrade`, `OnValidateUpgrade`, `OnInstallApp`, `DataTransfer`, `CopyFields`, `Insert`, `Modify`, `Delete`, `Rename`, `InitValue`, `ObsoleteState`, `ObsoleteReason`, `ObsoleteTag`, `ModuleInfo`, `AppVersion`, `DataVersion`, `NavApp.GetCurrentModuleInfo`, `ExecutionContext`, `PrimaryKey`, `key(`, `field(`, `value(`, `enum`, `enumextension`, `HybridSL`, `HybridGP`, `HybridBC`, `HybridBaseDeployment`). - For each `OnCheckPreconditions...` and `OnValidateUpgrade...` trigger, build the best available call graph from surrounding unchanged source as well as changed hunks, tracing resolved calls through reachable local or internal helpers. Worklist the check-only rule when a database write occurs either directly in the trigger or in any helper procedure reachable from it. Writes include `Insert`, `Modify`, `ModifyAll`, `Delete`, `DeleteAll`, `Rename`, and `DataTransfer`. Also perform the reverse check when a PR changes a writing helper body: worklist the rule when that helper is invoked directly or transitively by an unchanged check or validation trigger. - Treat a direct write or a fully resolved call chain as high-confidence evidence. When cross-object dispatch, unavailable declarations, or an incomplete call graph prevents proving the complete chain, cap confidence at `medium`, name the unresolved edge in the finding, and do not claim a violation without a resolved path from a check or validation trigger to a write. - Worklist the install-versus-upgrade rule when migration helpers are reachable only from an install codeunit. +- Worklist `install-and-upgrade-codeunits-have-no-order.md` when a change adds multiple install or upgrade codeunits whose same-phase triggers share state or depend on one another. +- Worklist `appversion-meaning-depends-on-execution-context.md` when install or upgrade code branches on `ModuleInfo.AppVersion()` or confuses it with `DataVersion()`. A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. When the diff contains no upgrade-related changes by any of the above signals, return `outcome: "not-applicable"` without evaluating files. diff --git a/plugin.json b/plugin.json index c99785b0..d74e69a2 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "name": "bcquality", - "description": "Quality skills and knowledge for Business Central development. Exposes a standalone AL review adapter backed by BCQuality's Entry protocol.", - "version": "0.2.0", + "description": "Quality skills and knowledge for Business Central development. Exposes AL development and code-review adapters backed by BCQuality's Entry protocol.", + "version": "0.3.0", "author": { "name": "microsoft/BCQuality", "url": "https://github.com/microsoft/BCQuality" @@ -13,6 +13,7 @@ "al", "business-central", "code-review", + "development", "quality" ], "skills": [ diff --git a/skills/README.md b/skills/README.md index 3d8fdfb5..a5f78efc 100644 --- a/skills/README.md +++ b/skills/README.md @@ -1,7 +1,7 @@ # BCQuality global skills This folder contains BCQuality's layer-independent protocol files and the -host-native adapter used by standalone plugin installations. +host-native adapters used by standalone plugin installations. The protocol files have two kinds: @@ -31,8 +31,9 @@ READ and DO are read on demand — typically by the first action skill the agent | Path | Role | |---|---| | [`al-code-review/SKILL.md`](al-code-review/SKILL.md) | Exposes BCQuality through the standard `SKILL.md` format when this repository is installed as a plugin. | +| [`al-development/SKILL.md`](al-development/SKILL.md) | Exposes knowledge-backed AL development through the standard `SKILL.md` format. | -The adapter is deliberately thin. It translates the caller's request into an +Each adapter is deliberately thin. It translates the caller's request into an Entry task context, then follows Entry's dispatch without owning routing, review, index, or output policy. It is not an action skill, is not considered by Entry, and should not accumulate behavior already defined by `entry.md`, @@ -40,20 +41,23 @@ by Entry, and should not accumulate behavior already defined by `entry.md`, This gives the two skill formats distinct roles: -- `skills/al-code-review/SKILL.md` is the public host integration surface for a - standalone plugin installation. +- `skills/al-code-review/SKILL.md` and + `skills/al-development/SKILL.md` are the public host integration + surfaces for a standalone plugin installation. - `microsoft/skills/review/al-code-review.md` is BCQuality's internal Microsoft-layer super-skill for coordinating a broad AL review. - -The host adapter and internal coordinator deliberately share the -`al-code-review` name because they represent the same user-facing operation in -their respective formats. Their locations distinguish their roles. The -adapter remains distinct from BC-ALAgents' separately installed `al-review` -skill, avoiding a collision in hosts that use one shared skill inventory. The -reference from the adapter to Entry, and from a dispatched super-skill to its -leaf skills, is intentional progressive disclosure. It avoids registering -every internal BCQuality protocol file as an ambient host skill while allowing -each review domain to run in an isolated context. +- `microsoft/skills/development/al-development.md` is the internal + Microsoft-layer implementation skill for all supported development modes. +- `microsoft/skills/development/al-development-plan.md` is the read-only + planning interface for repository-specific orchestrators that retain + implementation ownership. + +Each host adapter deliberately shares its name with the internal action skill +for the same operation. Their locations distinguish the host integration from +the layered policy. `al-code-review` remains distinct from BC-ALAgents' +separately installed `al-review` skill, avoiding a collision in hosts that use +one shared skill inventory. References from adapters to Entry, and from a +dispatched super-skill to its leaves, are intentional progressive disclosure. These contracts are stable. Changes require a PR approved by both maintainers. diff --git a/skills/al-development/SKILL.md b/skills/al-development/SKILL.md new file mode 100644 index 00000000..5f769801 --- /dev/null +++ b/skills/al-development/SKILL.md @@ -0,0 +1,29 @@ +--- +name: al-development +description: Implement Business Central AL features, bug fixes, refactors, upgrades, and maintenance changes using BCQuality's curated platform knowledge. +--- + +# AL development + +This is BCQuality's host-native adapter for standalone plugin installations. It translates a coding request into Entry's task context; the internal action skill owns classification, investigation, design, implementation, validation, and review policy. + +When the target repository exposes a more specific local workflow for the request, such as an end-to-end bug-fix skill with its own environment and delivery gates, prefer that repository workflow unless the caller explicitly asks to use BCQuality's generic development skill. + +## Execute + +1. Resolve `PLUGIN_ROOT` to the directory containing this plugin's root `plugin.json`. This file is `PLUGIN_ROOT/skills/al-development/SKILL.md`; when the host does not expose the plugin root, resolve it two levels above this file. +2. Build the `task-context` required by `PLUGIN_ROOT/skills/entry.md`: + - Copy the caller's request verbatim into `goal`. + - Set `inputs-available: [development-request, repository]`. + - Set `technologies: [al]` when the repository is an AL project. + - Pass `bc-version`, `countries`, and `application-area` only when supplied or reliably determined. + - Apply `BCQUALITY_ENABLED_LAYERS` and `BCQUALITY_DISABLED_SKILLS` exactly as the `al-code-review` adapter does. +3. Read and execute `PLUGIN_ROOT/skills/entry.md`, including Preparation. Resolve every path it names against `PLUGIN_ROOT`, not the user's repository. If knowledge-index generation is unavailable, use READ's path-based fallback. +4. Follow Entry's dispatch exactly. The normal result is `microsoft/skills/development/al-development.md`; do not select it directly or duplicate its behavior in this adapter. +5. Normalize the caller's input as `development-request`: + - Plain text becomes `{ kind: auto, description: }`. + - Preserve an explicit `kind`, supplied plan, and `acceptance-criteria`. + - A plan-only input becomes `{ kind: auto, description: "Implement the supplied development plan.", plan: }`. + Pass the writable current workspace as `repository`, execute the dispatched skill, and return its `implementation-report` unchanged. + +The adapter never edits BCQuality itself unless BCQuality is the caller's target repository. The target of implementation is the repository supplied by the caller. diff --git a/skills/do.md b/skills/do.md index a79c5fc2..154e14ad 100644 --- a/skills/do.md +++ b/skills/do.md @@ -19,7 +19,7 @@ An action skill is a single markdown file with YAML frontmatter. It lives inside - `/community/skills/` — community-contributed action skills. - `/custom/skills/` — partner or customer action skills (typically in a consumer repo, not in BCQuality itself). -Action skills do not live at the repo root. The files in `/skills/` — the three meta-skill contracts (READ, DO, WRITE) and the entry-point skill (`entry.md`, `kind: entry-point`) — are the only skills that sit outside a layer. The entry-point skill structurally follows this same four-step pattern but produces a dispatch record rather than a findings-report; see `skills/entry.md` for its contract. +Action skills do not live at the repo root. The files in `/skills/` — the three meta-skill contracts (READ, DO, WRITE) and the entry-point skill (`entry.md`, `kind: entry-point`) — are the only skills that sit outside a layer. The entry-point skill structurally follows this same four-step pattern but produces a dispatch record rather than an action-skill report; see `skills/entry.md` for its contract. ## Skills hold mechanics; knowledge files hold BC facts @@ -56,10 +56,20 @@ application-area: [all] `bc-version`, `technologies`, `countries`, `application-area` are optional filters that let an orchestrator pre-select applicable skills for a task. They follow the same semantics as in READ. -`inputs` is a list of abstract input types the skill **accepts**. Standard values: `pr-diff`, `object-list`, `file-path`, `repository`, `telemetry-query`. Semantics are any-of: the orchestrator supplies whichever listed input types it has, and the skill is invoked with a non-empty subset of its declared `inputs`. A skill that cannot proceed with the supplied subset MUST return `outcome: "not-applicable"`. `outputs` is always a single-element list naming the output kind; today only `findings-report` is defined. +`inputs` is a list of abstract input types the skill **accepts**. Standard values: `pr-diff`, `object-list`, `file-path`, `repository`, `telemetry-query`, `development-request`, `development-plan`. Semantics are any-of: the orchestrator supplies whichever listed input types it has, and the skill is invoked with a non-empty subset of its declared `inputs`. A skill that cannot proceed with the supplied subset MUST return `outcome: "not-applicable"`. + +`outputs` is always a single-element list naming the output kind: + +- `findings-report` — evaluates an input and reports defects or observations. +- `implementation-report` — changes a repository to satisfy a development request and reports the plan, knowledge used, changed files, validation, and post-implementation review. +- `development-guidance-report` — selects and summarizes applicable BCQuality knowledge for an existing development plan without changing the target repository. `sub-skills` is an optional field. When present and non-empty, the skill is a **super-skill** that composes other action skills; see *Composition* below. Values are repo-relative paths to action-skill files. +`quality-skill` is optional on an action skill that emits an `implementation-report`. It names one repo-relative review action skill to run over the completed diff. It is a post-implementation gate, not a composed sub-skill: Entry does not route through it, and its complete findings-report is returned in `review`. Consumer configuration still applies; if the named quality skill is disabled or unavailable, record its validation as `not-run` and do not claim `completed`. + +`guidance-skill` is optional on an action skill that emits an `implementation-report`. It names one repo-relative read-only action skill that accepts a `development-plan` and emits a `development-guidance-report`. The implementation skill invokes it after forming its plan and before editing product code. Consumer configuration still applies; when guidance is disabled or unavailable, the implementation skill must not claim knowledge-backed development. + ## Required sections Every action skill MUST contain these five sections, in order: @@ -80,7 +90,7 @@ Every action skill MUST contain these five sections, in order: **Action.** Execute the skill's work against the worklist. Evaluate each item in the worklist against the task input and emit findings. The action step is where skill behavior differs; the preceding three steps are uniform. -## Output contract +## Findings-report contract Every action skill emits a single JSON document that conforms to this schema: @@ -242,6 +252,136 @@ Severity taxonomy: - `minor` — quality concern; worth flagging but not a gate. - `info` — observation or context; not actionable on its own. +## Development-guidance-report contract + +An action skill with `outputs: [development-guidance-report]` emits one JSON document: + +```json +{ + "skill": { "id": "string", "version": 1 }, + "outcome": "completed | not-applicable | no-knowledge | partial | failed", + "outcome-reason": "string", + "summary": { + "request": "string", + "kind": "feature | bug | refactor | upgrade | maintenance", + "candidates": 0, + "selected": 0 + }, + "context": { + "bc-version": "string", + "technologies": ["string"], + "countries": ["string"], + "application-area": ["string"], + "unknown": ["bc-version | technologies | countries | application-area"] + }, + "knowledge": [ + { + "path": "string", + "sha": "string", + "used-for": "string", + "constraints": ["string"], + "sample-paths": ["string"] + } + ], + "validation-considerations": [ + { + "id": "string", + "reason": "string", + "evidence": "string" + } + ], + "suppressed": [ + { + "reference": { "path": "string", "sha": "string" }, + "reason": "layer-precedence | configuration" + } + ], + "unresolved": ["string"] +} +``` + +The skill is read-only with respect to the target repository. `completed` means every selected article was opened and converted into faithful implementation constraints. `no-knowledge` means no applicable article survived filtering; `knowledge` is empty. `partial` means candidate evaluation stopped early, with the gap named in `outcome-reason` and `unresolved`. + +`knowledge[].constraints` summarizes only normative `## Best Practice` and `## Anti Pattern` content from the referenced article. It must not introduce a Business Central fact absent from that article. `sample-paths` contains only sibling samples that exist and were opened. Every path is subject to the reference-integrity gate. + +`validation-considerations` states evidence the implementation workflow should obtain; it does not claim that a command or test has run. `unresolved` records missing repository context or plan decisions that prevent a reliable constraint. Unknown applicability dimensions must appear in both `context.unknown` and a relevant unresolved entry. + +## Implementation-report contract + +An action skill with `outputs: [implementation-report]` emits one JSON document: + +```json +{ + "skill": { "id": "string", "version": 1 }, + "outcome": "completed | not-applicable | no-knowledge | partial | failed", + "outcome-reason": "string", + "summary": { + "request": "string", + "files-created": 0, + "files-modified": 0, + "files-deleted": 0 + }, + "plan": { + "kind": "feature | bug | refactor | upgrade | maintenance", + "assumptions": ["string"], + "decisions": ["string"], + "objects": ["string"] + }, + "knowledge": [ + { "path": "string", "sha": "string", "used-for": "string" } + ], + "changes": [ + { + "path": "string", + "action": "created | modified | deleted", + "purpose": "string" + } + ], + "validation": [ + { + "id": "string", + "command": "string", + "status": "passed | failed | not-run", + "details": "string" + } + ], + "review": { "...full findings-report from the post-implementation review..." : null }, + "suppressed": [ + { + "reference": { "path": "string", "sha": "string" }, + "reason": "layer-precedence | configuration" + } + ], + "remaining": ["string"] +} +``` + +### Implementation outcome semantics + +- `completed` — the requested change is persisted in the repository, required validation passed, and the post-implementation review has no unresolved `blocker` or `major` finding. +- `not-applicable` — the request is not an implementation task accepted by the skill, or the supplied repository does not contain the required technology. +- `no-knowledge` — no applicable BCQuality knowledge survived filtering and the skill cannot safely implement the Business Central-specific request. No request changes are made. +- `partial` — useful changes were persisted, but part of the requested scope, validation, or post-implementation review could not be completed. `outcome-reason` and `remaining` identify the unfinished work. +- `failed` — the skill could not produce a reliable implementation. `outcome-reason` is required. Any working-tree changes remain visible and MUST still be listed in `changes`. + +### Implementation field semantics + +**`summary.request`** is a concise statement of the implemented change. File counts describe only changes made by this skill; pre-existing user changes are excluded. + +**`plan`** records the implementation decisions needed to understand the result. `kind` is the classified development mode: `feature`, `bug`, `refactor`, `upgrade`, or `maintenance`. `assumptions` contains only assumptions actually made; `decisions` captures consequential design choices; `objects` names the Business Central objects or other artifacts created or changed. + +**`knowledge`** lists every knowledge file whose normative guidance materially shaped the implementation. `path` and optional `sha` follow the same reference format as a findings-report. `used-for` briefly names the design or implementation decision. The reference-integrity gate applies: every path must exist in the live checkout, be copied verbatim from discovery, and have been opened in full. Applicability alone is not enough to list an article. + +**`changes`** is an exhaustive list of files created, modified, or deleted by the skill. Paths are repository-relative and use forward slashes. Do not include unrelated pre-existing changes. + +**`validation`** records commands actually run. `passed` and `failed` require a real command result; unavailable tooling or an intentionally skipped check is `not-run` with `details`. A skill MUST NOT manufacture a successful check or replace a failed command with a success-shaped fallback. + +**`review`** is optional for generic implementation skills and required when a skill's instructions mandate post-implementation review. When present, it is the complete findings-report returned by that review skill, not a rewritten summary. + +**`suppressed`** has the same semantics as in a findings-report and records applicable knowledge excluded by layer precedence or configuration. + +**`remaining`** contains concrete unfinished work only. It is empty for `completed`. + ## Composition (super-skills) A **super-skill** is an action skill whose frontmatter declares a non-empty `sub-skills: [...]`. A super-skill does not evaluate knowledge files directly; it invokes other action skills and composes their output. @@ -316,4 +456,4 @@ Conforms to the DO output contract. ## How orchestrators consume output -An orchestrator invokes an action skill with an input appropriate to the skill's declared `inputs`, receives the JSON output, and maps findings to its delivery surface (PR comments, build gates, IDE diagnostics). The orchestrator MUST NOT interpret skill-specific fields beyond the schema above. Skills that need richer semantics MUST encode them within the schema (for example, by adding structured `message` text) rather than extending the output shape. +An orchestrator invokes an action skill with an input appropriate to the skill's declared `inputs` and uses the single output kind declared in frontmatter. It maps a `findings-report` to PR comments, build gates, or IDE diagnostics; a `development-guidance-report` to constraints for a downstream implementation workflow; and an `implementation-report` to a coding-session summary, changed-file view, validation status, and any remaining work. The orchestrator MUST NOT interpret fields beyond the three schemas above. diff --git a/skills/entry.md b/skills/entry.md index 0196c355..4a1b613f 100644 --- a/skills/entry.md +++ b/skills/entry.md @@ -21,6 +21,9 @@ The agent invokes Entry with a **task context** supplied by the orchestrator: task-context: goal: string # free-text description of what needs doing inputs-available: # values the orchestrator has ready to pass to a chosen skill + - development-request + - development-plan + - repository - pr-diff - file-path technologies: [al] @@ -174,7 +177,7 @@ Populated example (PR review on a repo where only `al-performance-review` is ena 1. Invoke Entry with the orchestrator-supplied task context. 2. Receive the dispatch record. -3. For each entry in `dispatch[]`, read the referenced action skill, execute its Source → Relevance → Worklist → Action steps per DO, and produce a findings-report. -4. Return the findings-reports to the orchestrator. When `outcome` is `no-match` or `failed`, return the dispatch record itself so the orchestrator can log the reason. +3. For each entry in `dispatch[]`, read the referenced action skill, execute its Source → Relevance → Worklist → Action steps per DO, and produce the report kind declared by that skill's single `outputs` value. +4. Return the action-skill reports to the orchestrator. When Entry's `outcome` is `no-match` or `failed`, return the dispatch record itself so the orchestrator can log the reason. READ and DO are the contracts that govern what the dispatched skills do. An agent that has not yet read READ and DO reads them when it executes the first dispatched skill — they are not prerequisites for invoking Entry. diff --git a/tools/Test-DevelopmentFixtures.ps1 b/tools/Test-DevelopmentFixtures.ps1 new file mode 100644 index 00000000..ecd52591 --- /dev/null +++ b/tools/Test-DevelopmentFixtures.ps1 @@ -0,0 +1,516 @@ +<# +.SYNOPSIS + Validates and prepares BCQuality AL development fixtures. + +.DESCRIPTION + Static validation checks fixture IDs, capability links, knowledge references, + and the development skill. -PrepareDirectory emits opaque requests for model + runs. -ResultsDirectory scores implementation reports produced by an + external runner that performed the declared compile, test, and review checks. +#> +[CmdletBinding()] +param( + [string] $Root = (Resolve-Path (Join-Path $PSScriptRoot '..')), + [string] $ManifestPath, + [string] $CapabilitiesPath, + [string] $PrepareDirectory, + [string] $ResultsDirectory +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$Root = (Resolve-Path -LiteralPath $Root).Path +if (-not $ManifestPath) { + $ManifestPath = Join-Path $Root 'evaluation/development-fixtures.json' +} +if (-not $CapabilitiesPath) { + $CapabilitiesPath = Join-Path $Root 'coverage/development-capabilities.json' +} + +$manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json +$capabilityManifest = Get-Content -LiteralPath $CapabilitiesPath -Raw | ConvertFrom-Json +$problems = [System.Collections.Generic.List[string]]::new() + +function Get-ModelCaseId { + param([string] $ManifestId) + + $sha = [System.Security.Cryptography.SHA256]::Create() + try { + $bytes = [System.Text.Encoding]::UTF8.GetBytes($ManifestId) + $hash = $sha.ComputeHash($bytes) + $token = ([System.BitConverter]::ToString($hash) -replace '-', '').Substring(0, 8).ToLowerInvariant() + return "case-$token" + } finally { + $sha.Dispose() + } +} + +if ($manifest.version -ne 1) { + $problems.Add("Unsupported development fixture version: $($manifest.version)") | Out-Null +} +if ($capabilityManifest.version -ne 1) { + $problems.Add("Unsupported capability manifest version: $($capabilityManifest.version)") | Out-Null +} +foreach ($thresholdName in @('minimumKnowledgeRecall', 'minimumKnowledgePrecision')) { + $threshold = [double]$manifest.$thresholdName + if ($threshold -lt 0 -or $threshold -gt 1) { + $problems.Add("$thresholdName must be between 0 and 1.") | Out-Null + } +} + +$skillPath = [string]$manifest.skill +if (-not (Test-Path -LiteralPath (Join-Path $Root $skillPath) -PathType Leaf)) { + $problems.Add("Development skill does not exist: $skillPath") | Out-Null +} + +$validChecks = @('compile', 'tests', 'review') +$validInputKinds = @('auto', 'feature', 'bug', 'refactor', 'upgrade', 'maintenance') +$validOutputKinds = @('feature', 'bug', 'refactor', 'upgrade', 'maintenance') +$caseById = @{} +foreach ($case in @($manifest.cases)) { + $id = [string]$case.id + if ($id -notmatch '^[a-z0-9]+(?:-[a-z0-9]+)*$') { + $problems.Add("Fixture id must be kebab-case: '$id'.") | Out-Null + } elseif ($caseById.ContainsKey($id)) { + $problems.Add("Duplicate fixture id: $id") | Out-Null + } else { + $caseById[$id] = $case + } + + if ($case.PSObject.Properties.Name -notcontains 'development-request') { + $problems.Add("${id}: development-request is required.") | Out-Null + continue + } + $request = $case.'development-request' + if ($validInputKinds -notcontains [string]$request.kind) { + $problems.Add("${id}: development-request.kind must be one of $($validInputKinds -join ', ').") | Out-Null + } + $description = if ($request.PSObject.Properties.Name -contains 'description') { + [string]$request.description + } else { + '' + } + $planText = if ($request.PSObject.Properties.Name -contains 'plan') { + [string]$request.plan + } else { + '' + } + if ([string]::IsNullOrWhiteSpace($description) -and [string]::IsNullOrWhiteSpace($planText)) { + $problems.Add("${id}: development-request requires description or plan.") | Out-Null + } + if ($request.PSObject.Properties.Name -notcontains 'acceptance-criteria' -or + -not @($request.'acceptance-criteria').Count) { + $problems.Add("${id}: development-request.acceptance-criteria must not be empty.") | Out-Null + } + $expectedKind = if ($case.PSObject.Properties.Name -contains 'expectedKind') { + [string]$case.expectedKind + } else { + '' + } + if ($validOutputKinds -notcontains $expectedKind) { + $problems.Add("${id}: expectedKind must be one of $($validOutputKinds -join ', ').") | Out-Null + } + if ([string]$request.kind -ne 'auto' -and [string]$request.kind -ne $expectedKind) { + $problems.Add("${id}: explicit request kind '$($request.kind)' must equal expectedKind '$expectedKind'.") | Out-Null + } + + $expectedKnowledge = @($case.requiredKnowledge) + @($case.optionalKnowledge) + if (@($expectedKnowledge | Sort-Object -Unique).Count -ne $expectedKnowledge.Count) { + $problems.Add("${id}: requiredKnowledge and optionalKnowledge contain duplicates.") | Out-Null + } + foreach ($reference in $expectedKnowledge) { + $reference = [string]$reference + if ($reference.Contains('\') -or -not $reference.EndsWith('.md')) { + $problems.Add("${id}: invalid knowledge path: $reference") | Out-Null + } elseif (-not (Test-Path -LiteralPath (Join-Path $Root $reference) -PathType Leaf)) { + $problems.Add("${id}: knowledge article does not exist: $reference") | Out-Null + } + } + foreach ($check in @($case.requiredChecks)) { + if ($validChecks -notcontains [string]$check) { + $problems.Add("${id}: unsupported required check '$check'.") | Out-Null + } + } +} + +$validCapabilityStatuses = @('planned', 'fixture', 'validated') +$capabilityIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) +foreach ($capability in @($capabilityManifest.capabilities)) { + $id = [string]$capability.id + if (-not $capabilityIds.Add($id)) { + $problems.Add("Duplicate capability id: $id") | Out-Null + } + if ($validCapabilityStatuses -notcontains [string]$capability.status) { + $problems.Add("${id}: invalid capability status '$($capability.status)'.") | Out-Null + } + $fixtureIds = @($capability.fixtureIds) + if ($capability.status -in @('fixture', 'validated') -and -not $fixtureIds.Count) { + $problems.Add("${id}: status '$($capability.status)' requires at least one fixture.") | Out-Null + } + foreach ($fixtureId in $fixtureIds) { + if (-not $caseById.ContainsKey([string]$fixtureId)) { + $problems.Add("${id}: unknown fixture id '$fixtureId'.") | Out-Null + } elseif (@($caseById[[string]$fixtureId].capabilities) -notcontains $id) { + $problems.Add("${id}: fixture '$fixtureId' does not link back to the capability.") | Out-Null + } + } +} + +foreach ($case in @($manifest.cases)) { + foreach ($capabilityId in @($case.capabilities)) { + if (-not $capabilityIds.Contains([string]$capabilityId)) { + $problems.Add("$($case.id): unknown capability '$capabilityId'.") | Out-Null + continue + } + $capability = @( + $capabilityManifest.capabilities | + Where-Object id -eq ([string]$capabilityId) + )[0] + if (@($capability.fixtureIds) -notcontains [string]$case.id) { + $problems.Add("$($case.id): capability '$capabilityId' does not link back to the fixture.") | Out-Null + } + } +} + +if ($problems.Count) { + Write-Host "Development fixture validation FAILED ($($problems.Count) problem(s)):" -ForegroundColor Red + $problems | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + exit 1 +} + +if ($PrepareDirectory) { + $markerPath = Join-Path $PrepareDirectory '.bcquality-development-evaluation' + if (Test-Path -LiteralPath $PrepareDirectory) { + $existing = @(Get-ChildItem -LiteralPath $PrepareDirectory -Force) + if ($existing.Count -and -not (Test-Path -LiteralPath $markerPath -PathType Leaf)) { + throw "PrepareDirectory is not empty and is not a BCQuality development evaluation directory: $PrepareDirectory" + } + if (Test-Path -LiteralPath $markerPath -PathType Leaf) { + Get-ChildItem -LiteralPath $PrepareDirectory -File | + Where-Object { + $_.Name -eq 'knowledge-index.json' -or + $_.Name -like 'request-*.json' -or + $_.Name -like 'result-*.json' + } | + Remove-Item -Force + } + } else { + New-Item -ItemType Directory -Force -Path $PrepareDirectory | Out-Null + } + Set-Content -LiteralPath $markerPath -Value 'BCQuality generated development evaluation directory' -Encoding UTF8 + + $indexPath = Join-Path $PrepareDirectory 'knowledge-index.json' + & (Join-Path $Root 'tools/Build-KnowledgeIndex.ps1') -BCQualityRoot $Root -IndexPath $indexPath | Out-Null + $skillInstructions = Get-Content -LiteralPath (Join-Path $Root $skillPath) -Raw + + foreach ($case in @($manifest.cases)) { + $modelId = Get-ModelCaseId ([string]$case.id) + $request = [ordered]@{ + protocol = 'Run the supplied AL development skill in a fresh writable fixture repository. Persist the implementation, run real checks, and return only implementationReport using the supplied schema.' + caseId = $modelId + skill = $skillPath + skillInstructions = $skillInstructions + knowledgeIndex = 'knowledge-index.json' + 'task-context' = [ordered]@{ + goal = if ($case.'development-request'.PSObject.Properties.Name -contains 'description') { + [string]$case.'development-request'.description + } else { + [string]$case.'development-request'.plan + } + 'inputs-available' = @('development-request', 'repository') + technologies = @($case.context.technologies) + countries = @($case.context.countries) + 'application-area' = @($case.context.'application-area') + } + 'development-request' = [ordered]@{ + kind = [string]$case.'development-request'.kind + description = if ($case.'development-request'.PSObject.Properties.Name -contains 'description') { + [string]$case.'development-request'.description + } else { + $null + } + plan = if ($case.'development-request'.PSObject.Properties.Name -contains 'plan') { + [string]$case.'development-request'.plan + } else { + $null + } + 'acceptance-criteria' = @($case.'development-request'.'acceptance-criteria') + } + resultSchema = [ordered]@{ + caseId = $modelId + workspaceRoot = 'absolute path to the retained fixture repository' + implementationReport = [ordered]@{ + skill = [ordered]@{ id = 'al-development'; version = 1 } + outcome = 'completed | not-applicable | no-knowledge | partial | failed' + 'outcome-reason' = 'required for partial or failed' + summary = [ordered]@{ + request = 'implemented change' + 'files-created' = 0 + 'files-modified' = 0 + 'files-deleted' = 0 + } + plan = [ordered]@{ + kind = 'feature | bug | refactor | upgrade | maintenance' + assumptions = @() + decisions = @() + objects = @() + } + knowledge = @([ordered]@{ path = 'repo-relative knowledge article path'; sha = 'optional commit sha'; 'used-for' = 'decision' }) + changes = @([ordered]@{ path = 'repo-relative changed file'; action = 'created | modified | deleted'; purpose = 'reason' }) + validation = @([ordered]@{ id = 'compile | tests | review'; command = 'command or quality-skill path'; status = 'passed | failed | not-run'; details = 'non-empty evidence' }) + review = [ordered]@{ + skill = [ordered]@{ id = 'al-code-review'; version = 1 } + outcome = 'completed | partial | failed' + summary = [ordered]@{ + counts = [ordered]@{ blocker = 0; major = 0; minor = 0; info = 0 } + coverage = [ordered]@{ 'worklist-size' = 0; 'items-evaluated' = 0 } + } + findings = @() + suppressed = @() + } + suppressed = @() + remaining = @() + } + } + } + $request | ConvertTo-Json -Depth 12 | + Set-Content -LiteralPath (Join-Path $PrepareDirectory "request-$modelId.json") -Encoding UTF8 + } +} + +if ($ResultsDirectory) { + $failures = [System.Collections.Generic.List[string]]::new() + foreach ($case in @($manifest.cases)) { + $modelId = Get-ModelCaseId ([string]$case.id) + $resultPath = Join-Path $ResultsDirectory "result-$modelId.json" + if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) { + $failures.Add("$($case.id): missing result file.") | Out-Null + continue + } + try { + $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json + } catch { + $failures.Add("$($case.id): result is not valid JSON: $($_.Exception.Message)") | Out-Null + continue + } + if ($result.PSObject.Properties.Name -notcontains 'caseId' -or [string]$result.caseId -ne $modelId) { + $failures.Add("$($case.id): result caseId mismatch.") | Out-Null + continue + } + if ($result.PSObject.Properties.Name -notcontains 'implementationReport') { + $failures.Add("$($case.id): implementationReport is missing.") | Out-Null + continue + } + $report = $result.implementationReport + foreach ($requiredField in @('skill', 'outcome', 'summary', 'plan', 'knowledge', 'changes', 'validation', 'review', 'suppressed', 'remaining')) { + if ($report.PSObject.Properties.Name -notcontains $requiredField) { + $failures.Add("$($case.id): implementation report is missing '$requiredField'.") | Out-Null + } + } + $reportedSkillId = if ( + $report.PSObject.Properties.Name -contains 'skill' -and + $report.skill.PSObject.Properties.Name -contains 'id' + ) { + [string]$report.skill.id + } else { + '' + } + if ($reportedSkillId -ne 'al-development') { + $failures.Add("$($case.id): implementation report skill is '$reportedSkillId'.") | Out-Null + } + $summaryRequest = if ( + $report.PSObject.Properties.Name -contains 'summary' -and + $report.summary.PSObject.Properties.Name -contains 'request' + ) { + [string]$report.summary.request + } else { + '' + } + if ([string]::IsNullOrWhiteSpace($summaryRequest)) { + $failures.Add("$($case.id): summary.request is missing or empty.") | Out-Null + } + $reportedKind = if ( + $report.PSObject.Properties.Name -contains 'plan' -and + $report.plan.PSObject.Properties.Name -contains 'kind' + ) { + [string]$report.plan.kind + } else { + '' + } + if ($reportedKind -ne [string]$case.expectedKind) { + $failures.Add("$($case.id): plan.kind '$reportedKind' does not match expected mode '$($case.expectedKind)'.") | Out-Null + } + $outcome = if ($report.PSObject.Properties.Name -contains 'outcome') { [string]$report.outcome } else { '' } + if ($outcome -ne 'completed') { + $failures.Add("$($case.id): implementation outcome is '$outcome'.") | Out-Null + } + + [object[]]$knowledgeEntries = @() + if ($report.PSObject.Properties.Name -contains 'knowledge') { + $knowledgeEntries = @($report.knowledge) + } + if (-not $knowledgeEntries.Count) { + $failures.Add("$($case.id): implementation report contains no knowledge entries.") | Out-Null + } + $usedKnowledge = @( + $knowledgeEntries | + Where-Object { $_.PSObject.Properties.Name -contains 'path' } | + ForEach-Object { [string]$_.path } + ) + if (@($usedKnowledge | Sort-Object -Unique).Count -ne $usedKnowledge.Count) { + $failures.Add("$($case.id): knowledge contains duplicate paths.") | Out-Null + } + foreach ($entry in $knowledgeEntries) { + $path = if ($entry.PSObject.Properties.Name -contains 'path') { [string]$entry.path } else { '' } + $usedFor = if ($entry.PSObject.Properties.Name -contains 'used-for') { [string]$entry.'used-for' } else { '' } + if ([string]::IsNullOrWhiteSpace($path) -or + [System.IO.Path]::IsPathRooted($path) -or + @($path -split '/|\\') -contains '..' -or + -not (Test-Path -LiteralPath (Join-Path $Root $path) -PathType Leaf)) { + $failures.Add("$($case.id): knowledge entry has a missing or invalid path '$path'.") | Out-Null + } + if ([string]::IsNullOrWhiteSpace($usedFor)) { + $failures.Add("$($case.id): knowledge entry '$path' has no used-for explanation.") | Out-Null + } + } + $requiredKnowledge = @($case.requiredKnowledge | ForEach-Object { [string]$_ }) + $matched = @($requiredKnowledge | Where-Object { $usedKnowledge -contains $_ }).Count + $recall = if ($requiredKnowledge.Count) { $matched / $requiredKnowledge.Count } else { 1.0 } + if ($recall -lt [double]$manifest.minimumKnowledgeRecall) { + $failures.Add("$($case.id): knowledge recall $recall is below $($manifest.minimumKnowledgeRecall).") | Out-Null + } + $acceptedKnowledge = @( + @($case.requiredKnowledge) + @($case.optionalKnowledge) | + ForEach-Object { [string]$_ } | + Sort-Object -Unique + ) + $acceptedUsed = @($usedKnowledge | Where-Object { $acceptedKnowledge -contains $_ }).Count + $precision = if ($usedKnowledge.Count) { $acceptedUsed / $usedKnowledge.Count } else { 0.0 } + if ($precision -lt [double]$manifest.minimumKnowledgePrecision) { + $failures.Add("$($case.id): knowledge precision $precision is below $($manifest.minimumKnowledgePrecision).") | Out-Null + } + + [object[]]$validationEntries = @() + if ($report.PSObject.Properties.Name -contains 'validation') { + $validationEntries = @($report.validation) + } + foreach ($requiredCheck in @($case.requiredChecks)) { + $check = @($validationEntries | Where-Object { + $_.PSObject.Properties.Name -contains 'id' -and [string]$_.id -eq $requiredCheck + }) + $checkStatus = if ($check.Count -eq 1 -and $check[0].PSObject.Properties.Name -contains 'status') { + [string]$check[0].status + } else { + '' + } + if ($check.Count -ne 1 -or $checkStatus -ne 'passed') { + $failures.Add("$($case.id): required check '$requiredCheck' did not pass exactly once.") | Out-Null + continue + } + $command = if ($check[0].PSObject.Properties.Name -contains 'command') { [string]$check[0].command } else { '' } + $details = if ($check[0].PSObject.Properties.Name -contains 'details') { [string]$check[0].details } else { '' } + if ([string]::IsNullOrWhiteSpace($command) -or [string]::IsNullOrWhiteSpace($details)) { + $failures.Add("$($case.id): required check '$requiredCheck' lacks command or evidence details.") | Out-Null + } + } + + [object[]]$changes = @() + if ($report.PSObject.Properties.Name -contains 'changes') { + $changes = @($report.changes) + } + if (-not $changes.Count) { + $failures.Add("$($case.id): implementation report contains no changed files.") | Out-Null + } + + $workspaceRoot = if ($result.PSObject.Properties.Name -contains 'workspaceRoot') { [string]$result.workspaceRoot } else { '' } + if ([string]::IsNullOrWhiteSpace($workspaceRoot) -or + -not (Test-Path -LiteralPath $workspaceRoot -PathType Container)) { + $failures.Add("$($case.id): workspaceRoot is missing or unavailable.") | Out-Null + } else { + & git -C $workspaceRoot rev-parse --is-inside-work-tree 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + $failures.Add("$($case.id): workspaceRoot is not a readable git worktree.") | Out-Null + continue + } + $actualChanges = @( + @(& git -C $workspaceRoot diff --name-only HEAD) + + @(& git -C $workspaceRoot ls-files --others --exclude-standard) | + Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | + ForEach-Object { ([string]$_).Replace('\', '/') } | + Sort-Object -Unique + ) + if ($LASTEXITCODE -ne 0) { + $failures.Add("$($case.id): unable to read the workspace diff.") | Out-Null + } else { + $reportedChanges = @( + $changes | + Where-Object { $_.PSObject.Properties.Name -contains 'path' } | + ForEach-Object { + $path = ([string]$_.path).Replace('\', '/') + if ([System.IO.Path]::IsPathRooted($path) -or @($path -split '/') -contains '..') { + $failures.Add("$($case.id): changed path is not repository-relative: $path") | Out-Null + } + $path + } | + Sort-Object -Unique + ) + foreach ($path in @($reportedChanges | Where-Object { $actualChanges -notcontains $_ })) { + $failures.Add("$($case.id): reported change is absent from the worktree diff: $path") | Out-Null + } + foreach ($path in @($actualChanges | Where-Object { $reportedChanges -notcontains $_ })) { + $failures.Add("$($case.id): worktree change is absent from the implementation report: $path") | Out-Null + } + } + } + + if ($report.PSObject.Properties.Name -notcontains 'review') { + $failures.Add("$($case.id): complete post-implementation review is missing.") | Out-Null + } else { + $review = $report.review + foreach ($requiredField in @('skill', 'outcome', 'summary', 'findings', 'suppressed')) { + if ($review.PSObject.Properties.Name -notcontains $requiredField) { + $failures.Add("$($case.id): review is missing '$requiredField'.") | Out-Null + } + } + [object[]]$reviewFindings = @() + if ($review.PSObject.Properties.Name -contains 'findings') { + $reviewFindings = @($review.findings) + } + $reviewOutcome = if ($review.PSObject.Properties.Name -contains 'outcome') { [string]$review.outcome } else { '' } + if ($reviewOutcome -ne 'completed') { + $failures.Add("$($case.id): post-implementation review outcome is '$reviewOutcome'.") | Out-Null + } + $gatingFindings = @( + $reviewFindings | + Where-Object { + $_.PSObject.Properties.Name -contains 'severity' -and + [string]$_.severity -in @('blocker', 'major') + } + ) + if ($gatingFindings.Count) { + $failures.Add("$($case.id): post-implementation review has $($gatingFindings.Count) gating finding(s).") | Out-Null + } + } + [object[]]$remaining = @() + if ($report.PSObject.Properties.Name -contains 'remaining') { + $remaining = @($report.remaining) + } + if ($remaining.Count) { + $failures.Add("$($case.id): completed report still lists remaining work.") | Out-Null + } + } + + if ($failures.Count) { + Write-Host "Development fixture scoring FAILED ($($failures.Count) problem(s)):" -ForegroundColor Red + $failures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + exit 1 + } + Write-Host "Development fixture scoring PASSED: $(@($manifest.cases).Count) case(s)." +} else { + $fixtureBackedCapabilities = @( + $capabilityManifest.capabilities | + Where-Object status -in @('fixture', 'validated') + ).Count + Write-Host "Development fixture validation PASSED: $(@($manifest.cases).Count) cases; $fixtureBackedCapabilities of $($capabilityIds.Count) capabilities have fixtures." +} diff --git a/tools/Test-DevelopmentGuidanceFixtures.ps1 b/tools/Test-DevelopmentGuidanceFixtures.ps1 new file mode 100644 index 00000000..d8709ffb --- /dev/null +++ b/tools/Test-DevelopmentGuidanceFixtures.ps1 @@ -0,0 +1,298 @@ +<# +.SYNOPSIS + Validates, prepares, and scores read-only AL development-guidance fixtures. +#> +[CmdletBinding()] +param( + [string] $Root = (Resolve-Path (Join-Path $PSScriptRoot '..')), + [string] $ManifestPath, + [string] $PrepareDirectory, + [string] $ResultsDirectory +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$Root = (Resolve-Path -LiteralPath $Root).Path +if (-not $ManifestPath) { + $ManifestPath = Join-Path $Root 'evaluation/development-guidance-fixtures.json' +} + +$manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json +$problems = [System.Collections.Generic.List[string]]::new() +$validKinds = @('feature', 'bug', 'refactor', 'upgrade', 'maintenance') + +function Get-ModelCaseId { + param([string] $ManifestId) + + $sha = [System.Security.Cryptography.SHA256]::Create() + try { + $bytes = [System.Text.Encoding]::UTF8.GetBytes($ManifestId) + $hash = $sha.ComputeHash($bytes) + $token = ([System.BitConverter]::ToString($hash) -replace '-', '').Substring(0, 8).ToLowerInvariant() + return "case-$token" + } finally { + $sha.Dispose() + } +} + +if ($manifest.version -ne 1) { + $problems.Add("Unsupported guidance fixture version: $($manifest.version)") | Out-Null +} +foreach ($thresholdName in @('minimumKnowledgeRecall', 'minimumKnowledgePrecision')) { + $threshold = [double]$manifest.$thresholdName + if ($threshold -lt 0 -or $threshold -gt 1) { + $problems.Add("$thresholdName must be between 0 and 1.") | Out-Null + } +} + +$skillPath = [string]$manifest.skill +if (-not (Test-Path -LiteralPath (Join-Path $Root $skillPath) -PathType Leaf)) { + $problems.Add("Guidance skill does not exist: $skillPath") | Out-Null +} + +$seenIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) +foreach ($case in @($manifest.cases)) { + $id = [string]$case.id + if ($id -notmatch '^[a-z0-9]+(?:-[a-z0-9]+)*$') { + $problems.Add("Fixture id must be kebab-case: '$id'.") | Out-Null + } elseif (-not $seenIds.Add($id)) { + $problems.Add("Duplicate fixture id: $id") | Out-Null + } + if ($case.PSObject.Properties.Name -notcontains 'development-plan') { + $problems.Add("${id}: development-plan is required.") | Out-Null + continue + } + $plan = $case.'development-plan' + if ($validKinds -notcontains [string]$plan.kind) { + $problems.Add("${id}: development-plan.kind is invalid.") | Out-Null + } + if ([string]::IsNullOrWhiteSpace([string]$plan.request)) { + $problems.Add("${id}: development-plan.request is required.") | Out-Null + } + + $expectedKnowledge = @($case.requiredKnowledge) + @($case.optionalKnowledge) + if (@($expectedKnowledge | Sort-Object -Unique).Count -ne $expectedKnowledge.Count) { + $problems.Add("${id}: requiredKnowledge and optionalKnowledge contain duplicates.") | Out-Null + } + foreach ($reference in $expectedKnowledge) { + $reference = [string]$reference + if ($reference.Contains('\') -or -not $reference.EndsWith('.md')) { + $problems.Add("${id}: invalid knowledge path: $reference") | Out-Null + } elseif (-not (Test-Path -LiteralPath (Join-Path $Root $reference) -PathType Leaf)) { + $problems.Add("${id}: knowledge article does not exist: $reference") | Out-Null + } + } +} + +if ($problems.Count) { + Write-Host "Development guidance fixture validation FAILED ($($problems.Count) problem(s)):" -ForegroundColor Red + $problems | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + exit 1 +} + +if ($PrepareDirectory) { + $markerPath = Join-Path $PrepareDirectory '.bcquality-development-guidance-evaluation' + if (Test-Path -LiteralPath $PrepareDirectory) { + $existing = @(Get-ChildItem -LiteralPath $PrepareDirectory -Force) + if ($existing.Count -and -not (Test-Path -LiteralPath $markerPath -PathType Leaf)) { + throw "PrepareDirectory is not empty and is not a BCQuality development-guidance evaluation directory: $PrepareDirectory" + } + if (Test-Path -LiteralPath $markerPath -PathType Leaf) { + Get-ChildItem -LiteralPath $PrepareDirectory -File | + Where-Object { + $_.Name -eq 'knowledge-index.json' -or + $_.Name -like 'request-*.json' -or + $_.Name -like 'result-*.json' + } | + Remove-Item -Force + } + } else { + New-Item -ItemType Directory -Force -Path $PrepareDirectory | Out-Null + } + Set-Content -LiteralPath $markerPath -Value 'BCQuality generated development-guidance evaluation directory' -Encoding UTF8 + + $indexPath = Join-Path $PrepareDirectory 'knowledge-index.json' + & (Join-Path $Root 'tools/Build-KnowledgeIndex.ps1') -BCQualityRoot $Root -IndexPath $indexPath | Out-Null + $skillInstructions = Get-Content -LiteralPath (Join-Path $Root $skillPath) -Raw + + foreach ($case in @($manifest.cases)) { + $modelId = Get-ModelCaseId ([string]$case.id) + [ordered]@{ + protocol = 'Run the supplied AL development-plan skill read-only in a clean fixture repository. Return only guidanceReport using the supplied schema and retain the repository for read-only verification.' + caseId = $modelId + skill = $skillPath + skillInstructions = $skillInstructions + knowledgeIndex = 'knowledge-index.json' + 'task-context' = [ordered]@{ + goal = [string]$case.'development-plan'.request + 'inputs-available' = @('development-plan', 'repository') + technologies = @($case.context.technologies) + countries = @($case.context.countries) + 'application-area' = @($case.context.'application-area') + } + 'development-plan' = $case.'development-plan' + resultSchema = [ordered]@{ + caseId = $modelId + workspaceRoot = 'absolute path to the retained clean fixture repository' + guidanceReport = [ordered]@{ + skill = [ordered]@{ id = 'al-development-plan'; version = 1 } + outcome = 'completed | not-applicable | no-knowledge | partial | failed' + 'outcome-reason' = 'required for partial or failed' + summary = [ordered]@{ + request = 'planned change' + kind = 'feature | bug | refactor | upgrade | maintenance' + candidates = 0 + selected = 0 + } + context = [ordered]@{ + 'bc-version' = 'resolved target or unknown' + technologies = @('al') + countries = @('w1') + 'application-area' = @('all') + unknown = @() + } + knowledge = @([ordered]@{ + path = 'repo-relative article path' + sha = 'optional commit sha' + 'used-for' = 'plan decision' + constraints = @('faithful normative constraint') + 'sample-paths' = @() + }) + 'validation-considerations' = @([ordered]@{ + id = 'stable id' + reason = 'why evidence is needed' + evidence = 'evidence implementation should obtain' + }) + suppressed = @() + unresolved = @() + } + } + } | ConvertTo-Json -Depth 15 | + Set-Content -LiteralPath (Join-Path $PrepareDirectory "request-$modelId.json") -Encoding UTF8 + } +} + +if ($ResultsDirectory) { + $failures = [System.Collections.Generic.List[string]]::new() + foreach ($case in @($manifest.cases)) { + $modelId = Get-ModelCaseId ([string]$case.id) + $resultPath = Join-Path $ResultsDirectory "result-$modelId.json" + if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) { + $failures.Add("$($case.id): missing result file.") | Out-Null + continue + } + try { + $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json + } catch { + $failures.Add("$($case.id): result is not valid JSON: $($_.Exception.Message)") | Out-Null + continue + } + if ($result.PSObject.Properties.Name -notcontains 'caseId' -or [string]$result.caseId -ne $modelId) { + $failures.Add("$($case.id): result caseId mismatch.") | Out-Null + continue + } + if ($result.PSObject.Properties.Name -notcontains 'guidanceReport') { + $failures.Add("$($case.id): guidanceReport is missing.") | Out-Null + continue + } + $report = $result.guidanceReport + foreach ($requiredField in @('skill', 'outcome', 'summary', 'context', 'knowledge', 'validation-considerations', 'suppressed', 'unresolved')) { + if ($report.PSObject.Properties.Name -notcontains $requiredField) { + $failures.Add("$($case.id): guidance report is missing '$requiredField'.") | Out-Null + } + } + $skillId = if ( + $report.PSObject.Properties.Name -contains 'skill' -and + $report.skill.PSObject.Properties.Name -contains 'id' + ) { [string]$report.skill.id } else { '' } + if ($skillId -ne 'al-development-plan') { + $failures.Add("$($case.id): guidance skill is '$skillId'.") | Out-Null + } + if ($report.PSObject.Properties.Name -notcontains 'outcome' -or [string]$report.outcome -ne 'completed') { + $failures.Add("$($case.id): guidance outcome is not completed.") | Out-Null + } + $kind = if ( + $report.PSObject.Properties.Name -contains 'summary' -and + $report.summary.PSObject.Properties.Name -contains 'kind' + ) { [string]$report.summary.kind } else { '' } + if ($kind -ne [string]$case.'development-plan'.kind) { + $failures.Add("$($case.id): summary.kind '$kind' does not match the plan.") | Out-Null + } + + [object[]]$knowledgeEntries = @() + if ($report.PSObject.Properties.Name -contains 'knowledge') { + $knowledgeEntries = @($report.knowledge) + } + $usedKnowledge = @() + foreach ($entry in $knowledgeEntries) { + $path = if ($entry.PSObject.Properties.Name -contains 'path') { [string]$entry.path } else { '' } + $usedFor = if ($entry.PSObject.Properties.Name -contains 'used-for') { [string]$entry.'used-for' } else { '' } + [object[]]$constraints = @() + if ($entry.PSObject.Properties.Name -contains 'constraints') { + $constraints = @($entry.constraints) + } + if ([string]::IsNullOrWhiteSpace($path) -or + -not (Test-Path -LiteralPath (Join-Path $Root $path) -PathType Leaf)) { + $failures.Add("$($case.id): invalid knowledge path '$path'.") | Out-Null + } else { + $usedKnowledge += $path + } + if ([string]::IsNullOrWhiteSpace($usedFor) -or -not $constraints.Count) { + $failures.Add("$($case.id): '$path' lacks used-for or constraints.") | Out-Null + } + if ($entry.PSObject.Properties.Name -contains 'sample-paths') { + foreach ($samplePath in @($entry.'sample-paths')) { + $samplePath = [string]$samplePath + if ([string]::IsNullOrWhiteSpace($samplePath) -or + -not (Test-Path -LiteralPath (Join-Path $Root $samplePath) -PathType Leaf)) { + $failures.Add("$($case.id): invalid sample path '$samplePath'.") | Out-Null + } + } + } + } + + $required = @($case.requiredKnowledge | ForEach-Object { [string]$_ }) + $matched = @($required | Where-Object { $usedKnowledge -contains $_ }).Count + $recall = if ($required.Count) { $matched / $required.Count } else { 1.0 } + if ($recall -lt [double]$manifest.minimumKnowledgeRecall) { + $failures.Add("$($case.id): knowledge recall $recall is below $($manifest.minimumKnowledgeRecall).") | Out-Null + } + $accepted = @( + @($case.requiredKnowledge) + @($case.optionalKnowledge) | + ForEach-Object { [string]$_ } | + Sort-Object -Unique + ) + $acceptedUsed = @($usedKnowledge | Where-Object { $accepted -contains $_ }).Count + $precision = if ($usedKnowledge.Count) { $acceptedUsed / $usedKnowledge.Count } else { 0.0 } + if ($precision -lt [double]$manifest.minimumKnowledgePrecision) { + $failures.Add("$($case.id): knowledge precision $precision is below $($manifest.minimumKnowledgePrecision).") | Out-Null + } + + $workspaceRoot = if ($result.PSObject.Properties.Name -contains 'workspaceRoot') { + [string]$result.workspaceRoot + } else { + '' + } + if ([string]::IsNullOrWhiteSpace($workspaceRoot) -or + -not (Test-Path -LiteralPath $workspaceRoot -PathType Container)) { + $failures.Add("$($case.id): workspaceRoot is missing or unavailable.") | Out-Null + } else { + $status = @(& git -C $workspaceRoot status --porcelain) + if ($LASTEXITCODE -ne 0) { + $failures.Add("$($case.id): workspaceRoot is not a readable git worktree.") | Out-Null + } elseif ($status.Count) { + $failures.Add("$($case.id): guidance skill changed the target repository.") | Out-Null + } + } + } + + if ($failures.Count) { + Write-Host "Development guidance scoring FAILED ($($failures.Count) problem(s)):" -ForegroundColor Red + $failures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + exit 1 + } + Write-Host "Development guidance scoring PASSED: $(@($manifest.cases).Count) case(s)." +} else { + Write-Host "Development guidance fixture validation PASSED: $(@($manifest.cases).Count) case(s)." +} diff --git a/tools/Test-LearnCoverage.ps1 b/tools/Test-LearnCoverage.ps1 new file mode 100644 index 00000000..ccd8f82c --- /dev/null +++ b/tools/Test-LearnCoverage.ps1 @@ -0,0 +1,240 @@ +<# +.SYNOPSIS + Validates Microsoft Learn ingestion coverage and reports progress. +#> +[CmdletBinding()] +param( + [string] $Root = (Resolve-Path (Join-Path $PSScriptRoot '..')), + [string] $CatalogPath, + [string] $CoveragePath, + [switch] $Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$Root = (Resolve-Path -LiteralPath $Root).Path +if (-not $CatalogPath) { + $CatalogPath = Join-Path $Root 'coverage/microsoft-learn-developer-catalog.json' +} +if (-not $CoveragePath) { + $CoveragePath = Join-Path $Root 'coverage/learn-coverage.json' +} + +foreach ($path in @($CatalogPath, $CoveragePath)) { + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + throw "Coverage input not found: $path" + } +} + +$catalog = Get-Content -LiteralPath $CatalogPath -Raw | ConvertFrom-Json +$coverage = Get-Content -LiteralPath $CoveragePath -Raw | ConvertFrom-Json +$problems = [System.Collections.Generic.List[string]]::new() + +if ($catalog.version -ne 1) { + $problems.Add("Unsupported catalog version: $($catalog.version)") | Out-Null +} +if ($coverage.version -ne 1) { + $problems.Add("Unsupported coverage version: $($coverage.version)") | Out-Null +} +if ([string]$coverage.catalog -ne 'coverage/microsoft-learn-developer-catalog.json') { + $problems.Add("Coverage catalog path must be coverage/microsoft-learn-developer-catalog.json.") | Out-Null +} + +$catalogUnits = @{} +foreach ($unit in @($catalog.units)) { + $uid = [string]$unit.uid + if ($catalogUnits.ContainsKey($uid)) { + $problems.Add("Duplicate catalog unit uid: $uid") | Out-Null + } else { + $catalogUnits[$uid] = $unit + } + if ([string]::IsNullOrWhiteSpace([string]$unit.title)) { + $problems.Add("${uid}: catalog unit title is empty.") | Out-Null + } + if ([string]::IsNullOrWhiteSpace([string]$unit.url) -or + -not ([string]$unit.url).StartsWith('https://learn.microsoft.com/', [System.StringComparison]::OrdinalIgnoreCase)) { + $problems.Add("${uid}: catalog unit URL is missing or not a Microsoft Learn URL.") | Out-Null + } +} + +$catalogModules = @{} +foreach ($module in @($catalog.modules)) { + $uid = [string]$module.uid + if ($catalogModules.ContainsKey($uid)) { + $problems.Add("Duplicate catalog module uid: $uid") | Out-Null + } else { + $catalogModules[$uid] = $module + } + foreach ($unitUid in @($module.unitUids)) { + if (-not $catalogUnits.ContainsKey([string]$unitUid)) { + $problems.Add("${uid}: references unknown unit '$unitUid'.") | Out-Null + } + } +} + +$catalogPaths = @{} +foreach ($path in @($catalog.learningPaths)) { + $uid = [string]$path.uid + if ($catalogPaths.ContainsKey($uid)) { + $problems.Add("Duplicate catalog learning-path uid: $uid") | Out-Null + } else { + $catalogPaths[$uid] = $path + } + foreach ($moduleUid in @($path.moduleUids)) { + if (-not $catalogModules.ContainsKey([string]$moduleUid)) { + $problems.Add("${uid}: references unknown module '$moduleUid'.") | Out-Null + } + } +} + +if ([int]$catalog.counts.units -ne $catalogUnits.Count) { + $problems.Add("Catalog unit count does not match units array.") | Out-Null +} +if ([int]$catalog.counts.modules -ne $catalogModules.Count) { + $problems.Add("Catalog module count does not match modules array.") | Out-Null +} +if ([int]$catalog.counts.learningPaths -ne $catalogPaths.Count) { + $problems.Add("Catalog learning-path count does not match learningPaths array.") | Out-Null +} + +foreach ($unit in @($catalog.units)) { + foreach ($moduleUid in @($unit.moduleUids)) { + if (-not $catalogModules.ContainsKey([string]$moduleUid)) { + $problems.Add("$($unit.uid): references unknown module '$moduleUid'.") | Out-Null + } elseif (@($catalogModules[[string]$moduleUid].unitUids) -notcontains [string]$unit.uid) { + $problems.Add("$($unit.uid): module '$moduleUid' does not link back to the unit.") | Out-Null + } + } +} + +$validStatuses = @('in-progress', 'complete') +$validDispositions = @('candidate', 'authored', 'covered-existing', 'rejected', 'deferred') +$seenUnits = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) +$seenOutcomes = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) +$articlePaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) +$dispositionCounts = @{} +foreach ($disposition in $validDispositions) { + $dispositionCounts[$disposition] = 0 +} + +foreach ($unitCoverage in @($coverage.units)) { + $uid = [string]$unitCoverage.uid + if (-not $seenUnits.Add($uid)) { + $problems.Add("Duplicate coverage unit uid: $uid") | Out-Null + } + if (-not $catalogUnits.ContainsKey($uid)) { + $problems.Add("Coverage unit is absent from the catalog: $uid") | Out-Null + } + + $reviewStatus = [string]$unitCoverage.reviewStatus + if ($validStatuses -notcontains $reviewStatus) { + $problems.Add("${uid}: invalid reviewStatus '$reviewStatus'.") | Out-Null + } + + [object[]]$outcomes = @() + if ($unitCoverage.PSObject.Properties.Name -contains 'outcomes') { + $outcomes = @($unitCoverage.outcomes) + } + if ($reviewStatus -eq 'in-progress' -and -not $outcomes.Count) { + $problems.Add("${uid}: an in-progress unit must contain at least one outcome.") | Out-Null + } + + foreach ($outcome in $outcomes) { + $id = [string]$outcome.id + if ($id -notmatch '^[a-z0-9]+(?:-[a-z0-9]+)*$') { + $problems.Add("${uid}: outcome id must be kebab-case: '$id'.") | Out-Null + } elseif (-not $seenOutcomes.Add($id)) { + $problems.Add("Duplicate outcome id: $id") | Out-Null + } + + $disposition = [string]$outcome.disposition + if ($validDispositions -notcontains $disposition) { + $problems.Add("${id}: invalid disposition '$disposition'.") | Out-Null + continue + } + $dispositionCounts[$disposition]++ + + if ([string]::IsNullOrWhiteSpace([string]$outcome.title)) { + $problems.Add("${id}: title is required.") | Out-Null + } + + if ($disposition -in @('candidate', 'authored', 'covered-existing')) { + if ([string]::IsNullOrWhiteSpace([string]$outcome.domain)) { + $problems.Add("${id}: domain is required for disposition '$disposition'.") | Out-Null + } + } + + [object[]]$paths = @() + if ($outcome.PSObject.Properties.Name -contains 'articlePaths') { + $paths = @($outcome.articlePaths) + } + if ($disposition -in @('authored', 'covered-existing')) { + if (-not $paths.Count) { + $problems.Add("${id}: articlePaths is required for disposition '$disposition'.") | Out-Null + } + foreach ($relativePath in $paths) { + $relativePath = [string]$relativePath + if ($relativePath.Contains('\') -or -not $relativePath.EndsWith('.md')) { + $problems.Add("${id}: article path must be a forward-slash .md path: $relativePath") | Out-Null + continue + } + if (-not (Test-Path -LiteralPath (Join-Path $Root $relativePath) -PathType Leaf)) { + $problems.Add("${id}: article does not exist: $relativePath") | Out-Null + } + $articlePaths.Add($relativePath) | Out-Null + } + } elseif ($paths.Count) { + $problems.Add("${id}: disposition '$disposition' must not set articlePaths.") | Out-Null + } + + $rationale = if ($outcome.PSObject.Properties.Name -contains 'rationale') { + [string]$outcome.rationale + } else { + '' + } + if ($disposition -in @('rejected', 'deferred') -and + [string]::IsNullOrWhiteSpace($rationale)) { + $problems.Add("${id}: rationale is required for disposition '$disposition'.") | Out-Null + } + } +} + +if ($problems.Count) { + Write-Host "Learn coverage validation FAILED ($($problems.Count) problem(s)):" -ForegroundColor Red + $problems | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + exit 1 +} + +$complete = @($coverage.units | Where-Object reviewStatus -eq 'complete').Count +$inProgress = @($coverage.units | Where-Object reviewStatus -eq 'in-progress').Count +$summary = [ordered]@{ + catalogUnits = $catalogUnits.Count + completeUnits = $complete + inProgressUnits = $inProgress + unreviewedUnits = $catalogUnits.Count - $complete - $inProgress + candidateOutcomes = $dispositionCounts.candidate + authoredOutcomes = $dispositionCounts.authored + coveredExistingOutcomes = $dispositionCounts.'covered-existing' + rejectedOutcomes = $dispositionCounts.rejected + deferredOutcomes = $dispositionCounts.deferred + referencedArticles = $articlePaths.Count +} + +if ($Json) { + $summary | ConvertTo-Json +} else { + $message = ( + 'Learn coverage: {0} units; {1} complete, {2} in progress, {3} unreviewed; ' + + '{4} candidate outcomes, {5} authored outcomes, {6} existing-coverage outcomes.' + ) -f @( + $summary.catalogUnits, + $summary.completeUnits, + $summary.inProgressUnits, + $summary.unreviewedUnits, + $summary.candidateOutcomes, + $summary.authoredOutcomes, + $summary.coveredExistingOutcomes + ) + Write-Host $message +} diff --git a/tools/Update-LearnCatalog.ps1 b/tools/Update-LearnCatalog.ps1 new file mode 100644 index 00000000..2368f604 --- /dev/null +++ b/tools/Update-LearnCatalog.ps1 @@ -0,0 +1,262 @@ +<# +.SYNOPSIS + Updates the committed Microsoft Learn Business Central developer catalog. + +.DESCRIPTION + Filters a Microsoft Learn catalog payload to modules tagged for both + dynamics-business-central and developer. The output is intentionally only + source metadata; editorial dispositions live in coverage/learn-coverage.json. + + The legacy unauthenticated catalog endpoint remains the default while it is + available. Use -CatalogPath with an export from the authenticated Learn + Platform API when the legacy endpoint is retired. +#> +[CmdletBinding(DefaultParameterSetName = 'Remote')] +param( + [string] $Root = (Resolve-Path (Join-Path $PSScriptRoot '..')), + + [Parameter(ParameterSetName = 'Remote')] + [uri] $CatalogUri = 'https://learn.microsoft.com/api/catalog/?locale=en-us', + + [Parameter(Mandatory, ParameterSetName = 'File')] + [string] $CatalogPath, + + [string] $OutputPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$Root = (Resolve-Path -LiteralPath $Root).Path +if (-not $OutputPath) { + $OutputPath = Join-Path $Root 'coverage/microsoft-learn-developer-catalog.json' +} + +function Get-CanonicalUrl { + param([string] $Url) + + if ([string]::IsNullOrWhiteSpace($Url)) { + return $null + } + + $parsed = [uri]$Url + return "$($parsed.Scheme)://$($parsed.Host)$($parsed.AbsolutePath)".TrimEnd('/') +} + +function Get-CatalogProperty { + param( + [object] $Object, + [string] $Name, + [object] $Default = $null + ) + + $property = $Object.PSObject.Properties[$Name] + if ($property) { + return $property.Value + } + return $Default +} + +function Get-LatestTimestamp { + param([object[]] $Values) + + $timestamps = @( + $Values | + Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | + ForEach-Object { [datetimeoffset]::Parse([string]$_) } | + Sort-Object + ) + if (-not $timestamps.Count) { + return $null + } + + return $timestamps[-1].ToUniversalTime().ToString( + "yyyy-MM-dd'T'HH:mm:ss'Z'", + [System.Globalization.CultureInfo]::InvariantCulture + ) +} + +function Convert-ToUtcTimestamp { + param([object] $Value) + + return ([datetimeoffset]$Value).ToUniversalTime().ToString( + "yyyy-MM-dd'T'HH:mm:ss'Z'", + [System.Globalization.CultureInfo]::InvariantCulture + ) +} + +$catalog = if ($PSCmdlet.ParameterSetName -eq 'File') { + Get-Content -LiteralPath $CatalogPath -Raw | ConvertFrom-Json +} else { + Invoke-RestMethod -Uri $CatalogUri +} + +$product = 'dynamics-business-central' +$role = 'developer' +$modules = @( + $catalog.modules | + Where-Object { + @(Get-CatalogProperty $_ 'products' @()) -contains $product -and + @(Get-CatalogProperty $_ 'roles' @()) -contains $role + } | + Sort-Object uid +) + +$moduleIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) +foreach ($module in $modules) { + $moduleIds.Add([string]$module.uid) | Out-Null +} + +$learningPaths = @( + $catalog.learningPaths | + Where-Object { + $path = $_ + $pathModules = @(Get-CatalogProperty $path 'modules' @()) + @($pathModules | Where-Object { $moduleIds.Contains([string]$_) }).Count -gt 0 -and + @(Get-CatalogProperty $path 'products' @()) -contains $product -and + @(Get-CatalogProperty $path 'roles' @()) -contains $role + } | + Sort-Object uid +) + +$pathIdsByModule = @{} +foreach ($path in $learningPaths) { + foreach ($moduleId in @(Get-CatalogProperty $path 'modules' @())) { + if (-not $moduleIds.Contains([string]$moduleId)) { + continue + } + if (-not $pathIdsByModule.ContainsKey([string]$moduleId)) { + $pathIdsByModule[[string]$moduleId] = [System.Collections.Generic.List[string]]::new() + } + $pathIdsByModule[[string]$moduleId].Add([string]$path.uid) + } +} + +$unitById = @{} +foreach ($unit in @(Get-CatalogProperty $catalog 'units' @())) { + $unitById[[string]$unit.uid] = $unit +} + +$moduleIdsByUnit = @{} +$moduleById = @{} +$unitIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) +foreach ($module in $modules) { + $moduleById[[string]$module.uid] = $module + foreach ($unitId in @(Get-CatalogProperty $module 'units' @())) { + $unitId = [string]$unitId + $unitIds.Add($unitId) | Out-Null + if (-not $moduleIdsByUnit.ContainsKey($unitId)) { + $moduleIdsByUnit[$unitId] = [System.Collections.Generic.List[string]]::new() + } + $moduleIdsByUnit[$unitId].Add([string]$module.uid) + } +} + +$pathRecords = @( + foreach ($path in $learningPaths) { + [ordered]@{ + uid = [string]$path.uid + title = [string]$path.title + url = Get-CanonicalUrl ([string]$path.url) + lastModified = if (Get-CatalogProperty $path 'last_modified') { + Convert-ToUtcTimestamp (Get-CatalogProperty $path 'last_modified') + } else { + $null + } + durationInMinutes = [int](Get-CatalogProperty $path 'duration_in_minutes' 0) + moduleUids = @( + @(Get-CatalogProperty $path 'modules' @()) | + Where-Object { $moduleIds.Contains([string]$_) } | + ForEach-Object { [string]$_ } | + Sort-Object -Unique + ) + } + } +) + +$moduleRecords = @( + foreach ($module in $modules) { + [ordered]@{ + uid = [string]$module.uid + title = [string]$module.title + summary = [string](Get-CatalogProperty $module 'summary' '') + url = Get-CanonicalUrl ([string]$module.url) + lastModified = Convert-ToUtcTimestamp (Get-CatalogProperty $module 'last_modified') + durationInMinutes = [int](Get-CatalogProperty $module 'duration_in_minutes' 0) + levels = @(@(Get-CatalogProperty $module 'levels' @()) | ForEach-Object { [string]$_ } | Sort-Object -Unique) + subjects = @(@(Get-CatalogProperty $module 'subjects' @()) | ForEach-Object { [string]$_ } | Sort-Object -Unique) + learningPathUids = if ($pathIdsByModule.ContainsKey([string]$module.uid)) { + @($pathIdsByModule[[string]$module.uid] | Sort-Object -Unique) + } else { + @() + } + unitUids = @(@(Get-CatalogProperty $module 'units' @()) | ForEach-Object { [string]$_ }) + } + } +) + +$unitRecords = @( + foreach ($unitId in @($unitIds | Sort-Object)) { + if (-not $unitById.ContainsKey($unitId)) { + throw "Catalog module references missing unit '$unitId'." + } + $unit = $unitById[$unitId] + $unitUrl = [string](Get-CatalogProperty $unit 'url' '') + if ([string]::IsNullOrWhiteSpace($unitUrl)) { + $moduleId = [string]@($moduleIdsByUnit[$unitId])[0] + $moduleUrl = Get-CanonicalUrl ([string]$moduleById[$moduleId].url) + $unitSlug = if ($unitId.StartsWith("$moduleId.", [System.StringComparison]::Ordinal)) { + $unitId.Substring($moduleId.Length + 1) + } else { + @($unitId -split '\.')[-1] + } + $unitUrl = "$moduleUrl/$unitSlug" + } + [ordered]@{ + uid = $unitId + title = [string]$unit.title + url = Get-CanonicalUrl $unitUrl + lastModified = if (Get-CatalogProperty $unit 'last_modified') { + Convert-ToUtcTimestamp (Get-CatalogProperty $unit 'last_modified') + } else { + $null + } + durationInMinutes = [int](Get-CatalogProperty $unit 'duration_in_minutes' 0) + moduleUids = @($moduleIdsByUnit[$unitId] | Sort-Object -Unique) + } + } +) + +$snapshot = [ordered]@{ + version = 1 + source = [ordered]@{ + provider = 'Microsoft Learn' + catalogUri = if ($PSCmdlet.ParameterSetName -eq 'Remote') { [string]$CatalogUri } else { $null } + locale = 'en-us' + filters = [ordered]@{ + product = $product + role = $role + } + catalogLastModified = Get-LatestTimestamp @( + @($moduleRecords | ForEach-Object lastModified) + + @($unitRecords | ForEach-Object lastModified) + + @($pathRecords | ForEach-Object lastModified) + ) + } + counts = [ordered]@{ + learningPaths = $pathRecords.Count + modules = $moduleRecords.Count + units = $unitRecords.Count + } + learningPaths = $pathRecords + modules = $moduleRecords + units = $unitRecords +} + +$outputDirectory = Split-Path -Parent $OutputPath +if (-not (Test-Path -LiteralPath $outputDirectory -PathType Container)) { + New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null +} +$snapshot | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $OutputPath -Encoding UTF8 +Write-Host "Microsoft Learn developer catalog: $($pathRecords.Count) paths, $($moduleRecords.Count) modules, $($unitRecords.Count) units." +Write-Host "Catalog: $OutputPath" From f6fca1d56d230ec85d582ba6e5d08fac1b9a2c14 Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Fri, 4 Sep 2026 13:36:49 +0200 Subject: [PATCH 2/4] Close AL development contract gaps Bound post-implementation review rounds, expose output kinds in Entry dispatch, enforce capability coverage, map BCFIX-HANDOFF v1, clarify no-knowledge behavior, and reject repository-escaping skill paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 638b66d2-9f06-4f60-8781-808709e1485c --- .github/scripts/validate_frontmatter.py | 65 +++++++++++--- README.md | 5 ++ agent-consumption.md | 6 +- coverage/README.md | 4 + coverage/development-capabilities.json | 7 +- evaluation/README.md | 3 +- evaluation/development-fixtures.json | 49 +++++++++++ evaluation/development-guidance-fixtures.json | 84 +++++++++++++++---- .../skills/development/al-development-plan.md | 1 + .../skills/development/al-development.md | 15 +++- skills/al-development/SKILL.md | 5 ++ skills/do.md | 11 ++- skills/entry.md | 9 +- tools/Test-DevelopmentFixtures.ps1 | 80 ++++++++++++++++-- tools/Test-DevelopmentGuidanceFixtures.ps1 | 64 ++++++++++++-- 15 files changed, 356 insertions(+), 52 deletions(-) diff --git a/.github/scripts/validate_frontmatter.py b/.github/scripts/validate_frontmatter.py index ada7505c..237545e3 100644 --- a/.github/scripts/validate_frontmatter.py +++ b/.github/scripts/validate_frontmatter.py @@ -18,7 +18,7 @@ import re import sys from dataclasses import dataclass, field -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import Any, Iterable try: @@ -39,7 +39,7 @@ } ACTION_SKILL_OPTIONAL_KEYS = { "bc-version", "technologies", "countries", "application-area", "sub-skills", - "quality-skill", "guidance-skill", + "quality-skill", "quality-round-limit", "guidance-skill", } META_SKILL_REQUIRED_KEYS = {"kind", "id", "version", "title"} ENTRY_SKILL_REQUIRED_KEYS = {"kind", "id", "version", "title"} @@ -151,6 +151,28 @@ def is_non_empty_list_of_str(value: Any) -> bool: return isinstance(value, list) and len(value) > 0 and all(isinstance(v, str) and v for v in value) +def normalize_repo_md_path(value: Any) -> tuple[str | None, str | None]: + """Normalize an optional leading './' and reject absolute/escaping paths.""" + if not isinstance(value, str) or not value: + return None, "must be a non-empty string" + if "\\" in value: + return None, "must use forward slashes" + + normalized = value[2:] if value.startswith("./") else value + segments = normalized.split("/") + if ( + not normalized + or normalized.startswith("/") + or re.match(r"^[A-Za-z]:", normalized) + or any(segment in ("", ".", "..") for segment in segments) + or PurePosixPath(normalized).is_absolute() + ): + return None, "must be a repository-relative path that does not escape the repository" + if not normalized.endswith(".md"): + return None, "must end in '.md'" + return normalized, None + + def expand_bc_version(value: Any) -> tuple[list[int] | str | None, str | None]: """Return (expanded, error-message). One of the two is None. @@ -384,21 +406,34 @@ def validate_action_skill(path: Path, parsed: Parsed, report: Report) -> None: if not is_non_empty_list_of_str(ss): report.error(path, "R20", "sub-skills must be a non-empty list of repo-relative paths", 1) else: - bad = [x for x in ss if not x.endswith(".md")] + bad = [f"{x}: {err}" for x in ss if (err := normalize_repo_md_path(x)[1])] if bad: - report.error(path, "R20", f"sub-skills entries must end in '.md': {bad}", 1) + report.error(path, "R20", f"invalid sub-skills paths: {bad}", 1) if "quality-skill" in fm: quality_skill = fm["quality-skill"] - if not isinstance(quality_skill, str) or not quality_skill.endswith(".md"): - report.error(path, "R31", "quality-skill must be one repo-relative .md path", 1) + _, err = normalize_repo_md_path(quality_skill) + if err: + report.error(path, "R31", f"quality-skill {err}", 1) if fm.get("outputs") != ["implementation-report"]: report.error(path, "R31", "quality-skill is valid only with outputs: [implementation-report]", 1) + if "quality-round-limit" not in fm: + report.error(path, "R31", "quality-skill requires quality-round-limit", 1) + + if "quality-round-limit" in fm: + limit = fm["quality-round-limit"] + if not isinstance(limit, int) or isinstance(limit, bool) or limit <= 0: + report.error(path, "R31", "quality-round-limit must be a positive integer", 1) + if "quality-skill" not in fm: + report.error(path, "R31", "quality-round-limit requires quality-skill", 1) + if fm.get("outputs") != ["implementation-report"]: + report.error(path, "R31", "quality-round-limit is valid only with outputs: [implementation-report]", 1) if "guidance-skill" in fm: guidance_skill = fm["guidance-skill"] - if not isinstance(guidance_skill, str) or not guidance_skill.endswith(".md"): - report.error(path, "R32", "guidance-skill must be one repo-relative .md path", 1) + _, err = normalize_repo_md_path(guidance_skill) + if err: + report.error(path, "R32", f"guidance-skill {err}", 1) if fm.get("outputs") != ["implementation-report"]: report.error(path, "R32", "guidance-skill is valid only with outputs: [implementation-report]", 1) @@ -598,7 +633,11 @@ def validate_sub_skills_registry(path: Path, fm: dict[str, Any], root: Path, rep if not is_non_empty_list_of_str(ss): return - declared = {s.lstrip("./") for s in ss} + declared = { + normalized + for s in ss + if (normalized := normalize_repo_md_path(s)[0]) is not None + } # Sibling leaves on disk, excluding the super-skill file itself. leaves = { @@ -626,10 +665,10 @@ def validate_sub_skills_registry(path: Path, fm: dict[str, Any], root: Path, rep def validate_quality_skill(path: Path, fm: dict[str, Any], root: Path, report: Report) -> None: """R30: implementation quality-skill paths resolve to a findings producer.""" quality_skill = fm.get("quality-skill") - if not isinstance(quality_skill, str) or not quality_skill.endswith(".md"): + normalized, err = normalize_repo_md_path(quality_skill) + if err or normalized is None: return - normalized = quality_skill.lstrip("./") target = root / normalized if not target.is_file(): report.error(path, "R30", f"quality-skill does not exist on disk: {normalized}", 1) @@ -651,10 +690,10 @@ def validate_quality_skill(path: Path, fm: dict[str, Any], root: Path, report: R def validate_guidance_skill(path: Path, fm: dict[str, Any], root: Path, report: Report) -> None: """R33: implementation guidance-skill paths resolve to a read-only planner.""" guidance_skill = fm.get("guidance-skill") - if not isinstance(guidance_skill, str) or not guidance_skill.endswith(".md"): + normalized, err = normalize_repo_md_path(guidance_skill) + if err or normalized is None: return - normalized = guidance_skill.lstrip("./") target = root / normalized if not target.is_file(): report.error(path, "R33", f"guidance-skill does not exist on disk: {normalized}", 1) diff --git a/README.md b/README.md index 8a97d214..6d5d8154 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,11 @@ Repository-specific orchestrators do not need to delegate implementation to plan, feed its read-only guidance report into their own phases, and retain their specialized environment, test, propagation, and delivery gates. +`al-development` does not silently fall back to generic generation when no +article applies. It returns `no-knowledge` without changing code, making corpus +coverage visible; callers can use their normal repository workflow or +contribute the missing Business Central-specific guidance. + Business Central functional domains (Finance, Supply Chain Management, Manufacturing, Jobs, Warehousing, Service), PowerShell, pipelines, and Power Platform remain valid future repository scope, but they are **not current coverage claims** until corresponding knowledge and action skills exist. Consumers should derive supported review scope from the live knowledge index and dispatched skills, not from roadmap breadth. ## Tracking developer coverage diff --git a/agent-consumption.md b/agent-consumption.md index d703aa24..62f6e714 100644 --- a/agent-consumption.md +++ b/agent-consumption.md @@ -44,7 +44,11 @@ development action skill itself or duplicate Entry's preparation, routing, and failure semantics. ### 3. Agent consumes the dispatch record -The dispatch record names one or more action skills and the subset of inputs each should receive. If the outcome is `no-match` or `failed`, the agent returns the record to the orchestrator unchanged. +The dispatch record names one or more action skills, the subset of inputs each +should receive, and each skill's output kind. The output kind identifies +read-only review or planning work versus repository-changing implementation +before invocation. If the outcome is `no-match` or `failed`, the agent returns +the record to the orchestrator unchanged. ### 4. Agent invokes each dispatched action skill Action skills live inside the layers — `/microsoft/skills/`, `/community/skills/`, `/custom/skills/` — so their authority is carried by their location. For a PR review, Entry typically dispatches `microsoft/skills/review/al-code-review.md`. The agent reads the file and executes it. diff --git a/coverage/README.md b/coverage/README.md index 3b2bb18a..200fd8aa 100644 --- a/coverage/README.md +++ b/coverage/README.md @@ -14,6 +14,10 @@ one-for-one. - `development-capabilities.json` tracks whether representative Business Central development capabilities have implementation fixtures. +The capability manifest declares `minimumFixtureCoverage`. CI fails when the +share of `fixture` or `validated` capabilities falls below that floor, so a +new planned capability cannot silently dilute generation coverage. + Each tracked unit has a `reviewStatus`: - `in-progress` — at least one concern has been identified, but editorial diff --git a/coverage/development-capabilities.json b/coverage/development-capabilities.json index 493316a0..1ad41132 100644 --- a/coverage/development-capabilities.json +++ b/coverage/development-capabilities.json @@ -1,5 +1,6 @@ { "version": 1, + "minimumFixtureCoverage": 0.5, "capabilities": [ { "id": "setup-and-master-data", @@ -82,13 +83,15 @@ { "id": "install-and-upgrade", "title": "Installation and data upgrade", - "status": "planned", + "status": "fixture", "domains": [ "breaking-changes", "testing", "upgrade" ], - "fixtureIds": [] + "fixtureIds": [ + "versioned-data-upgrade" + ] }, { "id": "external-services", diff --git a/evaluation/README.md b/evaluation/README.md index 40954eba..a3445fe4 100644 --- a/evaluation/README.md +++ b/evaluation/README.md @@ -83,7 +83,8 @@ pwsh ./tools/Test-DevelopmentFixtures.ps1 -Root . -ResultsDirectory ./.developme The initial fixtures cover setup-backed master data, document header/line workflows, versioned API integrations, and surgical diagnosis and repair of a -batch-processing bug. The broader capability roadmap lives in +batch-processing bug, plus a rerunnable data upgrade. The capability manifest +enforces a minimum fixture-backed coverage ratio; the broader roadmap lives in `coverage/development-capabilities.json`. ### Read-only plan guidance diff --git a/evaluation/development-fixtures.json b/evaluation/development-fixtures.json index dcedde4e..b0c9f024 100644 --- a/evaluation/development-fixtures.json +++ b/evaluation/development-fixtures.json @@ -1,6 +1,7 @@ { "version": 1, "skill": "microsoft/skills/development/al-development.md", + "maximumReviewRounds": 3, "minimumKnowledgeRecall": 1.0, "minimumKnowledgePrecision": 0.5, "cases": [ @@ -183,6 +184,54 @@ "tests", "review" ] + }, + { + "id": "versioned-data-upgrade", + "title": "Implement a rerunnable data upgrade", + "capabilities": [ + "install-and-upgrade" + ], + "expectedKind": "upgrade", + "development-request": { + "kind": "upgrade", + "description": "Upgrade an existing app from a text-based Customer Tier field to a new enum-backed Tier field. Preserve existing customer data, support tenants that skip intermediate app versions, keep fresh installation separate from migration, and add upgrade tests.", + "acceptance-criteria": [ + "Existing tier values are migrated without running field validation triggers.", + "The migration is guarded by an upgrade tag and is safe when the upgrade runs again.", + "Check and validation triggers do not write data.", + "Fresh installation does not run version-upgrade migration.", + "Tests cover more than one historical source data version." + ] + }, + "context": { + "technologies": [ + "al" + ], + "countries": [ + "w1" + ], + "application-area": [ + "all" + ] + }, + "requiredKnowledge": [ + "microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md", + "microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md", + "microsoft/knowledge/upgrade/use-upgrade-tags-not-version-checks.md", + "microsoft/knowledge/upgrade/check-only-triggers-do-not-migrate-data.md", + "microsoft/knowledge/upgrade/install-code-does-not-run-on-version-upgrade.md" + ], + "optionalKnowledge": [ + "microsoft/knowledge/upgrade/datatransfer-skips-triggers-and-subscribers.md", + "microsoft/knowledge/upgrade/datatransfer-for-bulk-init.md", + "microsoft/knowledge/upgrade/register-upgrade-tags-with-subscribers.md", + "microsoft/knowledge/testing/transactionmodel-attribute-governs-test-transactions.md" + ], + "requiredChecks": [ + "compile", + "tests", + "review" + ] } ] } diff --git a/evaluation/development-guidance-fixtures.json b/evaluation/development-guidance-fixtures.json index 9da4d0af..29417fab 100644 --- a/evaluation/development-guidance-fixtures.json +++ b/evaluation/development-guidance-fixtures.json @@ -2,29 +2,37 @@ "version": 1, "skill": "microsoft/skills/development/al-development-plan.md", "minimumKnowledgeRecall": 1.0, - "minimumKnowledgePrecision": 0.5, + "minimumKnowledgePrecision": 0.67, "cases": [ { "id": "bcapps-filtered-batch-bug-plan", - "title": "Select guidance for a filtered batch bug fix", + "title": "Select guidance from a BCFIX-HANDOFF v1 payload", + "expectedKind": "bug", "development-plan": { - "kind": "bug", - "request": "Fix a filtered batch routine that updates only the first matching record.", - "root-cause": "The routine calls FindFirst and updates the current record without entering an enumerator loop.", - "affected-files": [ - "src/Batch/UpdateSelectedEntries.Codeunit.al", + "format": "BCFIX-HANDOFF", + "version": 1, + "issue": 4312, + "phase": "implement", + "status": "paused", + "baton": 2, + "rootCause": "The routine calls FindFirst and updates the current record without entering an enumerator loop, so only the first record in the supplied filtered set is modified.", + "harnessMap": { + "testCodeunit": "Update Selected Entries Tests", + "libraries": [ + "Library - Random" + ], + "pages": [], + "handlers": [] + }, + "iterationsUsed": 1, + "filesCommitted": [ "test/Batch/UpdateSelectedEntries.Codeunit.al" ], - "proposed-changes": [ - "Iterate the supplied filtered record set and update every selected entry.", - "Add a regression test with multiple selected entries." + "lastTestResult": "1 failing, 4 passing; the red test shows only the first of three selected records is updated.", + "deadEnds": [ + "Changing the page selection did not help because the codeunit discarded the supplied enumerator." ], - "test-strategy": "Establish a red test where only one of several selected records is updated, then require all selected records to be updated after the fix.", - "acceptance-criteria": [ - "The supplied filters are preserved.", - "Every selected record is updated.", - "The regression test demonstrates red-to-green behavior." - ] + "nextStep": "Replace the single-record read with an update-safe FindSet/Next loop that preserves the supplied filters, then rerun the red test." }, "context": { "technologies": [ @@ -45,6 +53,50 @@ "optionalKnowledge": [ "microsoft/knowledge/performance/pass-var-record-to-preserve-partial-load-enumerator.md" ] + }, + { + "id": "versioned-upgrade-plan-guidance", + "title": "Select guidance for a versioned data upgrade", + "expectedKind": "upgrade", + "development-plan": { + "kind": "upgrade", + "request": "Migrate existing customer tier text values to a new enum field in an app upgrade.", + "root-cause": "The new schema needs an explicit, rerunnable migration for existing tenant data.", + "affected-files": [ + "src/Upgrade/CustomerTierUpgrade.Codeunit.al", + "test/Upgrade/CustomerTierUpgradeTests.Codeunit.al" + ], + "proposed-changes": [ + "Add a tagged upgrade step that copies existing values without validation triggers.", + "Add upgrade tests from multiple historical data versions." + ], + "test-strategy": "Run upgrade tests from two prior data versions and verify a second invocation makes no further changes.", + "acceptance-criteria": [ + "Existing values are preserved.", + "The migration is rerunnable.", + "Fresh installation does not execute upgrade migration." + ] + }, + "context": { + "technologies": [ + "al" + ], + "countries": [ + "w1" + ], + "application-area": [ + "all" + ] + }, + "requiredKnowledge": [ + "microsoft/knowledge/upgrade/use-upgrade-tags-not-version-checks.md", + "microsoft/knowledge/upgrade/check-only-triggers-do-not-migrate-data.md", + "microsoft/knowledge/upgrade/install-code-does-not-run-on-version-upgrade.md" + ], + "optionalKnowledge": [ + "microsoft/knowledge/upgrade/datatransfer-skips-triggers-and-subscribers.md", + "microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md" + ] } ] } diff --git a/microsoft/skills/development/al-development-plan.md b/microsoft/skills/development/al-development-plan.md index f824b683..976a11cf 100644 --- a/microsoft/skills/development/al-development-plan.md +++ b/microsoft/skills/development/al-development-plan.md @@ -38,6 +38,7 @@ When a dimension cannot be resolved, retain conditionally applicable candidates ## Worklist 1. Normalize the plan into: request summary, development kind, assumptions, root cause or design intent, affected files and symbols, proposed changes, test strategy, and acceptance criteria. When the plan has no normalized kind, apply the same categories as `al-development`: new or expanded behavior is `feature`, a defect correction is `bug`, behavior-preserving restructuring is `refactor`, migration is `upgrade`, and other bounded work is `maintenance`. A repository-specific additive event or extensibility request maps to `feature`; retain its original work-item type in the request summary. Do not redesign the repository-specific workflow. + - For a `BCFIX-HANDOFF` v1 payload, map `rootCause` to root cause, `harnessMap` to test context, `filesCommitted` to affected files, `lastTestResult` to existing test evidence, `deadEnds` to rejected approaches, and `nextStep` to the immediate proposed change. Preserve `issue`, `phase`, `status`, `baton`, and `iterationsUsed` as workflow context only; they do not create Business Central constraints. A handoff with a non-empty root cause is `bug` unless the surrounding plan identifies an additive Event Request, which maps to `feature`. 2. Build retrieval vocabulary from the plan and confirmed repository symbols. Give exact object types, properties, methods, analyzers, errors, and affected domains more weight than broad business nouns. 3. Search the index in separate passes: - data ownership, keys, setup, numbering, validation, transactions, and upgrade; diff --git a/microsoft/skills/development/al-development.md b/microsoft/skills/development/al-development.md index c4eefd74..755d46ca 100644 --- a/microsoft/skills/development/al-development.md +++ b/microsoft/skills/development/al-development.md @@ -12,6 +12,7 @@ countries: [w1] application-area: [all] guidance-skill: microsoft/skills/development/al-development-plan.md quality-skill: microsoft/skills/review/al-code-review.md +quality-round-limit: 3 --- # AL development @@ -66,7 +67,7 @@ Record unresolved dimensions in the development plan rather than silently substi 6. Invoke the frontmatter `guidance-skill` with that plan, the repository, and the resolved context. It performs Source, Relevance, and knowledge worklisting independently and read-only. 7. Require a complete guidance result before editing product code: - `completed` — use every returned constraint and validation consideration. - - `no-knowledge` — return `no-knowledge` without implementing a Business Central-specific change. + - `no-knowledge` — intentionally refuse to implement: return `no-knowledge` with no request changes, set `outcome-reason` to `No applicable BCQuality knowledge was found for this development plan.`, and add a `remaining` entry directing the caller to use a repository-specific workflow/general coding agent or contribute the missing BC-specific knowledge. - `not-applicable`, `partial`, or `failed` — return the corresponding non-completed outcome without editing product code; preserve its reason in `remaining`. 8. Copy the guidance report's selected paths into the eventual implementation report only when the corresponding constraint materially shaped the implementation. Carry its suppression records forward. @@ -83,9 +84,15 @@ Record unresolved dimensions in the development plan rather than silently substi 4. Implement the request end to end. Include all surfaces required by the mode, acceptance criteria, and repository conventions. Do not create success-shaped stubs. 5. Treat the guidance report as design constraints throughout implementation. Adapt its referenced companion samples to the target codebase; never copy demonstration IDs or names blindly. 6. Run the smallest existing build, analyzer, and test commands that cover the change. Fix failures caused by the implementation. Record every command and real outcome in `validation`; unavailable checks are `not-run`, never `passed`. -7. Invoke the frontmatter `quality-skill` against the final implementation diff. Fix all justified knowledge-backed `blocker` and `major` findings and concrete defects introduced by this work, then rerun affected validation and review. Preserve the last findings-report in `review` and add a `validation` entry with `id: "review"`. If review is disabled or unavailable, record `not-run` and return `partial`. -8. Verify the persisted files against the implementation contract, acceptance criteria, and mode-specific evidence. If behavior, validation, or review remains incomplete, return `partial` and list the exact gap in `remaining`. +7. Invoke the frontmatter `quality-skill` against the final implementation diff, bounded by `quality-round-limit`: + - Record every invocation in `review-rounds`, including its gating `blocker` and `major` IDs. + - When no gating finding remains, mark the round `clean` and stop. + - Otherwise fix every justified, safely actionable gating finding, rerun affected validation, mark the round `fixing`, and start the next review round. + - Stop early as `stalled` when the gating ID set is unchanged from the preceding round, no gating finding can be fixed safely, or validation cannot be restored. + - When the final allowed round still has gating findings, mark it `limit-reached`. + Preserve the last complete findings-report in `review` and add a `validation` entry with `id: "review"`. A `stalled` or `limit-reached` loop returns `partial` with the unresolved gating findings in `remaining`. If review is disabled or unavailable, record `not-run` and return `partial`. +8. Verify the persisted files against the implementation contract, acceptance criteria, and mode-specific evidence. If behavior, validation, guidance, or review remains incomplete, return `partial` and list the exact gap in `remaining`. ## Output -Return one `implementation-report` conforming to DO. Set `plan.kind` to the classified execution mode. `knowledge` lists only articles opened in full and materially used. `changes` lists only files changed by this skill. `completed` requires a persisted implementation, passing required validation, and no unresolved `blocker` or `major` finding in `review`. +Return one `implementation-report` conforming to DO. Set `plan.kind` to the classified execution mode. `knowledge` lists only articles opened in full and materially used. `changes` lists only files changed by this skill. `completed` requires a persisted implementation, passing required validation, and no unresolved `blocker` or `major` finding in `review`. `no-knowledge` is a visible coverage decision, not an error or silent fallback. diff --git a/skills/al-development/SKILL.md b/skills/al-development/SKILL.md index 5f769801..2d8873a8 100644 --- a/skills/al-development/SKILL.md +++ b/skills/al-development/SKILL.md @@ -9,6 +9,11 @@ This is BCQuality's host-native adapter for standalone plugin installations. It When the target repository exposes a more specific local workflow for the request, such as an end-to-end bug-fix skill with its own environment and delivery gates, prefer that repository workflow unless the caller explicitly asks to use BCQuality's generic development skill. +This adapter is deliberately knowledge-backed: if BCQuality has no applicable +guidance, it returns a visible `no-knowledge` result without changing code. Use +the repository's normal coding workflow for unbacked requests, or add the +missing BC-specific knowledge before expecting this skill to implement them. + ## Execute 1. Resolve `PLUGIN_ROOT` to the directory containing this plugin's root `plugin.json`. This file is `PLUGIN_ROOT/skills/al-development/SKILL.md`; when the host does not expose the plugin root, resolve it two levels above this file. diff --git a/skills/do.md b/skills/do.md index 154e14ad..ddbd96d6 100644 --- a/skills/do.md +++ b/skills/do.md @@ -66,7 +66,7 @@ application-area: [all] `sub-skills` is an optional field. When present and non-empty, the skill is a **super-skill** that composes other action skills; see *Composition* below. Values are repo-relative paths to action-skill files. -`quality-skill` is optional on an action skill that emits an `implementation-report`. It names one repo-relative review action skill to run over the completed diff. It is a post-implementation gate, not a composed sub-skill: Entry does not route through it, and its complete findings-report is returned in `review`. Consumer configuration still applies; if the named quality skill is disabled or unavailable, record its validation as `not-run` and do not claim `completed`. +`quality-skill` is optional on an action skill that emits an `implementation-report`. It names one repo-relative review action skill to run over the completed diff. It is a post-implementation gate, not a composed sub-skill: Entry does not route through it, and its final complete findings-report is returned in `review`. `quality-round-limit` is the required positive maximum number of review/fix rounds when a quality skill is declared. Consumer configuration still applies; if the named quality skill is disabled or unavailable, record its validation as `not-run` and do not claim `completed`. `guidance-skill` is optional on an action skill that emits an `implementation-report`. It names one repo-relative read-only action skill that accepts a `development-plan` and emits a `development-guidance-report`. The implementation skill invokes it after forming its plan and before editing product code. Consumer configuration still applies; when guidance is disabled or unavailable, the implementation skill must not claim knowledge-backed development. @@ -346,6 +346,13 @@ An action skill with `outputs: [implementation-report]` emits one JSON document: } ], "review": { "...full findings-report from the post-implementation review..." : null }, + "review-rounds": [ + { + "round": 1, + "outcome": "clean | fixing | stalled | limit-reached", + "gating-finding-ids": ["string"] + } + ], "suppressed": [ { "reference": { "path": "string", "sha": "string" }, @@ -378,6 +385,8 @@ An action skill with `outputs: [implementation-report]` emits one JSON document: **`review`** is optional for generic implementation skills and required when a skill's instructions mandate post-implementation review. When present, it is the complete findings-report returned by that review skill, not a rewritten summary. +**`review-rounds`** records every quality-skill invocation in order. `gating-finding-ids` contains the `blocker` and `major` IDs from that round. `clean` ends successfully; `fixing` means the skill applied justified fixes before another round; `stalled` means the same gating set persisted or no safe progress was possible; `limit-reached` means the configured round cap was exhausted. The array length MUST NOT exceed `quality-round-limit`. `stalled` or `limit-reached` requires implementation outcome `partial`, the final findings-report in `review`, and every unresolved gating item in `remaining`. + **`suppressed`** has the same semantics as in a findings-report and records applicable knowledge excluded by layer precedence or configuration. **`remaining`** contains concrete unfinished work only. It is empty for `completed`. diff --git a/skills/entry.md b/skills/entry.md index 4a1b613f..aefab7d9 100644 --- a/skills/entry.md +++ b/skills/entry.md @@ -99,7 +99,8 @@ Emit a single JSON document conforming to the output contract below. Entry does "path": "microsoft/skills/review/al-code-review.md" }, "rationale": "string", - "inputs": ["pr-diff"] + "inputs": ["pr-diff"], + "outputs": ["findings-report"] } ], "skipped": [ @@ -126,6 +127,7 @@ Emit a single JSON document conforming to the output contract below. Entry does - `skill.version` — copied from the dispatched skill's frontmatter so the orchestrator can detect drift between dispatch time and execution. - `rationale` — short human-readable string, for logs and traceability. - `inputs` — the intersection of `task-context.inputs-available` and the skill's declared `inputs`. The agent MUST pass exactly this subset when invoking the skill. Sending a strict intersection avoids accidental information leakage between skills. +- `outputs` — the dispatched skill's complete, single-element `outputs` value copied from frontmatter. This lets an orchestrator distinguish read-only `findings-report` and `development-guidance-report` work from repository-changing `implementation-report` work before invoking the skill. An orchestrator MAY require an additional write confirmation for `implementation-report`; it MUST NOT infer side effects from the skill ID or title. Ordering of `dispatch[]` is not significant. @@ -163,7 +165,8 @@ Populated example (PR review on a repo where only `al-performance-review` is ena { "skill": { "id": "al-performance-review", "version": 1, "path": "microsoft/skills/review/al-performance-review.md" }, "rationale": "Goal 'review pull request' matched; inputs-available contains pr-diff.", - "inputs": ["pr-diff"] + "inputs": ["pr-diff"], + "outputs": ["findings-report"] } ], "skipped": [ @@ -177,7 +180,7 @@ Populated example (PR review on a repo where only `al-performance-review` is ena 1. Invoke Entry with the orchestrator-supplied task context. 2. Receive the dispatch record. -3. For each entry in `dispatch[]`, read the referenced action skill, execute its Source → Relevance → Worklist → Action steps per DO, and produce the report kind declared by that skill's single `outputs` value. +3. For each entry in `dispatch[]`, inspect `outputs` before invocation, read the referenced action skill, execute its Source → Relevance → Worklist → Action steps per DO, and produce the declared report kind. Verify the file's frontmatter output still equals the dispatch value; return `failed` on drift rather than executing an unexpectedly mutating skill. 4. Return the action-skill reports to the orchestrator. When Entry's `outcome` is `no-match` or `failed`, return the dispatch record itself so the orchestrator can log the reason. READ and DO are the contracts that govern what the dispatched skills do. An agent that has not yet read READ and DO reads them when it executes the first dispatched skill — they are not prerequisites for invoking Entry. diff --git a/tools/Test-DevelopmentFixtures.ps1 b/tools/Test-DevelopmentFixtures.ps1 index ecd52591..b5dd29ca 100644 --- a/tools/Test-DevelopmentFixtures.ps1 +++ b/tools/Test-DevelopmentFixtures.ps1 @@ -52,6 +52,22 @@ if ($manifest.version -ne 1) { if ($capabilityManifest.version -ne 1) { $problems.Add("Unsupported capability manifest version: $($capabilityManifest.version)") | Out-Null } +$minimumFixtureCoverage = if ($capabilityManifest.PSObject.Properties.Name -contains 'minimumFixtureCoverage') { + [double]$capabilityManifest.minimumFixtureCoverage +} else { + -1 +} +if ($minimumFixtureCoverage -lt 0 -or $minimumFixtureCoverage -gt 1) { + $problems.Add("minimumFixtureCoverage must be between 0 and 1.") | Out-Null +} +$maximumReviewRounds = if ($manifest.PSObject.Properties.Name -contains 'maximumReviewRounds') { + [int]$manifest.maximumReviewRounds +} else { + 0 +} +if ($maximumReviewRounds -le 0) { + $problems.Add("maximumReviewRounds must be a positive integer.") | Out-Null +} foreach ($thresholdName in @('minimumKnowledgeRecall', 'minimumKnowledgePrecision')) { $threshold = [double]$manifest.$thresholdName if ($threshold -lt 0 -or $threshold -gt 1) { @@ -62,6 +78,12 @@ foreach ($thresholdName in @('minimumKnowledgeRecall', 'minimumKnowledgePrecisio $skillPath = [string]$manifest.skill if (-not (Test-Path -LiteralPath (Join-Path $Root $skillPath) -PathType Leaf)) { $problems.Add("Development skill does not exist: $skillPath") | Out-Null +} else { + $skillText = Get-Content -LiteralPath (Join-Path $Root $skillPath) -Raw + $limitMatch = [regex]::Match($skillText, '(?m)^quality-round-limit:\s*(\d+)\s*$') + if (-not $limitMatch.Success -or [int]$limitMatch.Groups[1].Value -ne $maximumReviewRounds) { + $problems.Add("maximumReviewRounds must match the development skill quality-round-limit.") | Out-Null + } } $validChecks = @('compile', 'tests', 'review') @@ -173,6 +195,16 @@ foreach ($case in @($manifest.cases)) { } } +$fixtureBackedCount = @( + $capabilityManifest.capabilities | + Where-Object status -in @('fixture', 'validated') +).Count +$capabilityCount = @($capabilityManifest.capabilities).Count +$fixtureCoverage = if ($capabilityCount) { $fixtureBackedCount / $capabilityCount } else { 0.0 } +if ($fixtureCoverage -lt $minimumFixtureCoverage) { + $problems.Add("Fixture-backed capability coverage $fixtureCoverage is below minimumFixtureCoverage $minimumFixtureCoverage.") | Out-Null +} + if ($problems.Count) { Write-Host "Development fixture validation FAILED ($($problems.Count) problem(s)):" -ForegroundColor Red $problems | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } @@ -269,6 +301,11 @@ if ($PrepareDirectory) { findings = @() suppressed = @() } + 'review-rounds' = @([ordered]@{ + round = 1 + outcome = 'clean | fixing | stalled | limit-reached' + 'gating-finding-ids' = @() + }) suppressed = @() remaining = @() } @@ -303,7 +340,7 @@ if ($ResultsDirectory) { continue } $report = $result.implementationReport - foreach ($requiredField in @('skill', 'outcome', 'summary', 'plan', 'knowledge', 'changes', 'validation', 'review', 'suppressed', 'remaining')) { + foreach ($requiredField in @('skill', 'outcome', 'summary', 'plan', 'knowledge', 'changes', 'validation', 'review', 'review-rounds', 'suppressed', 'remaining')) { if ($report.PSObject.Properties.Name -notcontains $requiredField) { $failures.Add("$($case.id): implementation report is missing '$requiredField'.") | Out-Null } @@ -492,6 +529,41 @@ if ($ResultsDirectory) { $failures.Add("$($case.id): post-implementation review has $($gatingFindings.Count) gating finding(s).") | Out-Null } } + [object[]]$reviewRounds = @() + if ($report.PSObject.Properties.Name -contains 'review-rounds') { + $reviewRounds = @($report.'review-rounds') + } + if (-not $reviewRounds.Count -or $reviewRounds.Count -gt $maximumReviewRounds) { + $failures.Add("$($case.id): review-rounds must contain 1..$maximumReviewRounds entries.") | Out-Null + } else { + for ($index = 0; $index -lt $reviewRounds.Count; $index++) { + $round = $reviewRounds[$index] + $roundNumber = if ($round.PSObject.Properties.Name -contains 'round') { [int]$round.round } else { 0 } + $roundOutcome = if ($round.PSObject.Properties.Name -contains 'outcome') { [string]$round.outcome } else { '' } + if ($roundNumber -ne ($index + 1)) { + $failures.Add("$($case.id): review round numbering is not contiguous.") | Out-Null + } + if ($roundOutcome -notin @('clean', 'fixing', 'stalled', 'limit-reached')) { + $failures.Add("$($case.id): review round $roundNumber has invalid outcome '$roundOutcome'.") | Out-Null + } + [object[]]$roundGatingIds = @() + if ($round.PSObject.Properties.Name -contains 'gating-finding-ids') { + $roundGatingIds = @($round.'gating-finding-ids') + } + if ($roundOutcome -eq 'clean' -and $roundGatingIds.Count) { + $failures.Add("$($case.id): clean review round $roundNumber must have no gating IDs.") | Out-Null + } + if ($roundOutcome -in @('fixing', 'stalled', 'limit-reached') -and -not $roundGatingIds.Count) { + $failures.Add("$($case.id): review round $roundNumber outcome '$roundOutcome' requires gating IDs.") | Out-Null + } + if (@($roundGatingIds | Sort-Object -Unique).Count -ne $roundGatingIds.Count) { + $failures.Add("$($case.id): review round $roundNumber has duplicate gating IDs.") | Out-Null + } + } + if ([string]$reviewRounds[-1].outcome -ne 'clean') { + $failures.Add("$($case.id): completed implementation must end with a clean review round.") | Out-Null + } + } [object[]]$remaining = @() if ($report.PSObject.Properties.Name -contains 'remaining') { $remaining = @($report.remaining) @@ -508,9 +580,5 @@ if ($ResultsDirectory) { } Write-Host "Development fixture scoring PASSED: $(@($manifest.cases).Count) case(s)." } else { - $fixtureBackedCapabilities = @( - $capabilityManifest.capabilities | - Where-Object status -in @('fixture', 'validated') - ).Count - Write-Host "Development fixture validation PASSED: $(@($manifest.cases).Count) cases; $fixtureBackedCapabilities of $($capabilityIds.Count) capabilities have fixtures." + Write-Host "Development fixture validation PASSED: $(@($manifest.cases).Count) cases; $fixtureBackedCount of $($capabilityIds.Count) capabilities have fixtures (minimum $minimumFixtureCoverage)." } diff --git a/tools/Test-DevelopmentGuidanceFixtures.ps1 b/tools/Test-DevelopmentGuidanceFixtures.ps1 index d8709ffb..0018ffc6 100644 --- a/tools/Test-DevelopmentGuidanceFixtures.ps1 +++ b/tools/Test-DevelopmentGuidanceFixtures.ps1 @@ -36,6 +36,22 @@ function Get-ModelCaseId { } } +function Get-PlanRequest { + param([object] $Plan) + + if ($Plan.PSObject.Properties.Name -contains 'request' -and + -not [string]::IsNullOrWhiteSpace([string]$Plan.request)) { + return [string]$Plan.request + } + if ($Plan.PSObject.Properties.Name -contains 'format' -and + [string]$Plan.format -eq 'BCFIX-HANDOFF' -and + $Plan.PSObject.Properties.Name -contains 'nextStep') { + $issue = if ($Plan.PSObject.Properties.Name -contains 'issue') { [string]$Plan.issue } else { 'unknown' } + return "Continue BCFIX issue #${issue}: $($Plan.nextStep)" + } + return '' +} + if ($manifest.version -ne 1) { $problems.Add("Unsupported guidance fixture version: $($manifest.version)") | Out-Null } @@ -64,11 +80,49 @@ foreach ($case in @($manifest.cases)) { continue } $plan = $case.'development-plan' - if ($validKinds -notcontains [string]$plan.kind) { + $expectedKind = if ($case.PSObject.Properties.Name -contains 'expectedKind') { + [string]$case.expectedKind + } else { + '' + } + if ($validKinds -notcontains $expectedKind) { + $problems.Add("${id}: expectedKind is invalid.") | Out-Null + } + $isBcfixHandoff = ( + $plan.PSObject.Properties.Name -contains 'format' -and + [string]$plan.format -eq 'BCFIX-HANDOFF' + ) + if ($isBcfixHandoff) { + $requiredHandoffFields = @( + 'version', 'issue', 'phase', 'status', 'baton', 'rootCause', + 'harnessMap', 'iterationsUsed', 'filesCommitted', 'lastTestResult', + 'deadEnds', 'nextStep' + ) + foreach ($field in $requiredHandoffFields) { + if ($plan.PSObject.Properties.Name -notcontains $field) { + $problems.Add("${id}: BCFIX-HANDOFF is missing '$field'.") | Out-Null + } + } + $handoffVersion = if ($plan.PSObject.Properties.Name -contains 'version') { [int]$plan.version } else { 0 } + $handoffPhase = if ($plan.PSObject.Properties.Name -contains 'phase') { [string]$plan.phase } else { '' } + $handoffStatus = if ($plan.PSObject.Properties.Name -contains 'status') { [string]$plan.status } else { '' } + if ($handoffVersion -ne 1) { + $problems.Add("${id}: only BCFIX-HANDOFF version 1 is supported.") | Out-Null + } + if ($handoffPhase -notin @('plan', 'baseline', 'implement', 'pr')) { + $problems.Add("${id}: BCFIX-HANDOFF phase is invalid.") | Out-Null + } + if ($handoffStatus -notin @('in-progress', 'paused', 'done')) { + $problems.Add("${id}: BCFIX-HANDOFF status is invalid.") | Out-Null + } + } elseif ( + $plan.PSObject.Properties.Name -notcontains 'kind' -or + $validKinds -notcontains [string]$plan.kind + ) { $problems.Add("${id}: development-plan.kind is invalid.") | Out-Null } - if ([string]::IsNullOrWhiteSpace([string]$plan.request)) { - $problems.Add("${id}: development-plan.request is required.") | Out-Null + if ([string]::IsNullOrWhiteSpace((Get-PlanRequest $plan))) { + $problems.Add("${id}: development-plan must provide request or BCFIX nextStep.") | Out-Null } $expectedKnowledge = @($case.requiredKnowledge) + @($case.optionalKnowledge) @@ -125,7 +179,7 @@ if ($PrepareDirectory) { skillInstructions = $skillInstructions knowledgeIndex = 'knowledge-index.json' 'task-context' = [ordered]@{ - goal = [string]$case.'development-plan'.request + goal = Get-PlanRequest $case.'development-plan' 'inputs-available' = @('development-plan', 'repository') technologies = @($case.context.technologies) countries = @($case.context.countries) @@ -216,7 +270,7 @@ if ($ResultsDirectory) { $report.PSObject.Properties.Name -contains 'summary' -and $report.summary.PSObject.Properties.Name -contains 'kind' ) { [string]$report.summary.kind } else { '' } - if ($kind -ne [string]$case.'development-plan'.kind) { + if ($kind -ne [string]$case.expectedKind) { $failures.Add("$($case.id): summary.kind '$kind' does not match the plan.") | Out-Null } From 1deac52a53f9638b10890658b96d54267ca68fac Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Mon, 7 Sep 2026 11:47:42 +0200 Subject: [PATCH 3/4] Narrow AL development to read-only plan guidance Retain shared knowledge enrichment and review guidance; defer standalone implementation and source-ingestion tracking. Add runner-owned baseline evidence, contract regressions, and explicit consumer/pilot boundaries. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .claude-plugin/marketplace.json | 2 +- .github/scripts/validate_frontmatter.py | 88 +- ...-coverage.yml => development-guidance.yml} | 21 +- README.md | 59 +- agent-consumption.md | 102 +- coverage/README.md | 48 - coverage/development-capabilities.json | 132 - coverage/learn-coverage.json | 456 - .../microsoft-learn-developer-catalog.json | 8282 ----------------- evaluation/README.md | 130 +- evaluation/development-fixtures.json | 237 - evaluation/development-guidance-fixtures.json | 133 +- .../skills/development/al-development-plan.md | 11 +- .../skills/development/al-development.md | 98 - plugin.json | 4 +- skills/README.md | 10 +- skills/al-development-plan/SKILL.md | 22 + skills/al-development/SKILL.md | 34 - skills/do.md | 112 +- skills/entry.md | 10 +- tools/DevelopmentGuidance.Evidence.ps1 | 496 + tools/Test-DevelopmentFixtures.ps1 | 584 -- tools/Test-DevelopmentGuidanceEvaluator.ps1 | 421 + tools/Test-DevelopmentGuidanceFixtures.ps1 | 513 +- tools/Test-LearnCoverage.ps1 | 240 - tools/Update-LearnCatalog.ps1 | 262 - 26 files changed, 1478 insertions(+), 11029 deletions(-) rename .github/workflows/{development-coverage.yml => development-guidance.yml} (52%) delete mode 100644 coverage/README.md delete mode 100644 coverage/development-capabilities.json delete mode 100644 coverage/learn-coverage.json delete mode 100644 coverage/microsoft-learn-developer-catalog.json delete mode 100644 evaluation/development-fixtures.json delete mode 100644 microsoft/skills/development/al-development.md create mode 100644 skills/al-development-plan/SKILL.md delete mode 100644 skills/al-development/SKILL.md create mode 100644 tools/DevelopmentGuidance.Evidence.ps1 delete mode 100644 tools/Test-DevelopmentFixtures.ps1 create mode 100644 tools/Test-DevelopmentGuidanceEvaluator.ps1 delete mode 100644 tools/Test-LearnCoverage.ps1 delete mode 100644 tools/Update-LearnCatalog.ps1 diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5926b79f..1fd35cd9 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ { "name": "bcquality", "source": "./", - "description": "Business Central AL quality knowledge base and skills, packaged as an installable plugin. Exposes development and review adapters while preserving BCQuality's internal Entry and action-skill protocols.", + "description": "Business Central AL quality knowledge base and skills, packaged as an installable plugin. Exposes read-only plan enrichment and review adapters through BCQuality's Entry protocol.", "version": "0.3.0", "skills": [ "./skills/" diff --git a/.github/scripts/validate_frontmatter.py b/.github/scripts/validate_frontmatter.py index 237545e3..46ae20e0 100644 --- a/.github/scripts/validate_frontmatter.py +++ b/.github/scripts/validate_frontmatter.py @@ -39,7 +39,6 @@ } ACTION_SKILL_OPTIONAL_KEYS = { "bc-version", "technologies", "countries", "application-area", "sub-skills", - "quality-skill", "quality-round-limit", "guidance-skill", } META_SKILL_REQUIRED_KEYS = {"kind", "id", "version", "title"} ENTRY_SKILL_REQUIRED_KEYS = {"kind", "id", "version", "title"} @@ -47,10 +46,10 @@ STANDARD_INPUTS = { "pr-diff", "object-list", "file-path", "repository", "telemetry-query", - "development-request", "development-plan", + "development-plan", } ALLOWED_OUTPUTS = { - "findings-report", "implementation-report", "development-guidance-report", + "findings-report", "development-guidance-report", } VALID_SAMPLE_KINDS = {"good", "bad"} @@ -410,33 +409,6 @@ def validate_action_skill(path: Path, parsed: Parsed, report: Report) -> None: if bad: report.error(path, "R20", f"invalid sub-skills paths: {bad}", 1) - if "quality-skill" in fm: - quality_skill = fm["quality-skill"] - _, err = normalize_repo_md_path(quality_skill) - if err: - report.error(path, "R31", f"quality-skill {err}", 1) - if fm.get("outputs") != ["implementation-report"]: - report.error(path, "R31", "quality-skill is valid only with outputs: [implementation-report]", 1) - if "quality-round-limit" not in fm: - report.error(path, "R31", "quality-skill requires quality-round-limit", 1) - - if "quality-round-limit" in fm: - limit = fm["quality-round-limit"] - if not isinstance(limit, int) or isinstance(limit, bool) or limit <= 0: - report.error(path, "R31", "quality-round-limit must be a positive integer", 1) - if "quality-skill" not in fm: - report.error(path, "R31", "quality-round-limit requires quality-skill", 1) - if fm.get("outputs") != ["implementation-report"]: - report.error(path, "R31", "quality-round-limit is valid only with outputs: [implementation-report]", 1) - - if "guidance-skill" in fm: - guidance_skill = fm["guidance-skill"] - _, err = normalize_repo_md_path(guidance_skill) - if err: - report.error(path, "R32", f"guidance-skill {err}", 1) - if fm.get("outputs") != ["implementation-report"]: - report.error(path, "R32", "guidance-skill is valid only with outputs: [implementation-report]", 1) - # R21 five required sections, in order, each exactly once heads = [h for h, _ in headings_in_order(parsed.body)] indices: list[int] = [] @@ -662,58 +634,6 @@ def validate_sub_skills_registry(path: Path, fm: dict[str, Any], root: Path, rep report.error(path, "R26", f"leaf not registered in sub-skills: {leaf}", 1) -def validate_quality_skill(path: Path, fm: dict[str, Any], root: Path, report: Report) -> None: - """R30: implementation quality-skill paths resolve to a findings producer.""" - quality_skill = fm.get("quality-skill") - normalized, err = normalize_repo_md_path(quality_skill) - if err or normalized is None: - return - - target = root / normalized - if not target.is_file(): - report.error(path, "R30", f"quality-skill does not exist on disk: {normalized}", 1) - return - if target.resolve() == path.resolve(): - report.error(path, "R30", "quality-skill must not reference the implementation skill itself", 1) - return - - try: - target_parsed = parse_markdown(target.read_text(encoding="utf-8")) - except UnicodeDecodeError as e: - report.error(path, "R30", f"quality-skill is not valid UTF-8: {e}", 1) - return - target_outputs = (target_parsed.frontmatter or {}).get("outputs") - if target_outputs != ["findings-report"]: - report.error(path, "R30", f"quality-skill must emit findings-report: {normalized}", 1) - - -def validate_guidance_skill(path: Path, fm: dict[str, Any], root: Path, report: Report) -> None: - """R33: implementation guidance-skill paths resolve to a read-only planner.""" - guidance_skill = fm.get("guidance-skill") - normalized, err = normalize_repo_md_path(guidance_skill) - if err or normalized is None: - return - - target = root / normalized - if not target.is_file(): - report.error(path, "R33", f"guidance-skill does not exist on disk: {normalized}", 1) - return - if target.resolve() == path.resolve(): - report.error(path, "R33", "guidance-skill must not reference the implementation skill itself", 1) - return - - try: - target_parsed = parse_markdown(target.read_text(encoding="utf-8")) - except UnicodeDecodeError as e: - report.error(path, "R33", f"guidance-skill is not valid UTF-8: {e}", 1) - return - target_fm = target_parsed.frontmatter or {} - if target_fm.get("outputs") != ["development-guidance-report"]: - report.error(path, "R33", f"guidance-skill must emit development-guidance-report: {normalized}", 1) - if "development-plan" not in (target_fm.get("inputs") or []): - report.error(path, "R33", f"guidance-skill must accept development-plan: {normalized}", 1) - - def run(root: Path) -> Report: report = Report() skill_records: list[SkillRecord] = [] @@ -781,11 +701,9 @@ def run(root: Path) -> Report: others = [q.relative_to(root).as_posix() for q in paths if q != p] report.error(p, "R24", f"skill id '{sid}' ({kind}) is not unique; also defined in: {others}") - # Fourth pass: cross-skill references + # Fourth pass: R26 sub-skills registry matches leaf files on disk for path, fm in action_skill_fms: validate_sub_skills_registry(path, fm, root, report) - validate_quality_skill(path, fm, root, report) - validate_guidance_skill(path, fm, root, report) return report diff --git a/.github/workflows/development-coverage.yml b/.github/workflows/development-guidance.yml similarity index 52% rename from .github/workflows/development-coverage.yml rename to .github/workflows/development-guidance.yml index 73383a16..51e24b3f 100644 --- a/.github/workflows/development-coverage.yml +++ b/.github/workflows/development-guidance.yml @@ -1,4 +1,4 @@ -name: Validate development coverage +name: Validate read-only development guidance on: pull_request: @@ -7,20 +7,19 @@ on: branches: [main] jobs: - validate-development-coverage: - runs-on: ubuntu-latest + validate-development-guidance: + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Validate Microsoft Learn coverage ledger - shell: pwsh - run: ./tools/Test-LearnCoverage.ps1 -Root . - - - name: Validate and prepare development fixtures - shell: pwsh - run: ./tools/Test-DevelopmentFixtures.ps1 -Root . -PrepareDirectory "$env:RUNNER_TEMP/bcquality-development-fixtures" - - name: Validate and prepare development-guidance fixtures shell: pwsh run: ./tools/Test-DevelopmentGuidanceFixtures.ps1 -Root . -PrepareDirectory "$env:RUNNER_TEMP/bcquality-development-guidance-fixtures" + + - name: Run credential-free guidance evaluator regressions + shell: pwsh + run: ./tools/Test-DevelopmentGuidanceEvaluator.ps1 diff --git a/README.md b/README.md index 6d5d8154..c1a183e5 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Skills define how agents consume knowledge. They come in three flavors: READ and DO are read on demand — typically when the first dispatched action skill runs. They are not prerequisites for invoking Entry. WRITE is only used when scaffolding new content. -- **Action skills** — concrete skills that follow the Action Skill template to do real work. Review skills emit findings reports; read-only planning skills emit development-guidance reports; implementation skills emit implementation reports. Action skills live inside the layers that own them (`/microsoft/skills/`, `/community/skills/`, `/custom/skills/`). [`microsoft/skills/development/al-development-plan.md`](microsoft/skills/development/al-development-plan.md) turns an existing plan into knowledge constraints, [`microsoft/skills/development/al-development.md`](microsoft/skills/development/al-development.md) consumes those constraints while implementing features, bugs, refactors, upgrades, and maintenance, and [`microsoft/skills/review/al-code-review.md`](microsoft/skills/review/al-code-review.md) provides the final quality gate. +- **Action skills** — concrete skills that follow the Action Skill template. Review skills emit findings reports; read-only plan enrichment emits development-guidance reports. Action skills live inside the layers that own them (`/microsoft/skills/`, `/community/skills/`, `/custom/skills/`). [`microsoft/skills/development/al-development-plan.md`](microsoft/skills/development/al-development-plan.md) turns an existing plan into additional knowledge constraints for the consumer's established workflow. [`microsoft/skills/review/al-code-review.md`](microsoft/skills/review/al-code-review.md) independently reviews the resulting changes. ### Agent bootstrapping @@ -66,7 +66,7 @@ An orchestrator (such as AL-Go) points the agent at BCQuality's URL and provides BCQuality can also be installed directly as a plugin. The plugin registers host-native adapters for [`al-code-review`](skills/al-code-review/SKILL.md) and -[`al-development`](skills/al-development/SKILL.md). Both adapt +[`al-development-plan`](skills/al-development-plan/SKILL.md). Both adapt the caller's request to the same Entry protocol used by orchestrators. For GitHub Copilot CLI: @@ -81,8 +81,8 @@ must be updated. The name remains distinct from BC-ALAgents' public `al-review` skill because current hosts may load plugin skill names into one shared inventory. -Plugin version `0.3.0` adds `al-development`, the knowledge-backed -implementation adapter for features, bugs, refactors, upgrades, and maintenance. +Plugin version `0.3.0` adds `al-development-plan`, a read-only adapter for +enriching an **existing** plan. It does not generate a plan or implement code. The adapters are intentionally not second implementations: @@ -92,10 +92,10 @@ standalone host skill: skills/al-code-review/SKILL.md -> review coordinator: microsoft/skills/review/al-code-review.md -> domain review leaves -standalone host skill: skills/al-development/SKILL.md +standalone host skill: skills/al-development-plan/SKILL.md -> routing contract: skills/entry.md - -> implementation skill: microsoft/skills/development/al-development.md - -> knowledge-guided implementation + AL review quality gate + -> enrichment skill: microsoft/skills/development/al-development-plan.md + -> referenced constraints for the consumer's existing workflow (read-only) ``` Only the files under `skills/*/SKILL.md` follow the host's packaging format. @@ -143,33 +143,31 @@ Code examples belong in separate files, not in the knowledge file itself. Knowle ## Scope -The current curated corpus covers technical AL concerns across Agents, AppSource and compatibility, data modeling, error handling, events, interfaces, performance, privacy, Query objects, security, style, telemetry, testing, UI, upgrade, and web services. Review skills evaluate existing changes against those domains. The `al-development` skill applies them before and during implementation, then runs the review coordinator as a final gate. +The current curated corpus covers technical AL concerns across Agents, AppSource and compatibility, data modeling, error handling, events, interfaces, performance, privacy, Query objects, security, style, telemetry, testing, UI, upgrade, and web services. Review skills evaluate existing changes against those domains. The read-only `al-development-plan` interface selects relevant constraints before the consumer implements its own plan. -Repository-specific orchestrators do not need to delegate implementation to -`al-development`. They can invoke `al-development-plan` with their existing -plan, feed its read-only guidance report into their own phases, and retain their -specialized environment, test, propagation, and delivery gates. +Repository-specific orchestrators retain planning, implementation, approvals, +tests, environment, propagation, and delivery ownership. The intended flow is +consumer analysis and normalized plan -> read-only BCQuality guidance -> +existing implementation phases -> independent final BCQuality review -> +delivery. Consumer uptake and a real runtime pilot are follow-up work, not +implemented integrations or demonstrated authoring improvements. -`al-development` does not silently fall back to generic generation when no -article applies. It returns `no-knowledge` without changing code, making corpus -coverage visible; callers can use their normal repository workflow or -contribute the missing Business Central-specific guidance. +`no-knowledge` means no additional applicable BCQuality constraints, with empty +`knowledge`; it does not make a plan unsafe or prevent the consumer from using +its ordinary gates. Retrieval failures and materially unresolved conditional +guidance are distinct outcomes, not empty knowledge. Do not add generic advice +just to avoid a `no-knowledge` result. Business Central functional domains (Finance, Supply Chain Management, Manufacturing, Jobs, Warehousing, Service), PowerShell, pipelines, and Power Platform remain valid future repository scope, but they are **not current coverage claims** until corresponding knowledge and action skills exist. Consumers should derive supported review scope from the live knowledge index and dispatched skills, not from roadmap breadth. -## Tracking developer coverage +## Evidence and follow-up scope -BCQuality tracks source ingestion and implementation capability separately: - -- [`coverage/microsoft-learn-developer-catalog.json`](coverage/microsoft-learn-developer-catalog.json) is the generated inventory of Business Central developer training. -- [`coverage/learn-coverage.json`](coverage/learn-coverage.json) records editorial progress and the disposition of each extracted concern. -- [`coverage/development-capabilities.json`](coverage/development-capabilities.json) tracks representative development capabilities and their evaluation fixtures. - -Run `pwsh ./tools/Test-LearnCoverage.ps1` for current source progress and -`pwsh ./tools/Test-DevelopmentFixtures.ps1` for capability coverage. Article -count alone is not a completion metric: a capability becomes `validated` only -after its generated implementation passes compilation, tests, and the review -quality gate. +The [guidance evaluation](evaluation/README.md#read-only-plan-guidance) separates +credential-free contract/scorer regressions from external agent and runtime +evidence. Prepared requests and fixture counts do not establish compilation, +test execution, better repairs, or a capability percentage. Consumer adoption, +a pinned baseline comparison and runtime pilot, standalone authoring, and +source-ingestion catalog work remain separate follow-ups. ## How agents consume BCQuality @@ -180,7 +178,7 @@ Action skills follow a four-step pattern: 3. **Worklist** — narrow from N candidates to the M that apply to the current task 4. **Action** — apply the relevant knowledge and produce structured output -Every action skill declares one structured JSON output. Review skills emit a `findings-report`; planning skills emit a read-only `development-guidance-report`; implementation skills emit an `implementation-report` containing the plan, knowledge used, changed files, real validation results, and post-implementation review. All contracts are defined in the Action Skill meta-skill so orchestrators and action skills remain independently evolvable. +Every action skill declares one structured JSON output. Review skills emit a `findings-report`; plan enrichment emits a read-only `development-guidance-report`. Both contracts are defined in the Action Skill meta-skill so orchestrators and action skills remain independently evolvable. BCQuality is an **additive** knowledge layer: it augments the agent's review judgement, it does not replace it. Super-skills (such as `al-code-review`) run a self-review pass alongside their sub-skills and surface concerns the agent identified on its own, marked with `from-sub-skill: "agent"` and an empty `references: []` so consumers can render them distinctly from knowledge-backed findings. See [agent-consumption.md](agent-consumption.md) and [`skills/do.md`](skills/do.md) for the full contract. @@ -192,8 +190,7 @@ For the end-to-end flow — from orchestrator trigger through to how output reac ``` ├── /skills/ # Global: entry-point skill + meta-skill contracts (READ, DO, WRITE) -├── /coverage/ # Source-ingestion ledger and development capability matrix -├── /evaluation/ # Review and development evaluation fixtures +├── /evaluation/ # Review and read-only guidance evaluation fixtures ├── /.github/ # Actions and workflows ├── /microsoft/ # Microsoft-endorsed layer │ ├── /knowledge/ # Knowledge files by domain diff --git a/agent-consumption.md b/agent-consumption.md index 62f6e714..fc8af978 100644 --- a/agent-consumption.md +++ b/agent-consumption.md @@ -14,7 +14,7 @@ For the high-level framing and repo structure, start with the [README](README.md When BCQuality is installed as a standalone plugin, it additionally exposes `skills/al-code-review/SKILL.md` and -`skills/al-development/SKILL.md`. These are host-format adapters, not +`skills/al-development-plan/SKILL.md`. These are host-format adapters, not additional action skills: each creates the task context and enters the same flow at Entry. @@ -27,7 +27,7 @@ flowchart LR E -->|3 dispatch record| A A -->|4 invoke dispatched skill| S[Action skill
e.g. al-code-review] S -->|5 execute| P[Source → Relevance
→ Worklist → Action
reading READ · DO on demand] - P -->|6 emit| R[Findings report
or implementation report] + P -->|6 emit| R[Findings report
or read-only guidance report] R -->|7 integrate| O ``` @@ -40,14 +40,14 @@ The agent reads `/skills/entry.md` and runs it against the task context. Entry a For a standalone plugin installation, the host activates the matching adapter first. The adapter preserves the caller's actual goal, constructs the task context, and invokes Entry. It does not select the internal review or -development action skill itself or duplicate Entry's preparation, routing, and +plan-enrichment action skill itself or duplicate Entry's preparation, routing, and failure semantics. ### 3. Agent consumes the dispatch record The dispatch record names one or more action skills, the subset of inputs each -should receive, and each skill's output kind. The output kind identifies -read-only review or planning work versus repository-changing implementation -before invocation. If the outcome is `no-match` or `failed`, the agent returns +should receive, and each skill's output kind. The output kind distinguishes +findings from read-only plan guidance before invocation; it is not proof of +runtime side effects. If the outcome is `no-match` or `failed`, the agent returns the record to the orchestrator unchanged. ### 4. Agent invokes each dispatched action skill @@ -81,40 +81,78 @@ The output contracts are defined in the DO meta-skill: - A **findings report** carries review findings, domain labels, references, confidence, and suppressions. - A **development guidance report** carries read-only knowledge constraints and validation considerations for an existing plan. -- An **implementation report** carries the development plan, classified mode, knowledge applied, changed files, validation results, final review, and remaining work. The orchestrator parses this **without skill-specific logic**. This is the point of the contract: orchestrators and action skills evolve independently. -For development, the action happens before the report: the skill -first invokes the read-only planning skill to select applicable knowledge, then -changes the target repository, runs its native validation, and invokes the -configured review quality-skill over the resulting diff. A specialized -repository orchestrator may invoke only the planning skill and feed its -guidance report into its own implementation phases. The implementation report -is a machine-readable record of persisted work, not a code proposal for the -orchestrator to apply later. +For plan enrichment, the skill reads the existing plan and target repository, +selects applicable knowledge, and returns constraints without changing the +target. It does not generate a replacement plan, run tests, implement code, or +drive a review/fix loop. Implementation stays in the consuming workflow. ### 7. Orchestrator integrates -The orchestrator turns findings into PR comments, build gates, or IDE diagnostics. For implementation it presents the changed files and validation state, while the agent has already persisted the requested change in the target repository. +The orchestrator turns findings into PR comments, build gates, or IDE diagnostics. It can feed read-only guidance into its own implementation phases, preserving all existing approvals and delivery gates. ## Repository-specific development orchestrators -A repository-specific workflow can keep ownership of implementation and consume -BCQuality only for planning and review: - -1. Produce its normal development plan after repository investigation. -2. Invoke Entry with `inputs-available: [development-plan, repository]` plus - the resolved applicability dimensions. -3. Execute the dispatched `al-development-plan` skill and preserve its - `development-guidance-report`. -4. Pass the selected article references, constraints, samples, and validation - considerations into its own test, implementation, and critique phases. -5. Run its existing BCQuality-backed review gate over the completed diff. - -This is the integration model for specialized bug-fix or release workflows. -They keep environment provisioning, retries, state, commits, propagation, and -pull-request delivery; BCQuality supplies shared product knowledge before and -after the code change. +A repository-specific workflow can consume this read-only foundation before +authoring while retaining its independent final review. This is the intended +integration boundary, not a shipped consumer integration: + +1. Investigate and produce the consumer's normal initial plan. Normalize any + consumer-specific format outside BCQuality. A full serialized plan document + containing metadata plus a markdown body (root cause or design intent, + proposed changes, affected files, test strategy, acceptance criteria) is a + valid boundary. A continuation/checkpoint payload is not a substitute for + initial-plan coverage; workflow identifiers and state stay with the consumer. +2. Resolve and record an immutable BCQuality checkout and filtering policy. + Invoke Entry with a read-only enrichment goal, the existing + `development-plan`, `repository`, and established applicability dimensions. + Keep index, guidance, and runner artifacts outside the target repository. +3. Execute the dispatched `al-development-plan` skill. Persist the unchanged + report and provenance **after** any consumer state initialization or cleanup + that could erase them. BCQuality does not own the state directory or lifecycle. +4. Inject relevant constraints and validation considerations into the existing + Baseline, Implement, propagation (such as MiApp), and Critique phases, or + equivalents. Re-enrich on material plan or applicability changes; preserve + the relationship between plan version, guidance, and implementation attempt. +5. Run an independent final BCQuality review against the completed diff using + the **same recorded immutable checkout** used for enrichment. Review the + actual changes, not the guidance report as proof of correctness, then apply + the consumer's ordinary delivery gates. + +The consumer owns analysis, normalization, persistence, per-phase injection, +approvals, TDD and runtime execution, propagation, retries, commits, and PR +delivery. BCQuality supplies additional referenced product knowledge, not a +replacement orchestrator. + +### Outcomes are additive, not a universal coding gate + +`no-knowledge` with empty `knowledge` means no additional applicable BCQuality +constraints. The consumer may proceed under its ordinary gates. It must not be +conflated with failed retrieval/reference integrity (`failed`), incomplete +evaluation or materially unresolved conditional guidance (`partial`), or absent +required inputs (`not-applicable`). Consumers own the policy for handling those +outcomes and recorded unknowns: seek missing context, re-enrich, escalate, or +apply their existing risk controls without relabeling the report as successful. +Do not fill gaps with generic articles simply to unlock implementation. + +### Pinning and pilot evidence + +A configured tag or ref alone does not establish runtime pinning. Record the +resolved commit and actual checkout/content identity used at invocation, along +with enabled layers, pruning policy, index identity, plan version, and runner +provenance. Verify that identity at both enrichment and final review; fetching +default HEAD into an explicitly supplied checkout can bypass a configured ref. +Use an isolated checkout that cannot drift during the run. + +Consumer rollout and an external pilot remain follow-up work. A pilot must +compare a pinned independent baseline run of the existing workflow without +enrichment against a matched enriched run, keeping starting code, task, model, +tools, runtime, and gates controlled and recording the actual BCQuality +checkout. Retain external logs, diffs, test/compile outcomes, and independent +final reviews, including failures and unresolved results. Credential-free +fixture preparation and scorer regressions do not demonstrate improved repair +quality, compilation, runtime success, or production integration. ## Knowledge-backed and agent findings diff --git a/coverage/README.md b/coverage/README.md deleted file mode 100644 index 200fd8aa..00000000 --- a/coverage/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Developer knowledge coverage - -This directory separates **source coverage** from the knowledge corpus itself. -Microsoft Learn units are inputs to editorial work, not articles to import -one-for-one. - -## Files - -- `microsoft-learn-developer-catalog.json` is generated source metadata for all - Microsoft Learn modules tagged with both `dynamics-business-central` and - `developer`. -- `learn-coverage.json` is the maintained editorial ledger. Units absent from - this file are unreviewed. -- `development-capabilities.json` tracks whether representative Business - Central development capabilities have implementation fixtures. - -The capability manifest declares `minimumFixtureCoverage`. CI fails when the -share of `fixture` or `validated` capabilities falls below that floor, so a -new planned capability cannot silently dilute generation coverage. - -Each tracked unit has a `reviewStatus`: - -- `in-progress` — at least one concern has been identified, but editorial - triage of the unit is not complete. -- `complete` — every relevant concern in the unit has a recorded outcome. A - complete unit may have no outcomes when it contains no remedial knowledge. - -Each concern has one disposition: - -- `candidate` — worth authoring or reconciling with existing knowledge. -- `authored` — produced one or more new knowledge articles. -- `covered-existing` — already represented by the linked article. -- `rejected` — fails BCQuality's remedial admission test. -- `deferred` — valid but intentionally postponed, with a rationale. - -An authored article does not make its source unit complete automatically. One -unit can contain several independent concerns. - -## Update and report - -```powershell -pwsh ./tools/Update-LearnCatalog.ps1 -pwsh ./tools/Test-LearnCoverage.ps1 -``` - -The catalog updater also accepts `-CatalogPath` for an exported Microsoft Learn -Platform API response. CI validates the committed snapshot and editorial ledger -without network access. diff --git a/coverage/development-capabilities.json b/coverage/development-capabilities.json deleted file mode 100644 index 1ad41132..00000000 --- a/coverage/development-capabilities.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "version": 1, - "minimumFixtureCoverage": 0.5, - "capabilities": [ - { - "id": "setup-and-master-data", - "title": "Setup and master data", - "status": "fixture", - "domains": [ - "data-modeling", - "security", - "testing", - "ui" - ], - "fixtureIds": [ - "setup-backed-master-data" - ] - }, - { - "id": "document-workflows", - "title": "Document header and lines workflows", - "status": "fixture", - "domains": [ - "data-modeling", - "events", - "performance", - "testing", - "ui" - ], - "fixtureIds": [ - "document-header-and-lines" - ] - }, - { - "id": "api-integrations", - "title": "Versioned API integrations", - "status": "fixture", - "domains": [ - "security", - "testing", - "web-services" - ], - "fixtureIds": [ - "versioned-master-data-api" - ] - }, - { - "id": "bug-diagnosis-and-fix", - "title": "Bug diagnosis and surgical repair", - "status": "fixture", - "domains": [ - "performance", - "testing" - ], - "fixtureIds": [ - "fix-filtered-batch-processing" - ] - }, - { - "id": "journals-and-posting", - "title": "Journals and posting routines", - "status": "planned", - "domains": [ - "data-modeling", - "error-handling", - "events", - "performance", - "testing" - ], - "fixtureIds": [] - }, - { - "id": "reports-and-documents", - "title": "Reports and document layouts", - "status": "planned", - "domains": [ - "performance", - "testing", - "ui" - ], - "fixtureIds": [] - }, - { - "id": "install-and-upgrade", - "title": "Installation and data upgrade", - "status": "fixture", - "domains": [ - "breaking-changes", - "testing", - "upgrade" - ], - "fixtureIds": [ - "versioned-data-upgrade" - ] - }, - { - "id": "external-services", - "title": "Outbound services and authentication", - "status": "planned", - "domains": [ - "error-handling", - "privacy", - "security", - "telemetry" - ], - "fixtureIds": [] - }, - { - "id": "role-centers-and-onboarding", - "title": "Role Centers, setup, and onboarding", - "status": "planned", - "domains": [ - "security", - "testing", - "ui" - ], - "fixtureIds": [] - }, - { - "id": "appsource-lifecycle", - "title": "AppSource packaging and lifecycle", - "status": "planned", - "domains": [ - "appsource", - "breaking-changes", - "testing", - "upgrade" - ], - "fixtureIds": [] - } - ] -} diff --git a/coverage/learn-coverage.json b/coverage/learn-coverage.json deleted file mode 100644 index 53934d6d..00000000 --- a/coverage/learn-coverage.json +++ /dev/null @@ -1,456 +0,0 @@ -{ - "version": 1, - "catalog": "coverage/microsoft-learn-developer-catalog.json", - "units": [ - { - "uid": "learn-dynamics.use-document-standards-business-central.4a-use-round-function", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "round-direction-symbols-use-magnitude", - "title": "Round direction symbols use magnitude rather than mathematical ordering", - "disposition": "authored", - "domain": "data-modeling", - "articlePaths": [ - "microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md" - ] - } - ] - }, - { - "uid": "learn-dynamics.use-document-standards-business-central.3-use-initrecord-function", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "initialize-document-defaults-in-initrecord", - "title": "Initialize document defaults in InitRecord after assigning the number", - "disposition": "authored", - "domain": "data-modeling", - "articlePaths": [ - "microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md" - ] - } - ] - }, - { - "uid": "learn-dynamics.business-central-interfaces.type-testing", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "guard-interface-casts-with-is", - "title": "Guard optional interface casts with is", - "disposition": "authored", - "domain": "interfaces", - "articlePaths": [ - "microsoft/knowledge/interfaces/guard-interface-casts-with-is.md" - ] - } - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-table.add-field-group", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "dropdown-fieldgroup-respects-lookup-page-visibility", - "title": "DropDown fields remain hidden when their lookup-page controls are hidden", - "disposition": "authored", - "domain": "ui", - "articlePaths": [ - "microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md" - ] - } - ] - }, - { - "uid": "learn-dynamics.work-with-pages.8-controls", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "updatepropagation-both-refreshes-main-page", - "title": "UpdatePropagation Both refreshes the main page after line edits", - "disposition": "authored", - "domain": "ui", - "articlePaths": [ - "microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md" - ] - }, - { - "id": "applicationarea-parent-inheritance", - "title": "Page-level ApplicationArea inheritance excludes extension controls", - "disposition": "covered-existing", - "domain": "style", - "articlePaths": [ - "microsoft/knowledge/style/applicationarea-required-on-page-controls.md" - ] - } - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.3-installation-upgrade-codeunits", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "install-and-upgrade-codeunits-have-no-order", - "title": "Separate install or upgrade codeunits have no execution order", - "disposition": "authored", - "domain": "upgrade", - "articlePaths": [ - "microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md" - ] - }, - { - "id": "appversion-meaning-depends-on-execution-context", - "title": "ModuleInfo AppVersion changes meaning with execution context", - "disposition": "authored", - "domain": "upgrade", - "articlePaths": [ - "microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md" - ] - } - ] - }, - { - "uid": "learn-dynamics.work-with-tables.text-search", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "optimize-for-text-search-requires-double-ampersand-filter", - "title": "Optimized full-text search requires the double-ampersand filter operator", - "disposition": "candidate", - "domain": "query" - } - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-table.define-extension-objects", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "same-app-extension-references-follow-object-id-order", - "title": "Same-app extension objects can only reference lower-ID extension objects", - "disposition": "candidate", - "domain": "data-modeling" - } - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.resource-policy-settings", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "resource-exposure-policy-is-not-nondebuggable", - "title": "Resource exposure policy and NonDebuggable protect different surfaces", - "disposition": "candidate", - "domain": "security" - } - ] - }, - { - "uid": "learn-dynamics.work-entitlements-permission-sets.3-create-entitlements-permission-sets", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "entitlement-objects-reference-same-app-permission-sets", - "title": "Entitlement objects are online-only and reference same-app permission sets", - "disposition": "candidate", - "domain": "security" - } - ] - }, - { - "uid": "learn-dynamics.work-entitlements-permission-sets.override-entitlements", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "inherent-permissions-cannot-elevate-other-extensions", - "title": "Inherent permissions cannot elevate access to another extension", - "disposition": "candidate", - "domain": "security" - } - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.3a-snapshot", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "snapshot-debugging-captures-only-snappoints-and-exceptions", - "title": "Snapshot debugging captures state only at snappoints and exceptions", - "disposition": "candidate", - "domain": "testing" - } - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.recovery-failures", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "publish-recovery-cannot-restore-every-extension-state", - "title": "Publish recovery cannot restore every upgrade or app-move failure", - "disposition": "candidate", - "domain": "upgrade" - } - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.database-wait-statistics", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "database-wait-statistics-are-exposed-as-a-virtual-table", - "title": "Database wait statistics are exposed as a Business Central virtual table", - "disposition": "candidate", - "domain": "performance" - } - ] - }, - { - "uid": "learn-dynamics.work-with-pages.use-rich-text-editor", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "rich-text-controls-use-html-backed-blob-fields", - "title": "Rich text controls use HTML-backed Blob fields and isolated layout groups", - "disposition": "candidate", - "domain": "ui" - } - ] - }, - { - "uid": "learn-dynamics.work-with-pages.scan-barcodes", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "barcode-integration-scenarios-have-different-platform-support", - "title": "Barcode field, camera action, and hardware-scanner scenarios have different platform support", - "disposition": "candidate", - "domain": "ui" - } - ] - }, - { - "uid": "learn-dynamics.work-with-pages.9-search", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "card-pages-should-not-be-directly-searchable", - "title": "Card pages should be opened through their list rather than Tell Me", - "disposition": "candidate", - "domain": "ui" - } - ] - }, - { - "uid": "learn-dynamics.work-with-pages.hidden-fields", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "allowincustomizations-controls-the-add-field-pane", - "title": "AllowInCustomizations controls exposure through the Add field pane", - "disposition": "candidate", - "domain": "privacy" - } - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.differentiate-apps", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "application-modules-depend-only-on-same-or-lower-layers", - "title": "Application modules depend only on the same or lower architectural layers", - "disposition": "candidate", - "domain": "data-modeling" - } - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code.2-retrieve-data", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "legacy-find-directions-do-not-use-top-one", - "title": "Legacy Find direction calls do not have FindFirst or FindLast query behavior", - "disposition": "candidate", - "domain": "performance" - } - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.2-upgrade-responsibilities", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "uninstall-preserves-extension-data", - "title": "Uninstall preserves extension data for reinstall or upgrade", - "disposition": "candidate", - "domain": "upgrade" - } - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.consider-update-lifecycle", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "incompatible-cloud-extensions-have-a-remediation-window", - "title": "Incompatible cloud extensions have a fixed remediation window", - "disposition": "candidate", - "domain": "upgrade" - } - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.6-answers-about-updates", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "minor-and-major-releases-update-apps-differently", - "title": "Minor and major Business Central releases update AppSource apps differently", - "disposition": "candidate", - "domain": "upgrade" - }, - { - "id": "dependency-version-is-a-minimum", - "title": "An app.json dependency version is a minimum rather than an exact pin", - "disposition": "candidate", - "domain": "upgrade" - } - ] - }, - { - "uid": "learn-dynamics.application-types.3-library-dependency-applications", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "library-apps-install-through-the-dependency-chain", - "title": "Library apps install and update through the dependency chain", - "disposition": "candidate", - "domain": "appsource" - } - ] - }, - { - "uid": "learn-dynamics.business-central-interfaces.extending-interfaces", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "interfaces-can-compose-multiple-base-interfaces", - "title": "Interfaces can compose multiple base interfaces", - "disposition": "candidate", - "domain": "interfaces" - } - ] - }, - { - "uid": "learn-dynamics.test-automation.page-scripting-tool", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "page-scripting-cannot-drive-non-al-ui", - "title": "Page scripting cannot drive control add-ins or other non-AL UI", - "disposition": "candidate", - "domain": "testing" - } - ] - }, - { - "uid": "learn-dynamics.test-automation.2-test-automation-responsibilities", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "appsource-validation-must-use-the-current-build", - "title": "AppSource validation must use the current validation build", - "disposition": "candidate", - "domain": "appsource" - }, - { - "id": "permission-testing-also-protects-the-essential-experience", - "title": "Permission testing must also protect the unextended Essential experience", - "disposition": "candidate", - "domain": "testing" - } - ] - }, - { - "uid": "learn-dynamics.test-automation.4-answers-about-testing", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "appsource-tests-run-for-every-supported-country", - "title": "AppSource tests must run separately for every supported country", - "disposition": "candidate", - "domain": "appsource" - }, - { - "id": "upgrade-tests-cover-nonadjacent-versions", - "title": "Upgrade tests cover nonadjacent historical versions", - "disposition": "candidate", - "domain": "testing" - } - ] - }, - { - "uid": "learn-dynamics.bring-app-appsource.4-technical-validation", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "appsource-submissions-cannot-use-runtime-packages", - "title": "AppSource submissions cannot use runtime packages", - "disposition": "candidate", - "domain": "appsource" - }, - { - "id": "per-tenant-and-marketplace-apps-need-distinct-identities", - "title": "Per-tenant and Marketplace variants need distinct app identities", - "disposition": "candidate", - "domain": "appsource" - }, - { - "id": "profiles-are-declared-with-profile-objects", - "title": "Profiles are shipped with AL profile objects rather than table inserts", - "disposition": "candidate", - "domain": "appsource" - }, - { - "id": "extension-layout-uses-relative-placement", - "title": "Extension layout uses named relative placement anchors", - "disposition": "candidate", - "domain": "breaking-changes" - } - ] - }, - { - "uid": "learn-dynamics.test-automation.3-documentation-examples", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "tests-use-reproducible-random-data", - "title": "Tests use reproducible randomized data instead of shared literals", - "disposition": "candidate", - "domain": "testing" - }, - { - "id": "al-tests-run-through-vscode-test-explorer", - "title": "AL tests can run and debug through Visual Studio Code Test Explorer", - "disposition": "candidate", - "domain": "testing" - } - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.4-manage-apps", - "reviewStatus": "in-progress", - "outcomes": [ - { - "id": "manage-apps-separates-install-and-update-requirements", - "title": "Manage Apps separates missing dependencies from dependency updates", - "disposition": "candidate", - "domain": "upgrade" - } - ] - } - ] -} diff --git a/coverage/microsoft-learn-developer-catalog.json b/coverage/microsoft-learn-developer-catalog.json deleted file mode 100644 index 68fe8595..00000000 --- a/coverage/microsoft-learn-developer-catalog.json +++ /dev/null @@ -1,8282 +0,0 @@ -{ - "version": 1, - "source": { - "provider": "Microsoft Learn", - "catalogUri": "https://learn.microsoft.com/api/catalog/?locale=en-us", - "locale": "en-us", - "filters": { - "product": "dynamics-business-central", - "role": "developer" - }, - "catalogLastModified": "2026-09-03T23:14:00Z" - }, - "counts": { - "learningPaths": 12, - "modules": 74, - "units": 623 - }, - "learningPaths": [ - { - "uid": "learn-dynamics.application-foundation-al-language", - "title": "Learn the application foundation with the AL programming language for Microsoft Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/application-foundation-al-language", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 573, - "moduleUids": [ - "learn-dynamics.al-built-in-functions", - "learn-dynamics.al-statements", - "learn-dynamics.create-custom-functions", - "learn-dynamics.events-triggers", - "learn-dynamics.handle-errors", - "learn-dynamics.interfaces", - "learn-dynamics.intro-basics-al-programming", - "learn-dynamics.manage-multilanguage-development", - "learn-dynamics.manipulate-data-via-code", - "learn-dynamics.work-with-codeunits" - ] - }, - { - "uid": "learn-dynamics.build-reports", - "title": "Build reports for Microsoft Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/build-reports", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 482, - "moduleUids": [ - "learn-dynamics.add-layout-report", - "learn-dynamics.add-reports-tell-me", - "learn-dynamics.complete-report-request-page", - "learn-dynamics.design-data-model-report", - "learn-dynamics.implement-multilanguage-reports", - "learn-dynamics.introduction-report-components", - "learn-dynamics.report-extensibility", - "learn-dynamics.understand-report-design-process", - "learn-dynamics.understand-report-triggers-functions", - "learn-dynamics.work-with-reports" - ] - }, - { - "uid": "learn-dynamics.continuous-integration", - "title": "Continuous integration with Azure DevOps for Microsoft Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/continuous-integration", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 439, - "moduleUids": [ - "learn-dynamics.introduction-test-automation", - "learn-dynamics.use-application-lifecycle", - "learn-dynamics.use-azure-pipelines", - "learn-dynamics.use-branch-merge-git", - "learn-dynamics.use-docker-business-central", - "learn-dynamics.work-azure-boards", - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.data-management-foundation-business-central", - "title": "Learn about the data management foundation in Microsoft Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/data-management-foundation-business-central", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 205, - "moduleUids": [ - "learn-dynamics.introduction-basics-file-management", - "learn-dynamics.work-with-queries", - "learn-dynamics.work-with-xmlports" - ] - }, - { - "uid": "learn-dynamics.development-get-started-business-central", - "title": "Get started with development in Microsoft Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/development-get-started-business-central", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 262, - "moduleUids": [ - "learn-dynamics.add-new-objects", - "learn-dynamics.assisted-setup-tooltips", - "learn-dynamics.extend-functionality-using-events", - "learn-dynamics.extend-tables-page-objects", - "learn-dynamics.initialize-data-extension-installation", - "learn-dynamics.introduction-development-process" - ] - }, - { - "uid": "learn-dynamics.essential-development-standards", - "title": "Work with essential development standards for Microsoft Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/essential-development-standards", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 223, - "moduleUids": [ - "learn-dynamics.identify-table-types", - "learn-dynamics.implement-data-process-model-business-central", - "learn-dynamics.use-document-standards-business-central", - "learn-dynamics.work-with-master-data-business-central" - ] - }, - { - "uid": "learn-dynamics.foundation-customize-business-central", - "title": "Discover the foundation of customizing Microsoft Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/foundation-customize-business-central", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 608, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension", - "learn-dynamics.extend-modify-existing-pages", - "learn-dynamics.extend-modify-existing-table", - "learn-dynamics.intro-development-environment", - "learn-dynamics.work-entitlements-permission-sets", - "learn-dynamics.work-with-pages", - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.interface-with-business-central", - "title": "Interface with Microsoft Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/interface-with-business-central", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 283, - "moduleUids": [ - "learn-dynamics.access-rest-services", - "learn-dynamics.build-control-add-ins", - "learn-dynamics.use-al-controlled-barcode-scanning", - "learn-dynamics.use-azure-functions", - "learn-dynamics.work-with-api", - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.power-apps-power-automate-business-central", - "title": "Develop using Power Apps and Power Automate for Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/power-apps-power-automate-business-central", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 396, - "moduleUids": [ - "learn-dynamics.create-canvas-app", - "learn-dynamics.create-custom-connector-power-platform", - "learn-dynamics.introduction-power-platform-business-central", - "learn-dynamics.use-model-driven-apps-common-data-service", - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.tailor-roles-design-ui", - "title": "Tailor roles and design the UI for Microsoft Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/tailor-roles-design-ui", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 256, - "moduleUids": [ - "learn-dynamics.build-assisted-setup", - "learn-dynamics.customize-ui-experience", - "learn-dynamics.develop-role-center", - "learn-dynamics.send-notifications", - "learn-dynamics.set-up-onboarding-experiences" - ] - }, - { - "uid": "learn-dynamics.use-application-development-business-central", - "title": "Use application development best practices in Microsoft Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/use-application-development-business-central", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 377, - "moduleUids": [ - "learn-dynamics.application-monetization", - "learn-dynamics.application-performance-monitor", - "learn-dynamics.application-types", - "learn-dynamics.bring-app-appsource", - "learn-dynamics.easy-application-upgrade", - "learn-dynamics.implement-saas-techniques", - "learn-dynamics.test-automation" - ] - }, - { - "uid": "learn-dynamics.use-power-bi", - "title": "Use Power BI in Microsoft Dynamics 365 Business Central", - "url": "https://learn.microsoft.com/en-us/training/paths/use-power-bi", - "lastModified": "2025-03-24T22:55:00Z", - "durationInMinutes": 233, - "moduleUids": [ - "learn-dynamics.build-power-bi-report", - "learn-dynamics.create-data-set", - "learn-dynamics.work-power-bi" - ] - } - ], - "modules": [ - { - "uid": "learn-dynamics.access-rest-services", - "title": "Access REST services from within Dynamics 365 Business Central", - "summary": "Learn how to use HTTP data types, connect to external REST services, and handle JSON data in Dynamics 365 Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 62, - "levels": [ - "intermediate" - ], - "subjects": [ - "cross-development" - ], - "learningPathUids": "learn-dynamics.interface-with-business-central", - "unitUids": [ - "learn-dynamics.access-rest-services.1-introduction", - "learn-dynamics.access-rest-services.2-http-classes", - "learn-dynamics.access-rest-services.3-get-send-data", - "learn-dynamics.access-rest-services.4-use-read-json", - "learn-dynamics.access-rest-services.5-get-json-data", - "learn-dynamics.access-rest-services.6-exercise", - "learn-dynamics.access-rest-services.7-check", - "learn-dynamics.access-rest-services.8-summary" - ] - }, - { - "uid": "learn-dynamics.add-layout-report", - "title": "Add a layout to a report in Dynamics 365 Business Central", - "summary": "Do you want to learn how to add a layout to a report in Microsoft Dynamics 365 Business Central? In this module, you'll learn about the client report definition (RDLC) layout and Word layout formats and how to create them.", - "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 70, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.build-reports", - "unitUids": [ - "learn-dynamics.add-layout-report.1-introduction", - "learn-dynamics.add-layout-report.2-add-report-definition", - "learn-dynamics.add-layout-report.3-add-word-layout", - "learn-dynamics.add-layout-report.word-add-in", - "learn-dynamics.add-layout-report.add-excel-layout", - "learn-dynamics.add-layout-report.4-exercise", - "learn-dynamics.add-layout-report.5-check", - "learn-dynamics.add-layout-report.6-summary" - ] - }, - { - "uid": "learn-dynamics.add-new-objects", - "title": "Add new objects to Dynamics 365 Business Central", - "summary": "Do you want to know how to add objects, like tables and pages, to Microsoft Dynamics 365 Business Central? If so, this module is for you. In this module, you'll learn how to create tables and pages for an extension.", - "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 57, - "levels": [ - "beginner" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.development-get-started-business-central", - "unitUids": [ - "learn-dynamics.add-new-objects.1-introduction", - "learn-dynamics.add-new-objects.2-reward", - "learn-dynamics.add-new-objects.3-activation-code", - "learn-dynamics.add-new-objects.4-customer-rewards", - "learn-dynamics.add-new-objects.5-reward-list", - "learn-dynamics.add-new-objects.6-exercise", - "learn-dynamics.add-new-objects.7-exercise", - "learn-dynamics.add-new-objects.8-exercise", - "learn-dynamics.add-new-objects.9-check", - "learn-dynamics.add-new-objects.10-summary" - ] - }, - { - "uid": "learn-dynamics.add-reports-tell-me", - "title": "Add reports to Tell Me and substitute a report in Dynamics 365 Business Central", - "summary": "Do you want to learn how to add reports to Tell Me and substitute a report in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to add reports to Tell Me and how to substitute a report.", - "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 27, - "levels": [ - "intermediate" - ], - "subjects": [ - "finance-accounting" - ], - "learningPathUids": "learn-dynamics.build-reports", - "unitUids": [ - "learn-dynamics.add-reports-tell-me.1-introduction", - "learn-dynamics.add-reports-tell-me.2-add-reports-tell-me", - "learn-dynamics.add-reports-tell-me.3-substitute-report", - "learn-dynamics.add-reports-tell-me.4-check", - "learn-dynamics.add-reports-tell-me.5-summary" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions", - "title": "Work with AL built-in functions in Dynamics 365 Business Central", - "summary": "Do you want to know more about the built-in programming functions of Business Central? This module will discuss the different available functions that you can use when developing for Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 86, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.application-foundation-al-language", - "unitUids": [ - "learn-dynamics.al-built-in-functions.1-introduction", - "learn-dynamics.al-built-in-functions.2-interaction-functions", - "learn-dynamics.al-built-in-functions.3-string-functions", - "learn-dynamics.al-built-in-functions.4-date-functions", - "learn-dynamics.al-built-in-functions.5-numeric-functions", - "learn-dynamics.al-built-in-functions.6-array-functions", - "learn-dynamics.al-built-in-functions.7-list-functions", - "learn-dynamics.al-built-in-functions.8-system-functions", - "learn-dynamics.al-built-in-functions.9-variable-functions", - "learn-dynamics.al-built-in-functions.10-exercise", - "learn-dynamics.al-built-in-functions.11-check", - "learn-dynamics.al-built-in-functions.12-summary" - ] - }, - { - "uid": "learn-dynamics.al-statements", - "title": "Learn the AL statements in Dynamics 365 Business Central", - "summary": "Do you want to know how to use basic AL statements? This module shows you how to use conditional statements, compound statements, and comments when developing for Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/al-statements", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 67, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.application-foundation-al-language", - "unitUids": [ - "learn-dynamics.al-statements.1-introduction", - "learn-dynamics.al-statements.2-compound", - "learn-dynamics.al-statements.3-conditional", - "learn-dynamics.al-statements.4-repetitive", - "learn-dynamics.al-statements.5-exercise", - "learn-dynamics.al-statements.6-check", - "learn-dynamics.al-statements.7-summary" - ] - }, - { - "uid": "learn-dynamics.application-monetization", - "title": "Consider application monetization responsibilities in Business Central", - "summary": "Do you want to understand your application monetization responsibilities? In this module, you'll learn about monetization responsibilities, and where to find useful guides and documentation.", - "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 23, - "levels": [ - "intermediate" - ], - "subjects": [ - "marketing-sales" - ], - "learningPathUids": "learn-dynamics.use-application-development-business-central", - "unitUids": [ - "learn-dynamics.application-monetization.1-introduction", - "learn-dynamics.application-monetization.2-responsibilities", - "learn-dynamics.application-monetization.3-documentation-examples", - "learn-dynamics.application-monetization.4-check", - "learn-dynamics.application-monetization.5-summary" - ] - }, - { - "uid": "learn-dynamics.application-performance-monitor", - "title": "Learn about application performance and monitoring in Business Central", - "summary": "Do you want to learn about application performance and monitoring? Learn what is required to write performant code, understand how you can monitor application performance, and get required insights.", - "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 33, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.use-application-development-business-central", - "unitUids": [ - "learn-dynamics.application-performance-monitor.1-introduction", - "learn-dynamics.application-performance-monitor.2-prepare-application", - "learn-dynamics.application-performance-monitor.3-get-insights", - "learn-dynamics.application-performance-monitor.4-check", - "learn-dynamics.application-performance-monitor.5-summary" - ] - }, - { - "uid": "learn-dynamics.application-types", - "title": "Introduction to application types in Business Central", - "summary": "Explore the differences between connect and addon applications, understand library and dependency apps, and learn app development resources in Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/application-types", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 66, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.use-application-development-business-central", - "unitUids": [ - "learn-dynamics.application-types.1-introduction", - "learn-dynamics.application-types.2-connect-vs-add-on", - "learn-dynamics.application-types.3-library-dependency-applications", - "learn-dynamics.application-types.4-find-development-resources", - "learn-dynamics.application-types.5-set-up-code-cop-tools", - "learn-dynamics.application-types.6-check", - "learn-dynamics.application-types.7-summary" - ] - }, - { - "uid": "learn-dynamics.assisted-setup-tooltips", - "title": "Introduction to assisted setup and tooltips in Dynamics 365 Business Central", - "summary": "Do you want to know an effective way to let a user perform the installation steps of your extension by using an efficient and accessible wizard in Microsoft Dynamics 365 Business Central? If so, this module is for you. In this module, you'll learn how to add a wizard page by using a navigate page object.", - "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 47, - "levels": [ - "beginner" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.development-get-started-business-central", - "unitUids": [ - "learn-dynamics.assisted-setup-tooltips.1-introduction", - "learn-dynamics.assisted-setup-tooltips.2-rewards-wizard", - "learn-dynamics.assisted-setup-tooltips.3-assisted-setup", - "learn-dynamics.assisted-setup-tooltips.4-tooltips", - "learn-dynamics.assisted-setup-tooltips.5-exercise", - "learn-dynamics.assisted-setup-tooltips.6-exercise", - "learn-dynamics.assisted-setup-tooltips.7-check", - "learn-dynamics.assisted-setup-tooltips.8-summary" - ] - }, - { - "uid": "learn-dynamics.bring-app-appsource", - "title": "Understand the process of bringing an app to AppSource in Business Central", - "summary": "Do you want to know about technical and marketing AppSource validation procedures? In this module, you'll learn what is required from a technical and marketing point of view for your application to be validated for AppSource.", - "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 45, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.use-application-development-business-central", - "unitUids": [ - "learn-dynamics.bring-app-appsource.1-introduction", - "learn-dynamics.bring-app-appsource.2-get-accounts", - "learn-dynamics.bring-app-appsource.3-marketing-validation", - "learn-dynamics.bring-app-appsource.4-technical-validation", - "learn-dynamics.bring-app-appsource.5-check", - "learn-dynamics.bring-app-appsource.6-summary" - ] - }, - { - "uid": "learn-dynamics.build-assisted-setup", - "title": "Build an Assisted Setup in Dynamics 365 Business Central", - "summary": "Assisted Setup guides you in configuring Business Central. Do you want to learn how you can integrate with the Assisted Setup feature and create your own wizard pages? If so, this module explains that process.", - "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 54, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.tailor-roles-design-ui", - "unitUids": [ - "learn-dynamics.build-assisted-setup.1-introduction", - "learn-dynamics.build-assisted-setup.2-assisted-setup", - "learn-dynamics.build-assisted-setup.3-wizard-pages", - "learn-dynamics.build-assisted-setup.4-extensions", - "learn-dynamics.build-assisted-setup.5-exercise", - "learn-dynamics.build-assisted-setup.6-check", - "learn-dynamics.build-assisted-setup.7-summary" - ] - }, - { - "uid": "learn-dynamics.build-control-add-ins", - "title": "Build control add-in objects in Dynamics 365 Business Central", - "summary": "Do you need to build control add-ins? This module discusses how to build and integrate control add-ins with JavaScript in AL.", - "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 51, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.interface-with-business-central", - "unitUids": [ - "learn-dynamics.build-control-add-ins.1-introduction", - "learn-dynamics.build-control-add-ins.2-apply-look-feel", - "learn-dynamics.build-control-add-ins.3-create", - "learn-dynamics.build-control-add-ins.4-asynchronous-considerations", - "learn-dynamics.build-control-add-ins.5-connect", - "learn-dynamics.build-control-add-ins.6-exercise", - "learn-dynamics.build-control-add-ins.7-check", - "learn-dynamics.build-control-add-ins.8-summary" - ] - }, - { - "uid": "learn-dynamics.build-power-bi-report", - "title": "Build a Power BI report with Business Central data", - "summary": "Do you want to build a Business Central report using Power BI Desktop? This module will guide you through the steps required to create a Power BI report.", - "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 74, - "levels": [ - "intermediate" - ], - "subjects": [ - "cloud-computing", - "data-visualization" - ], - "learningPathUids": "learn-dynamics.use-power-bi", - "unitUids": [ - "learn-dynamics.build-power-bi-report.1-introduction", - "learn-dynamics.build-power-bi-report.2-build-data-model", - "learn-dynamics.build-power-bi-report.3-add-data-dimension", - "learn-dynamics.build-power-bi-report.4-introduction-dax", - "learn-dynamics.build-power-bi-report.5-create-report", - "learn-dynamics.build-power-bi-report.6-build-dashboard", - "learn-dynamics.build-power-bi-report.7-exercise", - "learn-dynamics.build-power-bi-report.8-exercise", - "learn-dynamics.build-power-bi-report.9-check", - "learn-dynamics.build-power-bi-report.10-summary" - ] - }, - { - "uid": "learn-dynamics.business-central-teams-integration", - "title": "Integrate Dynamics 365 Business Central with Teams", - "summary": "Learn to integrate Dynamics 365 Business Central with Microsoft Teams.", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 48, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": null, - "unitUids": [ - "learn-dynamics.business-central-teams-integration.introduction", - "learn-dynamics.business-central-teams-integration.manage-install-use", - "learn-dynamics.business-central-teams-integration.extend-teams-cards", - "learn-dynamics.business-central-teams-integration.create-tabs", - "learn-dynamics.business-central-teams-integration.get-session-details", - "learn-dynamics.business-central-teams-integration.check", - "learn-dynamics.business-central-teams-integration.summary" - ] - }, - { - "uid": "learn-dynamics.complete-report-request-page", - "title": "Complete the report request page in Dynamics 365 Business Central", - "summary": "Do you want to learn how to complete the report request page for a report in Business Central? In this module, you'll learn about the report request page and how to create it.", - "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 36, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.build-reports", - "unitUids": [ - "learn-dynamics.complete-report-request-page.1-introduction", - "learn-dynamics.complete-report-request-page.2-design-request-page", - "learn-dynamics.complete-report-request-page.3-design-request-options", - "learn-dynamics.complete-report-request-page.4-check", - "learn-dynamics.complete-report-request-page.5-summary" - ] - }, - { - "uid": "learn-dynamics.create-canvas-app", - "title": "Create a canvas app based on Business Central data", - "summary": "Do you want to create a canvas app using Business Central data? This module focuses on how to create canvas app using Business Central data and how to customize the created app.", - "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app", - "lastModified": "2026-08-03T17:47:00Z", - "durationInMinutes": 73, - "levels": [ - "intermediate" - ], - "subjects": [ - "cross-development" - ], - "learningPathUids": "learn-dynamics.power-apps-power-automate-business-central", - "unitUids": [ - "learn-dynamics.create-canvas-app.1-introduction", - "learn-dynamics.create-canvas-app.2-data-app", - "learn-dynamics.create-canvas-app.3-blank-app", - "learn-dynamics.create-canvas-app.4-customizations", - "learn-dynamics.create-canvas-app.5-save-publish-share", - "learn-dynamics.create-canvas-app.6-embed", - "learn-dynamics.create-canvas-app.7-exercise", - "learn-dynamics.create-canvas-app.8-check", - "learn-dynamics.create-canvas-app.9-summary" - ] - }, - { - "uid": "learn-dynamics.create-custom-connector-power-platform", - "title": "Create a custom connector to extend Microsoft Power Platform functionalities with Business Central", - "summary": "Do you need to create custom connectors for Business Central? Use this module to find out the details of creating custom connectors.", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform", - "lastModified": "2026-08-05T23:09:00Z", - "durationInMinutes": 59, - "levels": [ - "intermediate" - ], - "subjects": [ - "cross-development" - ], - "learningPathUids": "learn-dynamics.power-apps-power-automate-business-central", - "unitUids": [ - "learn-dynamics.create-custom-connector-power-platform.1-introduction", - "learn-dynamics.create-custom-connector-power-platform.2-custom-connector", - "learn-dynamics.create-custom-connector-power-platform.3-power-apps", - "learn-dynamics.create-custom-connector-power-platform.4-power-automate", - "learn-dynamics.create-custom-connector-power-platform.5-share-custom-connectors", - "learn-dynamics.create-custom-connector-power-platform.6-exercise", - "learn-dynamics.create-custom-connector-power-platform.7-check", - "learn-dynamics.create-custom-connector-power-platform.8-summary" - ] - }, - { - "uid": "learn-dynamics.create-custom-functions", - "title": "Create custom procedures in Dynamics 365 Business Central", - "summary": "Do you want to create custom procedures in AL? This module focuses on explaining how you can create new procedures. Additionally, it describes the difference between local and global procedures, the difference between local and global variables, and how you can pass variables to a procedure.", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 37, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.application-foundation-al-language", - "unitUids": [ - "learn-dynamics.create-custom-functions.1-introduction", - "learn-dynamics.create-custom-functions.2-custom-function", - "learn-dynamics.create-custom-functions.3-local-global-function", - "learn-dynamics.create-custom-functions.4-local-global-variable", - "learn-dynamics.create-custom-functions.5-pass", - "learn-dynamics.create-custom-functions.6-parameter", - "learn-dynamics.create-custom-functions.7-check", - "learn-dynamics.create-custom-functions.8-summary" - ] - }, - { - "uid": "learn-dynamics.create-data-set", - "title": "Create a dataset for a Power BI report in Dynamics 365 Business Central", - "summary": "Learn how to create a dataset for a Power BI report in Business Central using assisted reporting setup and custom data sets.", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 94, - "levels": [ - "intermediate" - ], - "subjects": [ - "data-analytics" - ], - "learningPathUids": "learn-dynamics.use-power-bi", - "unitUids": [ - "learn-dynamics.create-data-set.1-introduction", - "learn-dynamics.create-data-set.2-install", - "learn-dynamics.create-data-set.3-use-data-source", - "learn-dynamics.create-data-set.5-develop-query-page-objects", - "learn-dynamics.create-data-set.develop-api-query", - "learn-dynamics.create-data-set.use-dedicated-apis", - "learn-dynamics.create-data-set.6-mind-performance-impact", - "learn-dynamics.create-data-set.create-power-bi-dataflow", - "learn-dynamics.create-data-set.7-exercise", - "learn-dynamics.create-data-set.9-exercise", - "learn-dynamics.create-data-set.10-exercise", - "learn-dynamics.create-data-set.11-check", - "learn-dynamics.create-data-set.12-summary" - ] - }, - { - "uid": "learn-dynamics.customize-ui-experience", - "title": "Customize the UI experience in Dynamics 365 Business Central", - "summary": "Do you want to further customize the UI experience for a user? Do you want to be able to add your pages and reports to the search functionality? In this module, you'll learn how to create page customizations for specific user profiles and extend the application areas.", - "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 50, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.tailor-roles-design-ui", - "unitUids": [ - "learn-dynamics.customize-ui-experience.1-introduction", - "learn-dynamics.customize-ui-experience.2-objects", - "learn-dynamics.customize-ui-experience.3-customizations", - "learn-dynamics.customize-ui-experience.4-profiles", - "learn-dynamics.customize-ui-experience.extend-customize", - "learn-dynamics.customize-ui-experience.5-views", - "learn-dynamics.customize-ui-experience.6-application-areas", - "learn-dynamics.customize-ui-experience.7-check", - "learn-dynamics.customize-ui-experience.8-summary" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension", - "title": "Debug and deploy your extension in Dynamics 365 Business Central", - "summary": "Discover how to debug with Visual Studio Code and deploy your extension using RAD feature in Dynamics 365 Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 134, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.foundation-customize-business-central", - "unitUids": [ - "learn-dynamics.debug-deploy-extension.1-introduction", - "learn-dynamics.debug-deploy-extension.2-debug", - "learn-dynamics.debug-deploy-extension.recovery-failures", - "learn-dynamics.debug-deploy-extension.3-attach", - "learn-dynamics.debug-deploy-extension.3a-snapshot", - "learn-dynamics.debug-deploy-extension.4-rapid-application-development", - "learn-dynamics.debug-deploy-extension.5-publish", - "learn-dynamics.debug-deploy-extension.resource-policy-settings", - "learn-dynamics.debug-deploy-extension.protect-sensitive-values", - "learn-dynamics.debug-deploy-extension.projects-workspaces", - "learn-dynamics.debug-deploy-extension.code-actions", - "learn-dynamics.debug-deploy-extension.generate-help", - "learn-dynamics.debug-deploy-extension.5a-performance-toolkit", - "learn-dynamics.debug-deploy-extension.database-wait-statistics", - "learn-dynamics.debug-deploy-extension.6-exercise", - "learn-dynamics.debug-deploy-extension.7-exercise", - "learn-dynamics.debug-deploy-extension.8-check", - "learn-dynamics.debug-deploy-extension.9-summary" - ] - }, - { - "uid": "learn-dynamics.design-data-model-report", - "title": "Design the data model of a report in Dynamics 365 Business Central", - "summary": "Do you want to learn how to build the data model for a report in Microsoft Dynamics 365 Business Central? This module will discuss the different components of a report data model.", - "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 48, - "levels": [ - "intermediate" - ], - "subjects": [ - "data-modeling" - ], - "learningPathUids": "learn-dynamics.build-reports", - "unitUids": [ - "learn-dynamics.design-data-model-report.1-introduction", - "learn-dynamics.design-data-model-report.2-create-report-data-items", - "learn-dynamics.design-data-model-report.3-add-columns-report-dataset", - "learn-dynamics.design-data-model-report.4-link-order-indent-data-items", - "learn-dynamics.design-data-model-report.use-query", - "learn-dynamics.design-data-model-report.5-check", - "learn-dynamics.design-data-model-report.6-summary" - ] - }, - { - "uid": "learn-dynamics.develop-role-center", - "title": "Develop a Role Center in Dynamics 365 Business Central", - "summary": "Create a Role Center page for Microsoft Dynamics 365 Business Central and learn how it's constructed.", - "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 60, - "levels": [ - "intermediate" - ], - "subjects": [ - "finance-accounting" - ], - "learningPathUids": "learn-dynamics.tailor-roles-design-ui", - "unitUids": [ - "learn-dynamics.develop-role-center.1-introduction", - "learn-dynamics.develop-role-center.2-create-page", - "learn-dynamics.develop-role-center.3-activity-parts", - "learn-dynamics.develop-role-center.4-cue-setup", - "learn-dynamics.develop-role-center.5-role-center-lists", - "learn-dynamics.develop-role-center.6-headline-parts", - "learn-dynamics.develop-role-center.7-exercise", - "learn-dynamics.develop-role-center.8-check", - "learn-dynamics.develop-role-center.9-summary" - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade", - "title": "Prepare for an easy application upgrade experience in Business Central", - "summary": "Do you want to learn about how to build efficient installation and upgrade procedures? There are essential techniques you should learn to build good and user-friendly applications.", - "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 88, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications", - "data-management" - ], - "learningPathUids": "learn-dynamics.use-application-development-business-central", - "unitUids": [ - "learn-dynamics.easy-application-upgrade.1-introduction", - "learn-dynamics.easy-application-upgrade.2-upgrade-responsibilities", - "learn-dynamics.easy-application-upgrade.consider-update-lifecycle", - "learn-dynamics.easy-application-upgrade.3-installation-upgrade-codeunits", - "learn-dynamics.easy-application-upgrade.transfer-data", - "learn-dynamics.easy-application-upgrade.4-manage-apps", - "learn-dynamics.easy-application-upgrade.5-detect-breaking-changes", - "learn-dynamics.easy-application-upgrade.6-answers-about-updates", - "learn-dynamics.easy-application-upgrade.7-check", - "learn-dynamics.easy-application-upgrade.8-summary" - ] - }, - { - "uid": "learn-dynamics.events-triggers", - "title": "Use events and triggers in Dynamics 365 Business Central", - "summary": "Do you want to know how to access different table and page triggers? Do you also want to learn how to develop by using event-based architecture? If so, this module will explain how to use events and triggers in Microsoft Dynamics 365 Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 60, - "levels": [ - "intermediate" - ], - "subjects": [ - "architecture", - "data-visualization" - ], - "learningPathUids": "learn-dynamics.application-foundation-al-language", - "unitUids": [ - "learn-dynamics.events-triggers.1-introduction", - "learn-dynamics.events-triggers.2-table-field-triggers", - "learn-dynamics.events-triggers.3-page-control-action-triggers", - "learn-dynamics.events-triggers.4-event-based", - "learn-dynamics.events-triggers.5-publish-subscribe", - "learn-dynamics.events-triggers.6-exercise", - "learn-dynamics.events-triggers.7-check", - "learn-dynamics.events-triggers.8-summary" - ] - }, - { - "uid": "learn-dynamics.extend-functionality-using-events", - "title": "Extend functionality by using events in Dynamics 365 Business Central", - "summary": "Do you want to know how to add your business logic to existing functionality in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to add code and work with events.", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 44, - "levels": [ - "beginner" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.development-get-started-business-central", - "unitUids": [ - "learn-dynamics.extend-functionality-using-events.1-introduction", - "learn-dynamics.extend-functionality-using-events.2-programming-basics", - "learn-dynamics.extend-functionality-using-events.3-customer-rewards", - "learn-dynamics.extend-functionality-using-events.4-exercise", - "learn-dynamics.extend-functionality-using-events.5-check", - "learn-dynamics.extend-functionality-using-events.6-summary" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-pages", - "title": "Extend and modify an existing page in Dynamics 365 Business Central", - "summary": "Do you want to add extra fields or remove fields from an existing page? If so, this module will help you discover how to modify an existing page by using page extensions.", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages", - "lastModified": "2026-09-01T17:24:00Z", - "durationInMinutes": 45, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.foundation-customize-business-central", - "unitUids": [ - "learn-dynamics.extend-modify-existing-pages.1-introduction", - "learn-dynamics.extend-modify-existing-pages.2-get-started", - "learn-dynamics.extend-modify-existing-pages.3-properties", - "learn-dynamics.extend-modify-existing-pages.4-layout", - "learn-dynamics.extend-modify-existing-pages.5-check", - "learn-dynamics.extend-modify-existing-pages.6-summary" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-table", - "title": "Extend and modify an existing table in Dynamics 365 Business Central", - "summary": "Do you want to add extra fields to an existing table? This module discusses how you can modify an existing table by using table extensions.", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table", - "lastModified": "2026-08-17T23:09:00Z", - "durationInMinutes": 33, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.foundation-customize-business-central", - "unitUids": [ - "learn-dynamics.extend-modify-existing-table.1-introduction", - "learn-dynamics.extend-modify-existing-table.2-get-started", - "learn-dynamics.extend-modify-existing-table.3-properties", - "learn-dynamics.extend-modify-existing-table.add-field-group", - "learn-dynamics.extend-modify-existing-table.define-extension-objects", - "learn-dynamics.extend-modify-existing-table.4-check", - "learn-dynamics.extend-modify-existing-table.5-summary" - ] - }, - { - "uid": "learn-dynamics.extend-tables-page-objects", - "title": "Extend existing table and page objects in Dynamics 365 Business Central", - "summary": "Do you want to know how to modify existing tables and pages for Microsoft Dynamics 365 Business Central? In this module, you'll learn ways to create and customize table and page objects.", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 42, - "levels": [ - "beginner" - ], - "subjects": [ - "data-modeling" - ], - "learningPathUids": "learn-dynamics.development-get-started-business-central", - "unitUids": [ - "learn-dynamics.extend-tables-page-objects.1-introduction", - "learn-dynamics.extend-tables-page-objects.2-customer-table", - "learn-dynamics.extend-tables-page-objects.3-customer-card", - "learn-dynamics.extend-tables-page-objects.4-customer-list", - "learn-dynamics.extend-tables-page-objects.5-exercise", - "learn-dynamics.extend-tables-page-objects.6-exercise", - "learn-dynamics.extend-tables-page-objects.7-exercise", - "learn-dynamics.extend-tables-page-objects.8-check", - "learn-dynamics.extend-tables-page-objects.9-summary" - ] - }, - { - "uid": "learn-dynamics.handle-errors", - "title": "Handle errors by using application language in Dynamics 365 Business Central", - "summary": "Learn how to handle errors in Dynamics 365 Business Central by using application language (AL).", - "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 35, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.application-foundation-al-language", - "unitUids": [ - "learn-dynamics.handle-errors.introduction", - "learn-dynamics.handle-errors.handle-errors", - "learn-dynamics.handle-errors.collect-errors", - "learn-dynamics.handle-errors.progress-windows", - "learn-dynamics.handle-errors.check", - "learn-dynamics.handle-errors.summary" - ] - }, - { - "uid": "learn-dynamics.identify-table-types", - "title": "Identify functional table types and characteristics in Dynamics 365 Business Central", - "summary": "Do you want to learn about table types in Business Central? In this module, you'll learn about the different functional table types and their specific characteristics, such as special fields, properties, and linked pages.", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 68, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.essential-development-standards", - "unitUids": [ - "learn-dynamics.identify-table-types.1-introduction", - "learn-dynamics.identify-table-types.2-define-table-types", - "learn-dynamics.identify-table-types.3-create-master-tables", - "learn-dynamics.identify-table-types.4-create-supplemental-tables", - "learn-dynamics.identify-table-types.5-create-subsidiary-tables", - "learn-dynamics.identify-table-types.6-create-ledger-tables", - "learn-dynamics.identify-table-types.7-create-register-tables", - "learn-dynamics.identify-table-types.8-create-journal-tables", - "learn-dynamics.identify-table-types.9-create-document-tables", - "learn-dynamics.identify-table-types.10-create-setup-tables", - "learn-dynamics.identify-table-types.11-check", - "learn-dynamics.identify-table-types.12-summary" - ] - }, - { - "uid": "learn-dynamics.implement-data-process-model-business-central", - "title": "Implement the data process model in Dynamics 365 Business Central", - "summary": "Do you want to understand how data flows in Business Central? This module provides an overview of how data flows within the application so you can develop solutions efficiently.", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 37, - "levels": [ - "intermediate" - ], - "subjects": [ - "data-modeling", - "process-workflow" - ], - "learningPathUids": "learn-dynamics.essential-development-standards", - "unitUids": [ - "learn-dynamics.implement-data-process-model-business-central.1-introduction", - "learn-dynamics.implement-data-process-model-business-central.2-standard-data-model", - "learn-dynamics.implement-data-process-model-business-central.3-typical-data-triggers", - "learn-dynamics.implement-data-process-model-business-central.4-standard-data-flow", - "learn-dynamics.implement-data-process-model-business-central.5-check", - "learn-dynamics.implement-data-process-model-business-central.6-summary" - ] - }, - { - "uid": "learn-dynamics.implement-multilanguage-reports", - "title": "Implement multilingual reports in Dynamics 365 Business Central", - "summary": "Do you want to learn how to make your reports multilingual in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to generate a report in the language of the user or the recipient.", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 36, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.build-reports", - "unitUids": [ - "learn-dynamics.implement-multilanguage-reports.1-introduction", - "learn-dynamics.implement-multilanguage-reports.2-create-report-user-language", - "learn-dynamics.implement-multilanguage-reports.3-create-report-recipient-language", - "learn-dynamics.implement-multilanguage-reports.define-regional-settings", - "learn-dynamics.implement-multilanguage-reports.4-check", - "learn-dynamics.implement-multilanguage-reports.5-summary" - ] - }, - { - "uid": "learn-dynamics.implement-saas-techniques", - "title": "Implement SaaSification techniques in Business Central", - "summary": "Do you want to learn about how to build your user interface using SaaSification techniques? Learn about properly building a good user interface and first trial experience for the users of your application.", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 31, - "levels": [ - "intermediate" - ], - "subjects": [ - "custom-app-development" - ], - "learningPathUids": "learn-dynamics.use-application-development-business-central", - "unitUids": [ - "learn-dynamics.implement-saas-techniques.1-introduction", - "learn-dynamics.implement-saas-techniques.2-build-trial", - "learn-dynamics.implement-saas-techniques.3-implement-saasification", - "learn-dynamics.implement-saas-techniques.4-check", - "learn-dynamics.implement-saas-techniques.5-summary" - ] - }, - { - "uid": "learn-dynamics.initialize-data-extension-installation", - "title": "Initialize data during extension installation in Dynamics 365 Business Central", - "summary": "Do you want to know how to initialize data during extension installation for Microsoft Dynamics 365 Business Central? Initializing data when you're installing an extension automate many steps that you must otherwise perform manually. By automating this process, you can quickly set up an extension and it becomes more user-friendly.", - "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 32, - "levels": [ - "beginner" - ], - "subjects": [ - "data-engineering" - ], - "learningPathUids": "learn-dynamics.development-get-started-business-central", - "unitUids": [ - "learn-dynamics.initialize-data-extension-installation.1-introduction", - "learn-dynamics.initialize-data-extension-installation.2-install-logic", - "learn-dynamics.initialize-data-extension-installation.3-exercise", - "learn-dynamics.initialize-data-extension-installation.4-check", - "learn-dynamics.initialize-data-extension-installation.5-summary" - ] - }, - { - "uid": "learn-dynamics.interfaces", - "title": "Implement interfaces in Dynamics 365 Business Central", - "summary": "Do you want to know how to implement interfaces in AL? If so, this module will explain how to implement interfaces in AL for use with Microsoft Dynamics 365 Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 54, - "levels": [ - "intermediate" - ], - "subjects": [ - "process-workflow" - ], - "learningPathUids": "learn-dynamics.application-foundation-al-language", - "unitUids": [ - "learn-dynamics.business-central-interfaces.1-introduction", - "learn-dynamics.business-central-interfaces.2-snippet-support", - "learn-dynamics.business-central-interfaces.type-testing", - "learn-dynamics.business-central-interfaces.3-why-interfaces", - "learn-dynamics.business-central-interfaces.extending-interfaces", - "learn-dynamics.business-central-interfaces.4-exercise", - "learn-dynamics.business-central-interfaces.5-check", - "learn-dynamics.business-central-interfaces.6-summary" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming", - "title": "Introduction to the basics of AL programming in Dynamics 365 Business Central", - "summary": "Explore the basics of Application Language (AL) programming, including variables, data types, options, and enums.", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 88, - "levels": [ - "beginner" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.application-foundation-al-language", - "unitUids": [ - "learn-dynamics.intro-basics-al-programming.1-intro", - "learn-dynamics.intro-basics-al-programming.2-get-started", - "learn-dynamics.intro-basics-al-programming.3-variables", - "learn-dynamics.intro-basics-al-programming.4-data-types", - "learn-dynamics.intro-basics-al-programming.5-options-enums", - "learn-dynamics.intro-basics-al-programming.6-collections", - "learn-dynamics.intro-basics-al-programming.7-assignments", - "learn-dynamics.intro-basics-al-programming.8-expressions", - "learn-dynamics.intro-basics-al-programming.9-exercise", - "learn-dynamics.intro-basics-al-programming.10-exercise", - "learn-dynamics.intro-basics-al-programming.11-check", - "learn-dynamics.intro-basics-al-programming.12-summary" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment", - "title": "Introduction to the development environment for Dynamics 365 Business Central", - "summary": "Do you want to start developing for Microsoft Dynamics 365 Business Central? If so, this module is for you. Learn about the development environment that is used for Business Central development and how to configure that environment. You will also learn about the different objects in Microsoft Dynamics 365 Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 95, - "levels": [ - "beginner" - ], - "subjects": [ - "devops" - ], - "learningPathUids": "learn-dynamics.foundation-customize-business-central", - "unitUids": [ - "learn-dynamics.intro-development-environment.1-intro", - "learn-dynamics.intro-development-environment.2-get-started", - "learn-dynamics.intro-development-environment.3-install", - "learn-dynamics.intro-development-environment.4-logical-database", - "learn-dynamics.intro-development-environment.differentiate-apps", - "learn-dynamics.intro-development-environment.5-extension", - "learn-dynamics.intro-development-environment.6-json", - "learn-dynamics.intro-development-environment.7-al-language", - "learn-dynamics.intro-development-environment.8-multiple", - "learn-dynamics.intro-development-environment.segment-code-reduce-conflicts", - "learn-dynamics.intro-development-environment.open-visual-studio-code-web-client", - "learn-dynamics.intro-development-environment.9-exercise", - "learn-dynamics.intro-development-environment.10-check", - "learn-dynamics.intro-development-environment.11-summary" - ] - }, - { - "uid": "learn-dynamics.introduction-basics-file-management", - "title": "Introduction to the basics of file management in Dynamics 365 Business Central", - "summary": "Explore the file data type, File Management codeunit, Temp Blob codeunit, and stream functions for file manipulation in Dynamics 365 Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management", - "lastModified": "2026-08-18T17:07:00Z", - "durationInMinutes": 61, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications", - "data-management" - ], - "learningPathUids": "learn-dynamics.data-management-foundation-business-central", - "unitUids": [ - "learn-dynamics.introduction-basics-file-management.1-introduction", - "learn-dynamics.introduction-basics-file-management.2-file-object", - "learn-dynamics.introduction-basics-file-management.3-temp-blob-object", - "learn-dynamics.introduction-basics-file-management.4-read-write-stream", - "learn-dynamics.introduction-basics-file-management.uploads-drop-zones", - "learn-dynamics.introduction-basics-file-management.5-exercise", - "learn-dynamics.introduction-basics-file-management.6-check", - "learn-dynamics.introduction-basics-file-management.7-summary" - ] - }, - { - "uid": "learn-dynamics.introduction-development-process", - "title": "Introduction to the development process in Dynamics 365 Business Central", - "summary": "Do you want to know how to start creating an extension for Microsoft Dynamics 365 Business Central? If so, this module is for you. In this module, you'll learn about the different components that make up a typical extension. Then, the module will provide a brief description of the development environment and explain the different steps that are involved in getting started with an extension.", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 40, - "levels": [ - "beginner" - ], - "subjects": [ - "process-workflow" - ], - "learningPathUids": "learn-dynamics.development-get-started-business-central", - "unitUids": [ - "learn-dynamics.introduction-development-process.1-introduction", - "learn-dynamics.introduction-development-process.2-process", - "learn-dynamics.introduction-development-process.3-setup", - "learn-dynamics.introduction-development-process.4-rewards", - "learn-dynamics.introduction-development-process.5-exercise", - "learn-dynamics.introduction-development-process.6-exercise", - "learn-dynamics.introduction-development-process.7-check", - "learn-dynamics.introduction-development-process.8-summary" - ] - }, - { - "uid": "learn-dynamics.introduction-power-platform-business-central", - "title": "Introduction to Microsoft Power Platform and Business Central", - "summary": "Do you want to use Microsoft Power Platform with Business Central? This module focuses on the business value and product capabilities of Microsoft Power Platform when you use it with Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 53, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.power-apps-power-automate-business-central", - "unitUids": [ - "learn-dynamics.introduction-power-platform-business-central.1-introduction", - "learn-dynamics.introduction-power-platform-business-central.2-data-connectors", - "learn-dynamics.introduction-power-platform-business-central.3-business-central-connectors", - "learn-dynamics.introduction-power-platform-business-central.4-power-apps", - "learn-dynamics.introduction-power-platform-business-central.5-power-automate", - "learn-dynamics.introduction-power-platform-business-central.use-cases", - "learn-dynamics.introduction-power-platform-business-central.6-embed-power-platform", - "learn-dynamics.introduction-power-platform-business-central.7-check", - "learn-dynamics.introduction-power-platform-business-central.8-summary" - ] - }, - { - "uid": "learn-dynamics.introduction-report-components", - "title": "Introduction to report components in Dynamics 365 Business Central", - "summary": "Do you want to know how to start creating reports for Business Central? If so, this module will explain the different components that a typical report consists of.", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 37, - "levels": [ - "intermediate" - ], - "subjects": [ - "data-management" - ], - "learningPathUids": "learn-dynamics.build-reports", - "unitUids": [ - "learn-dynamics.introduction-report-components.1-introduction", - "learn-dynamics.introduction-report-components.2-learn-overview-report-description", - "learn-dynamics.introduction-report-components.3-report-properties-triggers", - "learn-dynamics.introduction-report-components.4-data-items-columns", - "learn-dynamics.introduction-report-components.5-request-page", - "learn-dynamics.introduction-report-components.6-work-report-labels", - "learn-dynamics.introduction-report-components.7-report-layouts", - "learn-dynamics.introduction-report-components.8-check", - "learn-dynamics.introduction-report-components.9-summary" - ] - }, - { - "uid": "learn-dynamics.introduction-test-automation", - "title": "Introduction to test automation in Business Central", - "summary": "Discover how to write test code in AL using Test Codeunits and run standard Business Central tests.", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 58, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.continuous-integration", - "unitUids": [ - "learn-dynamics.introduction-test-automation.1-introduction", - "learn-dynamics.introduction-test-automation.2-test-applications", - "learn-dynamics.introduction-test-automation.3-install-run-test-toolkit", - "learn-dynamics.introduction-test-automation.4-run-standard-tests", - "learn-dynamics.introduction-test-automation.5-create-test-code-units", - "learn-dynamics.introduction-test-automation.6-check", - "learn-dynamics.introduction-test-automation.7-summary" - ] - }, - { - "uid": "learn-dynamics.manage-multilanguage-development", - "title": "Manage multilanguage development in Dynamics 365 Business Central", - "summary": "Do you want to learn how to manage multiple languages in an AL extension? If so, this module explains how to create and use translation files.", - "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 43, - "levels": [ - "intermediate" - ], - "subjects": [ - "devops" - ], - "learningPathUids": "learn-dynamics.application-foundation-al-language", - "unitUids": [ - "learn-dynamics.manage-multilanguage-development.1-introduction", - "learn-dynamics.manage-multilanguage-development.2-translation-files", - "learn-dynamics.manage-multilanguage-development.3-automate", - "learn-dynamics.manage-multilanguage-development.4-exercise", - "learn-dynamics.manage-multilanguage-development.5-check", - "learn-dynamics.manage-multilanguage-development.6-summary" - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code", - "title": "Manipulate data by using code in Dynamics 365 Business Central", - "summary": "Do you want to know how to retrieve data from the database and manipulate that data or add new data? In this module, you learn about the different AL data manipulation statements.", - "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 77, - "levels": [ - "intermediate" - ], - "subjects": [ - "data-engineering", - "data-management" - ], - "learningPathUids": "learn-dynamics.application-foundation-al-language", - "unitUids": [ - "learn-dynamics.manipulate-data-via-code.1-introduction", - "learn-dynamics.manipulate-data-via-code.2-retrieve-data", - "learn-dynamics.manipulate-data-via-code.3-sort-filter", - "learn-dynamics.manipulate-data-via-code.4-add-modify-delete", - "learn-dynamics.manipulate-data-via-code.5-field-functions", - "learn-dynamics.manipulate-data-via-code.control-database-locking-behavior", - "learn-dynamics.manipulate-data-via-code.6-exercise", - "learn-dynamics.manipulate-data-via-code.7-exercise", - "learn-dynamics.manipulate-data-via-code.8-check", - "learn-dynamics.manipulate-data-via-code.9-summary" - ] - }, - { - "uid": "learn-dynamics.report-extensibility", - "title": "Extend reports in Dynamics 365 Business Central", - "summary": "Do you want to learn how to extend reports in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to extend reports.", - "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 53, - "levels": [ - "intermediate" - ], - "subjects": [ - "finance-accounting" - ], - "learningPathUids": "learn-dynamics.build-reports", - "unitUids": [ - "learn-dynamics.report-extensibility.1-introduction", - "learn-dynamics.report-extensibility.2-anatomy", - "learn-dynamics.report-extensibility.3-report-extension-example", - "learn-dynamics.report-extensibility.4-check", - "learn-dynamics.report-extensibility.5-summary" - ] - }, - { - "uid": "learn-dynamics.send-notifications", - "title": "Send notifications in Dynamics 365 Business Central", - "summary": "Discover how to create, send, and enhance user interactions through notifications in this comprehensive guide in Dynamics 365 Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 36, - "levels": [ - "intermediate" - ], - "subjects": [ - "collaboration", - "communication" - ], - "learningPathUids": "learn-dynamics.tailor-roles-design-ui", - "unitUids": [ - "learn-dynamics.send-notifications.1-introduction", - "learn-dynamics.send-notifications.2-notifications", - "learn-dynamics.send-notifications.3-create-send", - "learn-dynamics.send-notifications.4-exercise", - "learn-dynamics.send-notifications.5-check", - "learn-dynamics.send-notifications.6-summary" - ] - }, - { - "uid": "learn-dynamics.set-up-onboarding-experiences", - "title": "Set up onboarding experiences in Dynamics 365 Business Central", - "summary": "Learn how to set up onboarding experiences in Dynamics 365 Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 56, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.tailor-roles-design-ui", - "unitUids": [ - "learn-dynamics.set-up-onboarding-experiences.introduction", - "learn-dynamics.set-up-onboarding-experiences.onboard-new-users", - "learn-dynamics.set-up-onboarding-experiences.get-users-started-checklist", - "learn-dynamics.set-up-onboarding-experiences.implement-tips-tour", - "learn-dynamics.set-up-onboarding-experiences.check", - "learn-dynamics.set-up-onboarding-experiences.summary" - ] - }, - { - "uid": "learn-dynamics.test-automation", - "title": "Implement test automation techniques in Business Central", - "summary": "Do you want to implement test automation techniques? In this module, you learn about implementing test automation techniques and where to find useful guides and documentation.", - "url": "https://learn.microsoft.com/en-us/training/modules/test-automation", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 91, - "levels": [ - "intermediate" - ], - "subjects": [ - "process-workflow" - ], - "learningPathUids": "learn-dynamics.use-application-development-business-central", - "unitUids": [ - "learn-dynamics.test-automation.1-introduction", - "learn-dynamics.test-automation.2-test-automation-responsibilities", - "learn-dynamics.test-automation.3-documentation-examples", - "learn-dynamics.test-automation.page-scripting-tool", - "learn-dynamics.test-automation.4-answers-about-testing", - "learn-dynamics.test-automation.5-check", - "learn-dynamics.test-automation.6-summary" - ] - }, - { - "uid": "learn-dynamics.understand-report-design-process", - "title": "Use the report design process in Dynamics 365 Business Central", - "summary": "Do you want to learn about the report design process for Microsoft Dynamics 365 Business Central? If so, this module will explain the different steps that are involved in creating a report.", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 24, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.build-reports", - "unitUids": [ - "learn-dynamics.understand-report-design-process.1-introduction", - "learn-dynamics.understand-report-design-process.2-report-design-process", - "learn-dynamics.understand-report-design-process.3-use-snippet-create-report", - "learn-dynamics.understand-report-design-process.4-check", - "learn-dynamics.understand-report-design-process.5-summary" - ] - }, - { - "uid": "learn-dynamics.understand-report-triggers-functions", - "title": "Use report triggers, functions, and processing-only reports in Dynamics 365 Business Central", - "summary": "Do you want to learn how to work with report triggers and functions in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to create processing-only reports and how to use the report triggers and functions.", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions", - "lastModified": "2026-08-27T23:37:00Z", - "durationInMinutes": 56, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.build-reports", - "unitUids": [ - "learn-dynamics.understand-report-triggers-functions.1-introduction", - "learn-dynamics.understand-report-triggers-functions.2-understand-report-trigger-execution", - "learn-dynamics.understand-report-triggers-functions.3-use-report-functions", - "learn-dynamics.understand-report-triggers-functions.4-create-processing-only-report", - "learn-dynamics.understand-report-triggers-functions.4a-adding-barcodes", - "learn-dynamics.understand-report-triggers-functions.5-exercise", - "learn-dynamics.understand-report-triggers-functions.6-check", - "learn-dynamics.understand-report-triggers-functions.7-summary" - ] - }, - { - "uid": "learn-dynamics.use-al-controlled-barcode-scanning", - "title": "Use AL-controlled barcode scanning in Dynamics 365 Business Central", - "summary": "Learn how Business Central provides barcode scanning capabilities in the mobile application and AL language.", - "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning", - "lastModified": "2026-08-20T23:07:00Z", - "durationInMinutes": 35, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.interface-with-business-central", - "unitUids": [ - "learn-dynamics.use-al-controlled-barcode-scanning.introduction", - "learn-dynamics.use-al-controlled-barcode-scanning.scenarios-barcodes", - "learn-dynamics.use-al-controlled-barcode-scanning.add-button", - "learn-dynamics.use-al-controlled-barcode-scanning.invoke-scanning", - "learn-dynamics.use-al-controlled-barcode-scanning.integrate-scanners", - "learn-dynamics.use-al-controlled-barcode-scanning.check", - "learn-dynamics.use-al-controlled-barcode-scanning.summary" - ] - }, - { - "uid": "learn-dynamics.use-application-lifecycle", - "title": "Use Application Lifecycle Management for Business Central", - "summary": "Do you want to know how to start implementing a version control system to manage your Business Central applications source code? With Azure DevOps you can do that, and much more. You'll learn about the features of the application lifecycle management tool Azure DevOps.", - "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle", - "lastModified": "2026-08-28T23:11:00Z", - "durationInMinutes": 47, - "levels": [ - "intermediate" - ], - "subjects": [ - "devops" - ], - "learningPathUids": "learn-dynamics.continuous-integration", - "unitUids": [ - "learn-dynamics.use-application-lifecycle.1-introduction", - "learn-dynamics.use-application-lifecycle.2-create-azure-devops", - "learn-dynamics.use-application-lifecycle.3-manage-azure-devops", - "learn-dynamics.use-application-lifecycle.4-introduction-devops-services", - "learn-dynamics.use-application-lifecycle.5-connect-devops-tokens", - "learn-dynamics.use-application-lifecycle.6-introduction-github", - "learn-dynamics.use-application-lifecycle.7-exercise", - "learn-dynamics.use-application-lifecycle.8-check", - "learn-dynamics.use-application-lifecycle.9-summary" - ] - }, - { - "uid": "learn-dynamics.use-azure-functions", - "title": "Use Azure Functions with Dynamics 365 Business Central", - "summary": "Do you want to learn how to use Azure Functions with Business Central? This module will explain what an Azure function is and how to create one. You'll also learn how to integrate them with Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions", - "lastModified": "2026-08-17T18:23:00Z", - "durationInMinutes": 25, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.interface-with-business-central", - "unitUids": [ - "learn-dynamics.use-azure-functions.1-introduction", - "learn-dynamics.use-azure-functions.2-create-basic", - "learn-dynamics.use-azure-functions.3-create-existing", - "learn-dynamics.use-azure-functions.4-use", - "learn-dynamics.use-azure-functions.6-check", - "learn-dynamics.use-azure-functions.7-summary" - ] - }, - { - "uid": "learn-dynamics.use-azure-pipelines", - "title": "Use Azure Pipelines for CI/CD with Business Central", - "summary": "Learn how to configure and set up an automated build process, create pipelines using YAML files, and set up a release process in Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 46, - "levels": [ - "intermediate" - ], - "subjects": [ - "process-workflow" - ], - "learningPathUids": "learn-dynamics.continuous-integration", - "unitUids": [ - "learn-dynamics.use-azure-pipelines.1-introduction", - "learn-dynamics.use-azure-pipelines.2-what-continuous-integration", - "learn-dynamics.use-azure-pipelines.3-configure-build-agents", - "learn-dynamics.use-azure-pipelines.4-create-build-pipeline", - "learn-dynamics.use-azure-pipelines.5-work-yaml-files", - "learn-dynamics.use-azure-pipelines.6-create-release-pipeline", - "learn-dynamics.use-azure-pipelines.7-exercise", - "learn-dynamics.use-azure-pipelines.8-check", - "learn-dynamics.use-azure-pipelines.9-summary" - ] - }, - { - "uid": "learn-dynamics.use-branch-merge-git", - "title": "Use branching and merging with Git for Business Central", - "summary": "Do you want to work with different branches in Git to separate your development code from the code in production? In this module we'll discuss how you can work with branches in Git and merge your modifications.", - "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 64, - "levels": [ - "intermediate" - ], - "subjects": [ - "devops" - ], - "learningPathUids": "learn-dynamics.continuous-integration", - "unitUids": [ - "learn-dynamics.use-branch-merge-git.1-introduction", - "learn-dynamics.use-branch-merge-git.2-branch-commands", - "learn-dynamics.use-branch-merge-git.3-use-rebase", - "learn-dynamics.use-branch-merge-git.4-set-branch-policies", - "learn-dynamics.use-branch-merge-git.5-use-pull-requests", - "learn-dynamics.use-branch-merge-git.6-work-branch-strategies", - "learn-dynamics.use-branch-merge-git.7-exercise", - "learn-dynamics.use-branch-merge-git.8-check", - "learn-dynamics.use-branch-merge-git.9-summary" - ] - }, - { - "uid": "learn-dynamics.use-docker-business-central", - "title": "Use Docker in your Business Central development process", - "summary": "Do you want to know how you can benefit from working with Docker and Microsoft Dynamics 365 Business Central? Docker offers images for every released version of Business Central in every available country or region.", - "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central", - "lastModified": "2026-08-13T17:20:00Z", - "durationInMinutes": 68, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.continuous-integration", - "unitUids": [ - "learn-dynamics.use-docker-business-central.1-introduction", - "learn-dynamics.use-docker-business-central.2-docker-images-containers", - "learn-dynamics.use-docker-business-central.3-install-docker-windows", - "learn-dynamics.use-docker-business-central.4-process-isolation-hyper-v", - "learn-dynamics.use-docker-business-central.5-basic-docker-commands", - "learn-dynamics.use-docker-business-central.6-use-bc-container-helper", - "learn-dynamics.use-docker-business-central.7-exercise", - "learn-dynamics.use-docker-business-central.8-check", - "learn-dynamics.use-docker-business-central.9-summary" - ] - }, - { - "uid": "learn-dynamics.use-document-standards-business-central", - "title": "Use document standards in Dynamics 365 Business Central", - "summary": "Do you plan to develop solutions involving documents? In this module, you learn how to work with document data and about document standards in Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 57, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.essential-development-standards", - "unitUids": [ - "learn-dynamics.use-document-standards-business-central.1-introduction", - "learn-dynamics.use-document-standards-business-central.2-create-document-page", - "learn-dynamics.use-document-standards-business-central.3-use-initrecord-function", - "learn-dynamics.use-document-standards-business-central.4-use-testfield-function", - "learn-dynamics.use-document-standards-business-central.4a-use-round-function", - "learn-dynamics.use-document-standards-business-central.4b-exercise", - "learn-dynamics.use-document-standards-business-central.5-check", - "learn-dynamics.use-document-standards-business-central.6-summary" - ] - }, - { - "uid": "learn-dynamics.use-model-driven-apps-common-data-service", - "title": "Use model-driven apps and Dataverse with Business Central", - "summary": "This module focuses on the building blocks of Dataverse and model-driven apps as well as using them together with Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 83, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.power-apps-power-automate-business-central", - "unitUids": [ - "learn-dynamics.use-model-driven-apps-common-data-service.1-introduction", - "learn-dynamics.use-model-driven-apps-common-data-service.2-entities-fields-common-data-service", - "learn-dynamics.use-model-driven-apps-common-data-service.3-create-entity-import-data", - "learn-dynamics.use-model-driven-apps-common-data-service.4-model-driven-apps", - "learn-dynamics.use-model-driven-apps-common-data-service.5-user-interface-logic", - "learn-dynamics.use-model-driven-apps-common-data-service.6-create-model-driven-app", - "learn-dynamics.use-model-driven-apps-common-data-service.7-exercise", - "learn-dynamics.use-model-driven-apps-common-data-service.8-check", - "learn-dynamics.use-model-driven-apps-common-data-service.9-summary" - ] - }, - { - "uid": "learn-dynamics.use-power-automate", - "title": "Use Power Automate with Business Central", - "summary": "Discover the power of Power Automate in Business Central and explore the available actions and triggers.", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 128, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.power-apps-power-automate-business-central", - "unitUids": [ - "learn-dynamics.use-power-automate.1-introduction", - "learn-dynamics.use-power-automate.2-triggers", - "learn-dynamics.use-power-automate.3-create-power-automate", - "learn-dynamics.use-power-automate.4-item-approvals", - "learn-dynamics.use-power-automate.5-general-journal-approvals", - "learn-dynamics.use-power-automate.6-document-approvals", - "learn-dynamics.use-power-automate.7-business-scenarios", - "learn-dynamics.use-power-automate.8-create-button", - "learn-dynamics.use-power-automate.9-schedule", - "learn-dynamics.use-power-automate.10-troubleshoot", - "learn-dynamics.use-power-automate.11-exercise", - "learn-dynamics.use-power-automate.12-check", - "learn-dynamics.use-power-automate.13-summary" - ] - }, - { - "uid": "learn-dynamics.work-azure-boards", - "title": "Work with Azure Boards for project management in Business Central", - "summary": "Learn to manage development tasks, track project progress, assign work items and integrate Microsoft Teams with Azure DevOps in Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards", - "lastModified": "2026-08-11T23:09:00Z", - "durationInMinutes": 62, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.continuous-integration", - "unitUids": [ - "learn-dynamics.work-azure-boards.1-introduction", - "learn-dynamics.work-azure-boards.2-work-process-templates", - "learn-dynamics.work-azure-boards.3-configure-iterations", - "learn-dynamics.work-azure-boards.4-create-work-items", - "learn-dynamics.work-azure-boards.5-project-dashboards-wikis", - "learn-dynamics.work-azure-boards.6-teams-integrate-devops", - "learn-dynamics.work-azure-boards.7-exercise", - "learn-dynamics.work-azure-boards.8-check", - "learn-dynamics.work-azure-boards.9-summary" - ] - }, - { - "uid": "learn-dynamics.work-entitlements-permission-sets", - "title": "Work with entitlements and permission sets in Dynamics 365 Business Central", - "summary": "Learn about entitlements and permission sets and how to extend them in Microsoft Dynamics 365 Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets", - "lastModified": "2026-09-03T23:14:00Z", - "durationInMinutes": 43, - "levels": [ - "intermediate" - ], - "subjects": [ - "identity-access" - ], - "learningPathUids": "learn-dynamics.foundation-customize-business-central", - "unitUids": [ - "learn-dynamics.work-entitlements-permission-sets.1-introduction", - "learn-dynamics.work-entitlements-permission-sets.2-get-started", - "learn-dynamics.work-entitlements-permission-sets.3-create-entitlements-permission-sets", - "learn-dynamics.work-entitlements-permission-sets.override-entitlements", - "learn-dynamics.work-entitlements-permission-sets.4-check", - "learn-dynamics.work-entitlements-permission-sets.5-summary" - ] - }, - { - "uid": "learn-dynamics.work-power-bi", - "title": "Work with Power BI in Business Central", - "summary": "Do you want to know how to work with Microsoft Power BI in Dynamics 365 Business Central? If so, this module is for you. You'll learn how to deploy Business Central dashboards in Power BI and how to set up Power BI inside Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 65, - "levels": [ - "intermediate" - ], - "subjects": [ - "business-applications" - ], - "learningPathUids": "learn-dynamics.use-power-bi", - "unitUids": [ - "learn-dynamics.work-power-bi.1-introduction", - "learn-dynamics.work-power-bi.2-deploy-dashboards", - "learn-dynamics.work-power-bi.3-set-up-power-bi", - "learn-dynamics.work-power-bi.4-view-list-data", - "learn-dynamics.work-power-bi.add-report-parts", - "learn-dynamics.work-power-bi.5-exercise", - "learn-dynamics.work-power-bi.6-check", - "learn-dynamics.work-power-bi.7-summary" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git", - "title": "Work with source control using Git in Visual Studio Code for Business Central", - "summary": "Explore how to work with local and remote Git repositories, including configuring Git and creating new repositories in Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 94, - "levels": [ - "intermediate" - ], - "subjects": [ - "process-workflow" - ], - "learningPathUids": "learn-dynamics.continuous-integration", - "unitUids": [ - "learn-dynamics.work-source-control-git.1-introduction", - "learn-dynamics.work-source-control-git.2-configure-git", - "learn-dynamics.work-source-control-git.3-local-remote-repositories", - "learn-dynamics.work-source-control-git.4-create-local-git", - "learn-dynamics.work-source-control-git.5-use-git-ignore", - "learn-dynamics.work-source-control-git.6-add-remove-files", - "learn-dynamics.work-source-control-git.7-get-status-repository", - "learn-dynamics.work-source-control-git.8-use-remote-git", - "learn-dynamics.work-source-control-git.9-clone-remote-git", - "learn-dynamics.work-source-control-git.10-work-remote-git", - "learn-dynamics.work-source-control-git.11-exercise", - "learn-dynamics.work-source-control-git.12-check", - "learn-dynamics.work-source-control-git.13-summary" - ] - }, - { - "uid": "learn-dynamics.work-with-api", - "title": "Work with the API in Dynamics 365 Business Central", - "summary": "Learn how to work with the API in Dynamics Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 43, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.interface-with-business-central", - "unitUids": [ - "learn-dynamics.work-with-api.1-introduction", - "learn-dynamics.work-with-api.2-access", - "learn-dynamics.work-with-api.3-tips", - "learn-dynamics.work-with-api.4-limits", - "learn-dynamics.work-with-api.5-create-new", - "learn-dynamics.work-with-api.6-read", - "learn-dynamics.work-with-api.7-bound-actions", - "learn-dynamics.work-with-api.9-check", - "learn-dynamics.work-with-api.10-summary" - ] - }, - { - "uid": "learn-dynamics.work-with-codeunits", - "title": "Work with codeunits in Dynamics 365 Business Central", - "summary": "Do you want to create your own codeunit and learn how to access the functions within that codeunit? If so, this module is the ideal place to start.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 26, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.application-foundation-al-language", - "unitUids": [ - "learn-dynamics.work-with-codeunits.1-introduction", - "learn-dynamics.work-with-codeunits.2-create-codeunit", - "learn-dynamics.work-with-codeunits.keyword", - "learn-dynamics.work-with-codeunits.3-check", - "learn-dynamics.work-with-codeunits.4-summary" - ] - }, - { - "uid": "learn-dynamics.work-with-master-data-business-central", - "title": "Work with master data in Dynamics 365 Business Central", - "summary": "Learn to enter or import master data into Dynamics 365 Business Central, including creating tables, implementing design patterns, and formatting fields.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central", - "lastModified": "2026-08-18T23:35:00Z", - "durationInMinutes": 61, - "levels": [ - "intermediate" - ], - "subjects": [ - "data-engineering", - "data-management" - ], - "learningPathUids": "learn-dynamics.essential-development-standards", - "unitUids": [ - "learn-dynamics.work-with-master-data-business-central.1-introduction", - "learn-dynamics.work-with-master-data-business-central.2-setup-table-signature-design", - "learn-dynamics.work-with-master-data-business-central.3-master-table-page-standards", - "learn-dynamics.work-with-master-data-business-central.4-implement-series-pattern", - "learn-dynamics.work-with-master-data-business-central.5-use-blocked-field", - "learn-dynamics.work-with-master-data-business-central.6-frequently-used-table-properties", - "learn-dynamics.work-with-master-data-business-central.6a-exercise", - "learn-dynamics.work-with-master-data-business-central.7-check", - "learn-dynamics.work-with-master-data-business-central.8-summary" - ] - }, - { - "uid": "learn-dynamics.work-with-pages", - "title": "Work with pages in Dynamics 365 Business Central", - "summary": "Do you want to create new pages in Business Central that have the same appearance as existing pages? This module focuses on how to create new pages. It will also show you how to enable users to search for specific pages.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 113, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.foundation-customize-business-central", - "unitUids": [ - "learn-dynamics.work-with-pages.1-introduction", - "learn-dynamics.work-with-pages.2-get-started", - "learn-dynamics.work-with-pages.3-types", - "learn-dynamics.work-with-pages.4-properties", - "learn-dynamics.work-with-pages.5-layout", - "learn-dynamics.work-with-pages.6-link", - "learn-dynamics.work-with-pages.7-snippets", - "learn-dynamics.work-with-pages.8-controls", - "learn-dynamics.work-with-pages.use-rich-text-editor", - "learn-dynamics.work-with-pages.scan-barcodes", - "learn-dynamics.work-with-pages.9-search", - "learn-dynamics.work-with-pages.10-actions", - "learn-dynamics.work-with-pages.hidden-fields", - "learn-dynamics.work-with-pages.11-exercise", - "learn-dynamics.work-with-pages.12-exercise", - "learn-dynamics.work-with-pages.13-check", - "learn-dynamics.work-with-pages.14-summary" - ] - }, - { - "uid": "learn-dynamics.work-with-queries", - "title": "Work with queries in Dynamics 365 Business Central", - "summary": "Learn how to work with the Query object in Dynamics 365 Business Central to join, filter, and aggregate data.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 79, - "levels": [ - "intermediate" - ], - "subjects": [ - "data-analytics" - ], - "learningPathUids": "learn-dynamics.data-management-foundation-business-central", - "unitUids": [ - "learn-dynamics.work-with-queries.1-introduction", - "learn-dynamics.work-with-queries.2-get-started", - "learn-dynamics.work-with-queries.3-create", - "learn-dynamics.work-with-queries.4-join-data", - "learn-dynamics.work-with-queries.5-filter-data", - "learn-dynamics.work-with-queries.6-aggregate-data", - "learn-dynamics.work-with-queries.7-al-function", - "learn-dynamics.work-with-queries.8-advanced-concepts", - "learn-dynamics.work-with-queries.optimize-performance", - "learn-dynamics.work-with-queries.9-check", - "learn-dynamics.work-with-queries.10-summary" - ] - }, - { - "uid": "learn-dynamics.work-with-reports", - "title": "Work with reports in Dynamics 365 Business Central", - "summary": "Do you want to learn how to work with reports in Microsoft Dynamics 365 Business Central? In this module, you'll learn how to work with reports and the difference between preview, run, email, and print.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports", - "lastModified": "2026-08-20T17:11:00Z", - "durationInMinutes": 59, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.build-reports", - "unitUids": [ - "learn-dynamics.work-with-reports.1-introduction", - "learn-dynamics.work-with-reports.2-preview-run", - "learn-dynamics.work-with-reports.set-layout", - "learn-dynamics.work-with-reports.3-printers", - "learn-dynamics.work-with-reports.4-check", - "learn-dynamics.work-with-reports.5-summary" - ] - }, - { - "uid": "learn-dynamics.work-with-tables", - "title": "Work with tables in Dynamics 365 Business Central", - "summary": "Do you want to know how to create new tables in Business Central? If so, this module is for you. This module focuses on the different table types in Business Central and show you how to create new tables. Additionally, you learn how to use Visual Studio Code snippets to create a table, fields, and keys.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 99, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.foundation-customize-business-central", - "unitUids": [ - "learn-dynamics.work-with-tables.1-introduction", - "learn-dynamics.work-with-tables.2-get-started", - "learn-dynamics.work-with-tables.3-types", - "learn-dynamics.work-with-tables.4-properties", - "learn-dynamics.work-with-tables.5-fields", - "learn-dynamics.work-with-tables.6-linking", - "learn-dynamics.work-with-tables.7-keys", - "learn-dynamics.work-with-tables.8-dropdown", - "learn-dynamics.work-with-tables.text-search", - "learn-dynamics.work-with-tables.9-calculated", - "learn-dynamics.work-with-tables.10-snippets", - "learn-dynamics.work-with-tables.11-exercise", - "learn-dynamics.work-with-tables.12-check", - "learn-dynamics.work-with-tables.13-summary" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services", - "title": "Work with web services in Dynamics 365 Business Central", - "summary": "Use SOAP and OData to read and update records, and handle UI interaction in Business Central.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 67, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.interface-with-business-central", - "unitUids": [ - "learn-dynamics.work-with-web-services.1-introduction", - "learn-dynamics.work-with-web-services.2-differences", - "learn-dynamics.work-with-web-services.3-authentication", - "learn-dynamics.work-with-web-services.4-enable-access", - "learn-dynamics.work-with-web-services.5-return-documents", - "learn-dynamics.work-with-web-services.6-request-records-odata", - "learn-dynamics.work-with-web-services.7-query-options", - "learn-dynamics.work-with-web-services.8-new-records", - "learn-dynamics.work-with-web-services.9-update-records", - "learn-dynamics.work-with-web-services.10-use-soap", - "learn-dynamics.work-with-web-services.11-request-records-soap", - "learn-dynamics.work-with-web-services.12-ui-interaction", - "learn-dynamics.work-with-web-services.14-check", - "learn-dynamics.work-with-web-services.15-summary" - ] - }, - { - "uid": "learn-dynamics.work-with-xmlports", - "title": "Work with XMLports in Dynamics 365 Business Central", - "summary": "Learn how to define and use XMLports in AL, understand different nodes and properties, and apply them in AL code.", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 65, - "levels": [ - "intermediate" - ], - "subjects": [], - "learningPathUids": "learn-dynamics.data-management-foundation-business-central", - "unitUids": [ - "learn-dynamics.work-with-xmlports.1-introduction", - "learn-dynamics.work-with-xmlports.2-get-started", - "learn-dynamics.work-with-xmlports.3-configure", - "learn-dynamics.work-with-xmlports.4-define-nodes", - "learn-dynamics.work-with-xmlports.5-design", - "learn-dynamics.work-with-xmlports.6-create", - "learn-dynamics.work-with-xmlports.7-al-functions", - "learn-dynamics.work-with-xmlports.8-exercise", - "learn-dynamics.work-with-xmlports.9-check", - "learn-dynamics.work-with-xmlports.10-summary" - ] - } - ], - "units": [ - { - "uid": "learn-dynamics.access-rest-services.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.access-rest-services" - ] - }, - { - "uid": "learn-dynamics.access-rest-services.2-http-classes", - "title": "Use the different HTTP classes and their functions", - "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/2-http-classes", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.access-rest-services" - ] - }, - { - "uid": "learn-dynamics.access-rest-services.3-get-send-data", - "title": "Get data from and send data to an external REST service", - "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/3-get-send-data", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.access-rest-services" - ] - }, - { - "uid": "learn-dynamics.access-rest-services.4-use-read-json", - "title": "Use and read JSON in Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/4-use-read-json", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.access-rest-services" - ] - }, - { - "uid": "learn-dynamics.access-rest-services.5-get-json-data", - "title": "Get JSON data from external REST services", - "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/5-get-json-data", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.access-rest-services" - ] - }, - { - "uid": "learn-dynamics.access-rest-services.6-exercise", - "title": "Exercise - Connect to external REST services", - "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/6-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.access-rest-services" - ] - }, - { - "uid": "learn-dynamics.access-rest-services.7-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/7-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.access-rest-services" - ] - }, - { - "uid": "learn-dynamics.access-rest-services.8-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/access-rest-services/8-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.access-rest-services" - ] - }, - { - "uid": "learn-dynamics.add-layout-report.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.add-layout-report" - ] - }, - { - "uid": "learn-dynamics.add-layout-report.2-add-report-definition", - "title": "Add an RDLC layout to a report", - "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/2-add-report-definition", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.add-layout-report" - ] - }, - { - "uid": "learn-dynamics.add-layout-report.3-add-word-layout", - "title": "Add a Word layout to a report", - "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/3-add-word-layout", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.add-layout-report" - ] - }, - { - "uid": "learn-dynamics.add-layout-report.4-exercise", - "title": "Exercise - Create a basic report", - "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/4-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.add-layout-report" - ] - }, - { - "uid": "learn-dynamics.add-layout-report.5-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/5-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.add-layout-report" - ] - }, - { - "uid": "learn-dynamics.add-layout-report.6-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/6-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.add-layout-report" - ] - }, - { - "uid": "learn-dynamics.add-layout-report.add-excel-layout", - "title": "Add an Excel layout to a report", - "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/add-excel-layout", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.add-layout-report" - ] - }, - { - "uid": "learn-dynamics.add-layout-report.word-add-in", - "title": "Use the Word add-in for report layouts", - "url": "https://learn.microsoft.com/en-us/training/modules/add-layout-report/word-add-in", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.add-layout-report" - ] - }, - { - "uid": "learn-dynamics.add-new-objects.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/1-introduction", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.add-new-objects" - ] - }, - { - "uid": "learn-dynamics.add-new-objects.10-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/10-summary", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.add-new-objects" - ] - }, - { - "uid": "learn-dynamics.add-new-objects.2-reward", - "title": "Create the reward level table object", - "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/2-reward", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.add-new-objects" - ] - }, - { - "uid": "learn-dynamics.add-new-objects.3-activation-code", - "title": "Create the activation code information table object", - "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/3-activation-code", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.add-new-objects" - ] - }, - { - "uid": "learn-dynamics.add-new-objects.4-customer-rewards", - "title": "Create the Customer Rewards Management Setup table object", - "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/4-customer-rewards", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.add-new-objects" - ] - }, - { - "uid": "learn-dynamics.add-new-objects.5-reward-list", - "title": "Create the Rewards Level List page object", - "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/5-reward-list", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.add-new-objects" - ] - }, - { - "uid": "learn-dynamics.add-new-objects.6-exercise", - "title": "Exercise - Create the Activation Code Information table object", - "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/6-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.add-new-objects" - ] - }, - { - "uid": "learn-dynamics.add-new-objects.7-exercise", - "title": "Exercise - Create the Customer Rewards Mgt Setup table object", - "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/7-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.add-new-objects" - ] - }, - { - "uid": "learn-dynamics.add-new-objects.8-exercise", - "title": "Exercise - Create the Rewards Level List page object", - "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/8-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.add-new-objects" - ] - }, - { - "uid": "learn-dynamics.add-new-objects.9-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/add-new-objects/9-check", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.add-new-objects" - ] - }, - { - "uid": "learn-dynamics.add-reports-tell-me.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.add-reports-tell-me" - ] - }, - { - "uid": "learn-dynamics.add-reports-tell-me.2-add-reports-tell-me", - "title": "Add reports to Tell Me", - "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me/2-add-reports-tell-me", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.add-reports-tell-me" - ] - }, - { - "uid": "learn-dynamics.add-reports-tell-me.3-substitute-report", - "title": "Substitute a report", - "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me/3-substitute-report", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.add-reports-tell-me" - ] - }, - { - "uid": "learn-dynamics.add-reports-tell-me.4-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me/4-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.add-reports-tell-me" - ] - }, - { - "uid": "learn-dynamics.add-reports-tell-me.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/add-reports-tell-me/5-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.add-reports-tell-me" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.10-exercise", - "title": "Exercise - Use built-in functions", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/10-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.11-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/11-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.12-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/12-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.2-interaction-functions", - "title": "Interaction methods", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/2-interaction-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.3-string-functions", - "title": "String functions", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/3-string-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.4-date-functions", - "title": "Date functions", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/4-date-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.5-numeric-functions", - "title": "Numeric functions", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/5-numeric-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.6-array-functions", - "title": "Array functions", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/6-array-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.7-list-functions", - "title": "List functions", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/7-list-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.8-system-functions", - "title": "System functions", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/8-system-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-built-in-functions.9-variable-functions", - "title": "Variable functions", - "url": "https://learn.microsoft.com/en-us/training/modules/al-built-in-functions/9-variable-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.al-built-in-functions" - ] - }, - { - "uid": "learn-dynamics.al-statements.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.al-statements" - ] - }, - { - "uid": "learn-dynamics.al-statements.2-compound", - "title": "Work with comment and compound statements", - "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/2-compound", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.al-statements" - ] - }, - { - "uid": "learn-dynamics.al-statements.3-conditional", - "title": "Work with conditional statements", - "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/3-conditional", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.al-statements" - ] - }, - { - "uid": "learn-dynamics.al-statements.4-repetitive", - "title": "Use repetitive statements", - "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/4-repetitive", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 18, - "moduleUids": [ - "learn-dynamics.al-statements" - ] - }, - { - "uid": "learn-dynamics.al-statements.5-exercise", - "title": "Exercise - Use conditional and compound statements", - "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/5-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.al-statements" - ] - }, - { - "uid": "learn-dynamics.al-statements.6-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/6-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.al-statements" - ] - }, - { - "uid": "learn-dynamics.al-statements.7-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/al-statements/7-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.al-statements" - ] - }, - { - "uid": "learn-dynamics.application-monetization.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.application-monetization" - ] - }, - { - "uid": "learn-dynamics.application-monetization.2-responsibilities", - "title": "Understand application monetization responsibilities", - "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization/2-responsibilities", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.application-monetization" - ] - }, - { - "uid": "learn-dynamics.application-monetization.3-documentation-examples", - "title": "Find application monetization documentation and examples", - "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization/3-documentation-examples", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.application-monetization" - ] - }, - { - "uid": "learn-dynamics.application-monetization.4-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization/4-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.application-monetization" - ] - }, - { - "uid": "learn-dynamics.application-monetization.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/application-monetization/5-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.application-monetization" - ] - }, - { - "uid": "learn-dynamics.application-performance-monitor.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor/1-introduction", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.application-performance-monitor" - ] - }, - { - "uid": "learn-dynamics.application-performance-monitor.2-prepare-application", - "title": "Prepare your application for optimal performance", - "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor/2-prepare-application", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.application-performance-monitor" - ] - }, - { - "uid": "learn-dynamics.application-performance-monitor.3-get-insights", - "title": "Get essential Application Insights", - "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor/3-get-insights", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 20, - "moduleUids": [ - "learn-dynamics.application-performance-monitor" - ] - }, - { - "uid": "learn-dynamics.application-performance-monitor.4-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor/4-check", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.application-performance-monitor" - ] - }, - { - "uid": "learn-dynamics.application-performance-monitor.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/application-performance-monitor/5-summary", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.application-performance-monitor" - ] - }, - { - "uid": "learn-dynamics.application-types.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/application-types/1-introduction", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.application-types" - ] - }, - { - "uid": "learn-dynamics.application-types.2-connect-vs-add-on", - "title": "Understand the difference between connect and add-on applications", - "url": "https://learn.microsoft.com/en-us/training/modules/application-types/2-connect-vs-add-on", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.application-types" - ] - }, - { - "uid": "learn-dynamics.application-types.3-library-dependency-applications", - "title": "Understand library and dependency applications", - "url": "https://learn.microsoft.com/en-us/training/modules/application-types/3-library-dependency-applications", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.application-types" - ] - }, - { - "uid": "learn-dynamics.application-types.4-find-development-resources", - "title": "Find development resources for building applications", - "url": "https://learn.microsoft.com/en-us/training/modules/application-types/4-find-development-resources", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 11, - "moduleUids": [ - "learn-dynamics.application-types" - ] - }, - { - "uid": "learn-dynamics.application-types.5-set-up-code-cop-tools", - "title": "Learn how to properly set up code cop tools for app validation", - "url": "https://learn.microsoft.com/en-us/training/modules/application-types/5-set-up-code-cop-tools", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 20, - "moduleUids": [ - "learn-dynamics.application-types" - ] - }, - { - "uid": "learn-dynamics.application-types.6-check", - "title": "Module assessment", - "url": "https://learn.microsoft.com/en-us/training/modules/application-types/6-check", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.application-types" - ] - }, - { - "uid": "learn-dynamics.application-types.7-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/application-types/7-summary", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.application-types" - ] - }, - { - "uid": "learn-dynamics.assisted-setup-tooltips.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/1-introduction", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.assisted-setup-tooltips" - ] - }, - { - "uid": "learn-dynamics.assisted-setup-tooltips.2-rewards-wizard", - "title": "Create the Customer Rewards Wizard page object", - "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/2-rewards-wizard", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.assisted-setup-tooltips" - ] - }, - { - "uid": "learn-dynamics.assisted-setup-tooltips.3-assisted-setup", - "title": "Add the Customer Rewards Wizard page to the assisted setup", - "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/3-assisted-setup", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.assisted-setup-tooltips" - ] - }, - { - "uid": "learn-dynamics.assisted-setup-tooltips.4-tooltips", - "title": "Add tooltips to your extension", - "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/4-tooltips", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.assisted-setup-tooltips" - ] - }, - { - "uid": "learn-dynamics.assisted-setup-tooltips.5-exercise", - "title": "Exercise - Create the Customer Rewards Wizard page object", - "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/5-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.assisted-setup-tooltips" - ] - }, - { - "uid": "learn-dynamics.assisted-setup-tooltips.6-exercise", - "title": "Exercise - Add the Customer Rewards Wizard page to the assisted setup", - "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/6-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.assisted-setup-tooltips" - ] - }, - { - "uid": "learn-dynamics.assisted-setup-tooltips.7-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/7-check", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.assisted-setup-tooltips" - ] - }, - { - "uid": "learn-dynamics.assisted-setup-tooltips.8-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/assisted-setup-tooltips/8-summary", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.assisted-setup-tooltips" - ] - }, - { - "uid": "learn-dynamics.bring-app-appsource.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/1-introduction", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.bring-app-appsource" - ] - }, - { - "uid": "learn-dynamics.bring-app-appsource.2-get-accounts", - "title": "Get your accounts in place", - "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/2-get-accounts", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.bring-app-appsource" - ] - }, - { - "uid": "learn-dynamics.bring-app-appsource.3-marketing-validation", - "title": "Understand the marketing validation requirements", - "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/3-marketing-validation", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.bring-app-appsource" - ] - }, - { - "uid": "learn-dynamics.bring-app-appsource.4-technical-validation", - "title": "Understand the technical validation requirements", - "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/4-technical-validation", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.bring-app-appsource" - ] - }, - { - "uid": "learn-dynamics.bring-app-appsource.5-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/5-check", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.bring-app-appsource" - ] - }, - { - "uid": "learn-dynamics.bring-app-appsource.6-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/bring-app-appsource/6-summary", - "lastModified": "2026-08-26T23:12:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.bring-app-appsource" - ] - }, - { - "uid": "learn-dynamics.build-assisted-setup.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.build-assisted-setup" - ] - }, - { - "uid": "learn-dynamics.build-assisted-setup.2-assisted-setup", - "title": "Use Assisted Setup", - "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/2-assisted-setup", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.build-assisted-setup" - ] - }, - { - "uid": "learn-dynamics.build-assisted-setup.3-wizard-pages", - "title": "Create a wizard page", - "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/3-wizard-pages", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 18, - "moduleUids": [ - "learn-dynamics.build-assisted-setup" - ] - }, - { - "uid": "learn-dynamics.build-assisted-setup.4-extensions", - "title": "Integrate with Assisted Setup in your extension", - "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/4-extensions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.build-assisted-setup" - ] - }, - { - "uid": "learn-dynamics.build-assisted-setup.5-exercise", - "title": "Exercise - Build a wizard page with Assisted Setup", - "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/5-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.build-assisted-setup" - ] - }, - { - "uid": "learn-dynamics.build-assisted-setup.6-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/6-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.build-assisted-setup" - ] - }, - { - "uid": "learn-dynamics.build-assisted-setup.7-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/build-assisted-setup/7-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.build-assisted-setup" - ] - }, - { - "uid": "learn-dynamics.build-control-add-ins.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.build-control-add-ins" - ] - }, - { - "uid": "learn-dynamics.build-control-add-ins.2-apply-look-feel", - "title": "Apply the Business Central look and feel", - "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/2-apply-look-feel", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.build-control-add-ins" - ] - }, - { - "uid": "learn-dynamics.build-control-add-ins.3-create", - "title": "Build a control add-in for Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/3-create", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.build-control-add-ins" - ] - }, - { - "uid": "learn-dynamics.build-control-add-ins.4-asynchronous-considerations", - "title": "Asynchronous considerations for control add-ins", - "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/4-asynchronous-considerations", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.build-control-add-ins" - ] - }, - { - "uid": "learn-dynamics.build-control-add-ins.5-connect", - "title": "Connect control add-ins with Azure Functions", - "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/5-connect", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.build-control-add-ins" - ] - }, - { - "uid": "learn-dynamics.build-control-add-ins.6-exercise", - "title": "Exercise - Build a control add-in object", - "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/6-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.build-control-add-ins" - ] - }, - { - "uid": "learn-dynamics.build-control-add-ins.7-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/7-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.build-control-add-ins" - ] - }, - { - "uid": "learn-dynamics.build-control-add-ins.8-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/build-control-add-ins/8-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.build-control-add-ins" - ] - }, - { - "uid": "learn-dynamics.build-power-bi-report.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/1-introduction", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.build-power-bi-report" - ] - }, - { - "uid": "learn-dynamics.build-power-bi-report.10-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/10-summary", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.build-power-bi-report" - ] - }, - { - "uid": "learn-dynamics.build-power-bi-report.2-build-data-model", - "title": "Build the data model for Power BI", - "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/2-build-data-model", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.build-power-bi-report" - ] - }, - { - "uid": "learn-dynamics.build-power-bi-report.3-add-data-dimension", - "title": "Add a date dimension", - "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/3-add-data-dimension", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.build-power-bi-report" - ] - }, - { - "uid": "learn-dynamics.build-power-bi-report.4-introduction-dax", - "title": "Introduction to DAX", - "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/4-introduction-dax", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.build-power-bi-report" - ] - }, - { - "uid": "learn-dynamics.build-power-bi-report.5-create-report", - "title": "Create a report", - "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/5-create-report", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.build-power-bi-report" - ] - }, - { - "uid": "learn-dynamics.build-power-bi-report.6-build-dashboard", - "title": "Build a dashboard", - "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/6-build-dashboard", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.build-power-bi-report" - ] - }, - { - "uid": "learn-dynamics.build-power-bi-report.7-exercise", - "title": "Exercise - Add a calculated column and measure in Power BI Desktop using DAX", - "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/7-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.build-power-bi-report" - ] - }, - { - "uid": "learn-dynamics.build-power-bi-report.8-exercise", - "title": "Exercise - Visualize your data in Power BI Desktop", - "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/8-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.build-power-bi-report" - ] - }, - { - "uid": "learn-dynamics.build-power-bi-report.9-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/build-power-bi-report/9-check", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.build-power-bi-report" - ] - }, - { - "uid": "learn-dynamics.business-central-interfaces.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/1-introduction", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.interfaces" - ] - }, - { - "uid": "learn-dynamics.business-central-interfaces.2-snippet-support", - "title": "Snippet support", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/2-snippet-support", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.interfaces" - ] - }, - { - "uid": "learn-dynamics.business-central-interfaces.3-why-interfaces", - "title": "Importance of interfaces", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/3-why-interfaces", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.interfaces" - ] - }, - { - "uid": "learn-dynamics.business-central-interfaces.4-exercise", - "title": "Exercise - Create an interface", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/4-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.interfaces" - ] - }, - { - "uid": "learn-dynamics.business-central-interfaces.5-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/5-check", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.interfaces" - ] - }, - { - "uid": "learn-dynamics.business-central-interfaces.6-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/6-summary", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.interfaces" - ] - }, - { - "uid": "learn-dynamics.business-central-interfaces.extending-interfaces", - "title": "Extending interfaces in AL", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/extending-interfaces", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.interfaces" - ] - }, - { - "uid": "learn-dynamics.business-central-interfaces.type-testing", - "title": "Understand type testing and casting operators for interfaces", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/type-testing", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.interfaces" - ] - }, - { - "uid": "learn-dynamics.business-central-teams-integration.check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.business-central-teams-integration" - ] - }, - { - "uid": "learn-dynamics.business-central-teams-integration.create-tabs", - "title": "Create Business Central tabs for Teams", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/create-tabs", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.business-central-teams-integration" - ] - }, - { - "uid": "learn-dynamics.business-central-teams-integration.extend-teams-cards", - "title": "Extend Teams cards", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/extend-teams-cards", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 18, - "moduleUids": [ - "learn-dynamics.business-central-teams-integration" - ] - }, - { - "uid": "learn-dynamics.business-central-teams-integration.get-session-details", - "title": "Get relevant session and environment details for Teams integration", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/get-session-details", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.business-central-teams-integration" - ] - }, - { - "uid": "learn-dynamics.business-central-teams-integration.introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.business-central-teams-integration" - ] - }, - { - "uid": "learn-dynamics.business-central-teams-integration.manage-install-use", - "title": "Manage, install, and use the Business Central App for Teams", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/manage-install-use", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.business-central-teams-integration" - ] - }, - { - "uid": "learn-dynamics.business-central-teams-integration.summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/business-central-teams-integration/summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.business-central-teams-integration" - ] - }, - { - "uid": "learn-dynamics.complete-report-request-page.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.complete-report-request-page" - ] - }, - { - "uid": "learn-dynamics.complete-report-request-page.2-design-request-page", - "title": "Design the request page", - "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page/2-design-request-page", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.complete-report-request-page" - ] - }, - { - "uid": "learn-dynamics.complete-report-request-page.3-design-request-options", - "title": "Design the request options page", - "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page/3-design-request-options", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.complete-report-request-page" - ] - }, - { - "uid": "learn-dynamics.complete-report-request-page.4-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page/4-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.complete-report-request-page" - ] - }, - { - "uid": "learn-dynamics.complete-report-request-page.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/complete-report-request-page/5-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.complete-report-request-page" - ] - }, - { - "uid": "learn-dynamics.create-canvas-app.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/1-introduction", - "lastModified": "2026-08-03T17:47:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.create-canvas-app" - ] - }, - { - "uid": "learn-dynamics.create-canvas-app.2-data-app", - "title": "Build an app using data", - "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/2-data-app", - "lastModified": "2026-08-03T17:47:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.create-canvas-app" - ] - }, - { - "uid": "learn-dynamics.create-canvas-app.3-blank-app", - "title": "Build an app from blank", - "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/3-blank-app", - "lastModified": "2026-08-03T17:47:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.create-canvas-app" - ] - }, - { - "uid": "learn-dynamics.create-canvas-app.4-customizations", - "title": "Make basic customizations in an app", - "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/4-customizations", - "lastModified": "2026-08-03T17:47:00Z", - "durationInMinutes": 14, - "moduleUids": [ - "learn-dynamics.create-canvas-app" - ] - }, - { - "uid": "learn-dynamics.create-canvas-app.5-save-publish-share", - "title": "Save, publish, and share an app", - "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/5-save-publish-share", - "lastModified": "2026-08-03T17:47:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.create-canvas-app" - ] - }, - { - "uid": "learn-dynamics.create-canvas-app.6-embed", - "title": "Embed an app", - "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/6-embed", - "lastModified": "2026-08-03T17:47:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.create-canvas-app" - ] - }, - { - "uid": "learn-dynamics.create-canvas-app.7-exercise", - "title": "Exercise - Create the customer financial details app in Power Apps", - "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/7-exercise", - "lastModified": "2026-08-03T17:47:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.create-canvas-app" - ] - }, - { - "uid": "learn-dynamics.create-canvas-app.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/8-check", - "lastModified": "2026-08-03T17:47:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.create-canvas-app" - ] - }, - { - "uid": "learn-dynamics.create-canvas-app.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/create-canvas-app/9-summary", - "lastModified": "2026-08-03T17:47:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.create-canvas-app" - ] - }, - { - "uid": "learn-dynamics.create-custom-connector-power-platform.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/1-introduction", - "lastModified": "2026-08-05T23:09:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.create-custom-connector-power-platform" - ] - }, - { - "uid": "learn-dynamics.create-custom-connector-power-platform.2-custom-connector", - "title": "Create a custom connector for Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/2-custom-connector", - "lastModified": "2026-08-05T23:09:00Z", - "durationInMinutes": 25, - "moduleUids": [ - "learn-dynamics.create-custom-connector-power-platform" - ] - }, - { - "uid": "learn-dynamics.create-custom-connector-power-platform.3-power-apps", - "title": "Use Business Central custom connector from Power Apps", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/3-power-apps", - "lastModified": "2026-08-05T23:09:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.create-custom-connector-power-platform" - ] - }, - { - "uid": "learn-dynamics.create-custom-connector-power-platform.4-power-automate", - "title": "Use a Business Central custom connector in Power Automate", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/4-power-automate", - "lastModified": "2026-08-05T23:09:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.create-custom-connector-power-platform" - ] - }, - { - "uid": "learn-dynamics.create-custom-connector-power-platform.5-share-custom-connectors", - "title": "Share Business Central custom connector across the organization", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/5-share-custom-connectors", - "lastModified": "2026-08-05T23:09:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.create-custom-connector-power-platform" - ] - }, - { - "uid": "learn-dynamics.create-custom-connector-power-platform.6-exercise", - "title": "Exercise - Create a testing process", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/6-exercise", - "lastModified": "2026-08-05T23:09:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.create-custom-connector-power-platform" - ] - }, - { - "uid": "learn-dynamics.create-custom-connector-power-platform.7-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/7-check", - "lastModified": "2026-08-05T23:09:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.create-custom-connector-power-platform" - ] - }, - { - "uid": "learn-dynamics.create-custom-connector-power-platform.8-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-connector-power-platform/8-summary", - "lastModified": "2026-08-05T23:09:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.create-custom-connector-power-platform" - ] - }, - { - "uid": "learn-dynamics.create-custom-functions.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.create-custom-functions" - ] - }, - { - "uid": "learn-dynamics.create-custom-functions.2-custom-function", - "title": "Custom procedures", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/2-custom-function", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.create-custom-functions" - ] - }, - { - "uid": "learn-dynamics.create-custom-functions.3-local-global-function", - "title": "Identify differences between local and global procedures", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/3-local-global-function", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.create-custom-functions" - ] - }, - { - "uid": "learn-dynamics.create-custom-functions.4-local-global-variable", - "title": "Identify differences between local and global variables", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/4-local-global-variable", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.create-custom-functions" - ] - }, - { - "uid": "learn-dynamics.create-custom-functions.5-pass", - "title": "Pass into a procedure", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/5-pass", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.create-custom-functions" - ] - }, - { - "uid": "learn-dynamics.create-custom-functions.6-parameter", - "title": "Identify differences between a parameter by value and by reference", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/6-parameter", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.create-custom-functions" - ] - }, - { - "uid": "learn-dynamics.create-custom-functions.7-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/7-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.create-custom-functions" - ] - }, - { - "uid": "learn-dynamics.create-custom-functions.8-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/create-custom-functions/8-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.create-custom-functions" - ] - }, - { - "uid": "learn-dynamics.create-data-set.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/1-introduction", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.10-exercise", - "title": "Exercise - Import data into Power BI Desktop", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/10-exercise", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.11-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/11-check", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.12-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/12-summary", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.2-install", - "title": "Install Power BI Desktop", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/2-install", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.3-use-data-source", - "title": "Use Business Central as a data source in Power BI", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/3-use-data-source", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.5-develop-query-page-objects", - "title": "Develop query and page objects for Power BI", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/5-develop-query-page-objects", - "lastModified": "2025-08-20T23:10:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.6-mind-performance-impact", - "title": "Weigh the performance impact", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/6-mind-performance-impact", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.7-exercise", - "title": "Exercise - Create a dataset for a Power BI report in Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/7-exercise", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.9-exercise", - "title": "Exercise - Publish and install the extension", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/9-exercise", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.create-power-bi-dataflow", - "title": "Create a Power BI dataflow with Business Central data", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/create-power-bi-dataflow", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.develop-api-query", - "title": "Develop API query and API page objects for Power BI", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/develop-api-query", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.create-data-set.use-dedicated-apis", - "title": "Use new dedicated APIs for financial reporting", - "url": "https://learn.microsoft.com/en-us/training/modules/create-data-set/use-dedicated-apis", - "lastModified": "2025-06-05T17:09:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.create-data-set" - ] - }, - { - "uid": "learn-dynamics.customize-ui-experience.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.customize-ui-experience" - ] - }, - { - "uid": "learn-dynamics.customize-ui-experience.2-objects", - "title": "Add objects to search", - "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/2-objects", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.customize-ui-experience" - ] - }, - { - "uid": "learn-dynamics.customize-ui-experience.3-customizations", - "title": "Create page customizations", - "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/3-customizations", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.customize-ui-experience" - ] - }, - { - "uid": "learn-dynamics.customize-ui-experience.4-profiles", - "title": "Create profiles in application language", - "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/4-profiles", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.customize-ui-experience" - ] - }, - { - "uid": "learn-dynamics.customize-ui-experience.5-views", - "title": "Use and create views in application language", - "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/5-views", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.customize-ui-experience" - ] - }, - { - "uid": "learn-dynamics.customize-ui-experience.6-application-areas", - "title": "Extend application areas", - "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/6-application-areas", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.customize-ui-experience" - ] - }, - { - "uid": "learn-dynamics.customize-ui-experience.7-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/7-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.customize-ui-experience" - ] - }, - { - "uid": "learn-dynamics.customize-ui-experience.8-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/8-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.customize-ui-experience" - ] - }, - { - "uid": "learn-dynamics.customize-ui-experience.extend-customize", - "title": "Extend and customize profiles", - "url": "https://learn.microsoft.com/en-us/training/modules/customize-ui-experience/extend-customize", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.customize-ui-experience" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.2-debug", - "title": "Debug your extension with Visual Studio Code", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/2-debug", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 18, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.3-attach", - "title": "Use attach and debug with attach configurations", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/3-attach", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.3a-snapshot", - "title": "Implement snapshot debugging", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/3a-snapshot", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.4-rapid-application-development", - "title": "Work with Rapid Application Development", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/4-rapid-application-development", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.5-publish", - "title": "Publish and install an extension", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/5-publish", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.5a-performance-toolkit", - "title": "Work with the Business Central Performance Toolkit", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/5a-performance-toolkit", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.6-exercise", - "title": "Exercise - Create a table extension", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/6-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.7-exercise", - "title": "Exercise - Create a page extension", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/7-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/8-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/9-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.code-actions", - "title": "Use AL code actions", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/code-actions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.database-wait-statistics", - "title": "Use Database Wait Statistics", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/database-wait-statistics", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.generate-help", - "title": "Generate help with the ALDoc tool", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/generate-help", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.projects-workspaces", - "title": "Configure projects and workspaces", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/projects-workspaces", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.protect-sensitive-values", - "title": "Protect sensitive values", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/protect-sensitive-values", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.recovery-failures", - "title": "Recovery from publishing failures", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/recovery-failures", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.debug-deploy-extension.resource-policy-settings", - "title": "Set Resource Exposure Policy settings", - "url": "https://learn.microsoft.com/en-us/training/modules/debug-deploy-extension/resource-policy-settings", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.debug-deploy-extension" - ] - }, - { - "uid": "learn-dynamics.design-data-model-report.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.design-data-model-report" - ] - }, - { - "uid": "learn-dynamics.design-data-model-report.2-create-report-data-items", - "title": "Create report data items and their properties", - "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/2-create-report-data-items", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 18, - "moduleUids": [ - "learn-dynamics.design-data-model-report" - ] - }, - { - "uid": "learn-dynamics.design-data-model-report.3-add-columns-report-dataset", - "title": "Add columns to a report dataset", - "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/3-add-columns-report-dataset", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.design-data-model-report" - ] - }, - { - "uid": "learn-dynamics.design-data-model-report.4-link-order-indent-data-items", - "title": "Link, order, and indent data items", - "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/4-link-order-indent-data-items", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.design-data-model-report" - ] - }, - { - "uid": "learn-dynamics.design-data-model-report.5-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/5-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.design-data-model-report" - ] - }, - { - "uid": "learn-dynamics.design-data-model-report.6-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/6-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.design-data-model-report" - ] - }, - { - "uid": "learn-dynamics.design-data-model-report.use-query", - "title": "Use a query to create a report dataset", - "url": "https://learn.microsoft.com/en-us/training/modules/design-data-model-report/use-query", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.design-data-model-report" - ] - }, - { - "uid": "learn-dynamics.develop-role-center.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.develop-role-center" - ] - }, - { - "uid": "learn-dynamics.develop-role-center.2-create-page", - "title": "Create a Role Center page", - "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/2-create-page", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.develop-role-center" - ] - }, - { - "uid": "learn-dynamics.develop-role-center.3-activity-parts", - "title": "Create activity page parts", - "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/3-activity-parts", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.develop-role-center" - ] - }, - { - "uid": "learn-dynamics.develop-role-center.4-cue-setup", - "title": "Integrate with the Cue Setup", - "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/4-cue-setup", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.develop-role-center" - ] - }, - { - "uid": "learn-dynamics.develop-role-center.5-role-center-lists", - "title": "Create Role Center lists", - "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/5-role-center-lists", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.develop-role-center" - ] - }, - { - "uid": "learn-dynamics.develop-role-center.6-headline-parts", - "title": "Implement headline parts", - "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/6-headline-parts", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.develop-role-center" - ] - }, - { - "uid": "learn-dynamics.develop-role-center.7-exercise", - "title": "Exercise - Create a Role Center page with an Activity page", - "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/7-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.develop-role-center" - ] - }, - { - "uid": "learn-dynamics.develop-role-center.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/8-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.develop-role-center" - ] - }, - { - "uid": "learn-dynamics.develop-role-center.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/develop-role-center/9-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.develop-role-center" - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.easy-application-upgrade" - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.2-upgrade-responsibilities", - "title": "Understand upgrade responsibilities and best practices", - "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/2-upgrade-responsibilities", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.easy-application-upgrade" - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.3-installation-upgrade-codeunits", - "title": "Create proper installation and upgrade codeunits", - "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/3-installation-upgrade-codeunits", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 25, - "moduleUids": [ - "learn-dynamics.easy-application-upgrade" - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.4-manage-apps", - "title": "Manage apps", - "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/4-manage-apps", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.easy-application-upgrade" - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.5-detect-breaking-changes", - "title": "Detect and avoid breaking changes", - "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/5-detect-breaking-changes", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.easy-application-upgrade" - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.6-answers-about-updates", - "title": "Frequently asked questions about updating your Business Central App", - "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/6-answers-about-updates", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.easy-application-upgrade" - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.7-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/7-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.easy-application-upgrade" - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.8-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/8-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.easy-application-upgrade" - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.consider-update-lifecycle", - "title": "Consider the update lifecycle", - "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/consider-update-lifecycle", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.easy-application-upgrade" - ] - }, - { - "uid": "learn-dynamics.easy-application-upgrade.transfer-data", - "title": "Transfer data between tables using DataTransfer", - "url": "https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/transfer-data", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.easy-application-upgrade" - ] - }, - { - "uid": "learn-dynamics.events-triggers.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.events-triggers" - ] - }, - { - "uid": "learn-dynamics.events-triggers.2-table-field-triggers", - "title": "Identify different table and field triggers", - "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/2-table-field-triggers", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.events-triggers" - ] - }, - { - "uid": "learn-dynamics.events-triggers.3-page-control-action-triggers", - "title": "Identify different page, control, and action triggers", - "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/3-page-control-action-triggers", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.events-triggers" - ] - }, - { - "uid": "learn-dynamics.events-triggers.4-event-based", - "title": "Event-based architecture", - "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/4-event-based", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.events-triggers" - ] - }, - { - "uid": "learn-dynamics.events-triggers.5-publish-subscribe", - "title": "Publish and subscribe to events", - "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/5-publish-subscribe", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.events-triggers" - ] - }, - { - "uid": "learn-dynamics.events-triggers.6-exercise", - "title": "Exercise - Events and triggers", - "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/6-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.events-triggers" - ] - }, - { - "uid": "learn-dynamics.events-triggers.7-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/7-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.events-triggers" - ] - }, - { - "uid": "learn-dynamics.events-triggers.8-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/events-triggers/8-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.events-triggers" - ] - }, - { - "uid": "learn-dynamics.extend-functionality-using-events.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/1-introduction", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.extend-functionality-using-events" - ] - }, - { - "uid": "learn-dynamics.extend-functionality-using-events.2-programming-basics", - "title": "Learn the basics of AL programming language", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/2-programming-basics", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.extend-functionality-using-events" - ] - }, - { - "uid": "learn-dynamics.extend-functionality-using-events.3-customer-rewards", - "title": "Create the Customer Rewards Extension Management codeunit object", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/3-customer-rewards", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.extend-functionality-using-events" - ] - }, - { - "uid": "learn-dynamics.extend-functionality-using-events.4-exercise", - "title": "Exercise - Create the Customer Rewards Extension Management codeunit object", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/4-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.extend-functionality-using-events" - ] - }, - { - "uid": "learn-dynamics.extend-functionality-using-events.5-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/5-check", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.extend-functionality-using-events" - ] - }, - { - "uid": "learn-dynamics.extend-functionality-using-events.6-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-functionality-using-events/6-summary", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.extend-functionality-using-events" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-pages.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/1-introduction", - "lastModified": "2026-09-01T17:24:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-pages" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-pages.2-get-started", - "title": "Get started with page extensions", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/2-get-started", - "lastModified": "2026-09-01T17:24:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-pages" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-pages.3-properties", - "title": "Compare page extension properties with page properties", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/3-properties", - "lastModified": "2026-09-01T17:24:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-pages" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-pages.4-layout", - "title": "Personalize pages", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/4-layout", - "lastModified": "2026-09-01T17:24:00Z", - "durationInMinutes": 20, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-pages" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-pages.5-check", - "title": "Module assessment", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/5-check", - "lastModified": "2026-09-01T17:24:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-pages" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-pages.6-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-pages/6-summary", - "lastModified": "2026-09-01T17:24:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-pages" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-table.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/1-introduction", - "lastModified": "2026-08-17T23:09:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-table" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-table.2-get-started", - "title": "Get started with table extensions", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/2-get-started", - "lastModified": "2026-08-17T23:09:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-table" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-table.3-properties", - "title": "Compare table extension properties with table properties", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/3-properties", - "lastModified": "2026-08-17T23:09:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-table" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-table.4-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/4-check", - "lastModified": "2026-08-17T23:09:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-table" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-table.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/5-summary", - "lastModified": "2026-08-17T23:09:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-table" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-table.add-field-group", - "title": "Add a new FieldGroup to an existing table", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/add-field-group", - "lastModified": "2026-08-17T23:09:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-table" - ] - }, - { - "uid": "learn-dynamics.extend-modify-existing-table.define-extension-objects", - "title": "Define extension objects in same app as their base object", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/define-extension-objects", - "lastModified": "2026-08-17T23:09:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.extend-modify-existing-table" - ] - }, - { - "uid": "learn-dynamics.extend-tables-page-objects.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/1-introduction", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.extend-tables-page-objects" - ] - }, - { - "uid": "learn-dynamics.extend-tables-page-objects.2-customer-table", - "title": "Create the customer table extension object", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/2-customer-table", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.extend-tables-page-objects" - ] - }, - { - "uid": "learn-dynamics.extend-tables-page-objects.3-customer-card", - "title": "Create the customer card page extension object", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/3-customer-card", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.extend-tables-page-objects" - ] - }, - { - "uid": "learn-dynamics.extend-tables-page-objects.4-customer-list", - "title": "Create the customer list page extension object", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/4-customer-list", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.extend-tables-page-objects" - ] - }, - { - "uid": "learn-dynamics.extend-tables-page-objects.5-exercise", - "title": "Exercise - Create the Customer table extension object", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/5-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.extend-tables-page-objects" - ] - }, - { - "uid": "learn-dynamics.extend-tables-page-objects.6-exercise", - "title": "Exercise - Create the Customer Card page extension object", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/6-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.extend-tables-page-objects" - ] - }, - { - "uid": "learn-dynamics.extend-tables-page-objects.7-exercise", - "title": "Exercise - Create the Customer List page extension object", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/7-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.extend-tables-page-objects" - ] - }, - { - "uid": "learn-dynamics.extend-tables-page-objects.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/8-check", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.extend-tables-page-objects" - ] - }, - { - "uid": "learn-dynamics.extend-tables-page-objects.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/extend-tables-page-objects/9-summary", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.extend-tables-page-objects" - ] - }, - { - "uid": "learn-dynamics.handle-errors.check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.handle-errors" - ] - }, - { - "uid": "learn-dynamics.handle-errors.collect-errors", - "title": "Collect errors", - "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/collect-errors", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.handle-errors" - ] - }, - { - "uid": "learn-dynamics.handle-errors.handle-errors", - "title": "Handle errors by using try methods", - "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/handle-errors", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.handle-errors" - ] - }, - { - "uid": "learn-dynamics.handle-errors.introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.handle-errors" - ] - }, - { - "uid": "learn-dynamics.handle-errors.progress-windows", - "title": "Progress windows and the message, error, and confirm methods", - "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/progress-windows", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.handle-errors" - ] - }, - { - "uid": "learn-dynamics.handle-errors.summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/handle-errors/summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 1, - "moduleUids": [ - "learn-dynamics.handle-errors" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.10-create-setup-tables", - "title": "Create setup tables", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/10-create-setup-tables", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.11-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/11-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.12-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/12-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.2-define-table-types", - "title": "Define the different functional table types and characteristics", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/2-define-table-types", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.3-create-master-tables", - "title": "Create master tables", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/3-create-master-tables", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.4-create-supplemental-tables", - "title": "Create supplemental tables", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/4-create-supplemental-tables", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.5-create-subsidiary-tables", - "title": "Create subsidiary tables", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/5-create-subsidiary-tables", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.6-create-ledger-tables", - "title": "Create ledger tables", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/6-create-ledger-tables", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.7-create-register-tables", - "title": "Create register tables", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/7-create-register-tables", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.8-create-journal-tables", - "title": "Create journal tables", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/8-create-journal-tables", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.identify-table-types.9-create-document-tables", - "title": "Create document tables", - "url": "https://learn.microsoft.com/en-us/training/modules/identify-table-types/9-create-document-tables", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.identify-table-types" - ] - }, - { - "uid": "learn-dynamics.implement-data-process-model-business-central.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.implement-data-process-model-business-central" - ] - }, - { - "uid": "learn-dynamics.implement-data-process-model-business-central.2-standard-data-model", - "title": "Work with the standard data model", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/2-standard-data-model", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.implement-data-process-model-business-central" - ] - }, - { - "uid": "learn-dynamics.implement-data-process-model-business-central.3-typical-data-triggers", - "title": "Use typical data triggers", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/3-typical-data-triggers", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.implement-data-process-model-business-central" - ] - }, - { - "uid": "learn-dynamics.implement-data-process-model-business-central.4-standard-data-flow", - "title": "Adopt the standard data flow", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/4-standard-data-flow", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.implement-data-process-model-business-central" - ] - }, - { - "uid": "learn-dynamics.implement-data-process-model-business-central.5-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/5-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.implement-data-process-model-business-central" - ] - }, - { - "uid": "learn-dynamics.implement-data-process-model-business-central.6-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-data-process-model-business-central/6-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.implement-data-process-model-business-central" - ] - }, - { - "uid": "learn-dynamics.implement-multilanguage-reports.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.implement-multilanguage-reports" - ] - }, - { - "uid": "learn-dynamics.implement-multilanguage-reports.2-create-report-user-language", - "title": "Create a report in the language of the user", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/2-create-report-user-language", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 11, - "moduleUids": [ - "learn-dynamics.implement-multilanguage-reports" - ] - }, - { - "uid": "learn-dynamics.implement-multilanguage-reports.3-create-report-recipient-language", - "title": "Create a report in the language of the recipient", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/3-create-report-recipient-language", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.implement-multilanguage-reports" - ] - }, - { - "uid": "learn-dynamics.implement-multilanguage-reports.4-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/4-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.implement-multilanguage-reports" - ] - }, - { - "uid": "learn-dynamics.implement-multilanguage-reports.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/5-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.implement-multilanguage-reports" - ] - }, - { - "uid": "learn-dynamics.implement-multilanguage-reports.define-regional-settings", - "title": "Define regional settings in reports", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-multilanguage-reports/define-regional-settings", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.implement-multilanguage-reports" - ] - }, - { - "uid": "learn-dynamics.implement-saas-techniques.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques/1-introduction", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.implement-saas-techniques" - ] - }, - { - "uid": "learn-dynamics.implement-saas-techniques.2-build-trial", - "title": "Build a first trial experience", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques/2-build-trial", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 11, - "moduleUids": [ - "learn-dynamics.implement-saas-techniques" - ] - }, - { - "uid": "learn-dynamics.implement-saas-techniques.3-implement-saasification", - "title": "Implement SaaSification techniques when building your user interface", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques/3-implement-saasification", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.implement-saas-techniques" - ] - }, - { - "uid": "learn-dynamics.implement-saas-techniques.4-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques/4-check", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.implement-saas-techniques" - ] - }, - { - "uid": "learn-dynamics.implement-saas-techniques.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/implement-saas-techniques/5-summary", - "lastModified": "2026-08-27T17:36:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.implement-saas-techniques" - ] - }, - { - "uid": "learn-dynamics.initialize-data-extension-installation.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation/1-introduction", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.initialize-data-extension-installation" - ] - }, - { - "uid": "learn-dynamics.initialize-data-extension-installation.2-install-logic", - "title": "Create the Customer Rewards Install Logic codeunit object", - "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation/2-install-logic", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.initialize-data-extension-installation" - ] - }, - { - "uid": "learn-dynamics.initialize-data-extension-installation.3-exercise", - "title": "Exercise - Create the Customer Rewards Install Logic codeunit object", - "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation/3-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.initialize-data-extension-installation" - ] - }, - { - "uid": "learn-dynamics.initialize-data-extension-installation.4-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation/4-check", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.initialize-data-extension-installation" - ] - }, - { - "uid": "learn-dynamics.initialize-data-extension-installation.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/initialize-data-extension-installation/5-summary", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.initialize-data-extension-installation" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.1-intro", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/1-intro", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.10-exercise", - "title": "Exercise - Use logical and relational expressions", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/10-exercise", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.11-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/11-check", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.12-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/12-summary", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.2-get-started", - "title": "Get started with application language", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/2-get-started", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.3-variables", - "title": "Work with identifiers, syntax, and variables", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/3-variables", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.4-data-types", - "title": "Discover intrinsic data types", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/4-data-types", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.5-options-enums", - "title": "Options and enums", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/5-options-enums", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.6-collections", - "title": "Work with collections", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/6-collections", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.7-assignments", - "title": "Use assignments and type conversions", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/7-assignments", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.8-expressions", - "title": "Define different expression types", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/8-expressions", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-basics-al-programming.9-exercise", - "title": "Exercise - Discover the intrinsic data types", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-basics-al-programming/9-exercise", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.intro-basics-al-programming" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.1-intro", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/1-intro", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.10-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/10-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.11-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/11-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.2-get-started", - "title": "Get started with Visual Studio Code", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/2-get-started", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.3-install", - "title": "Install the AL language extension in Visual Studio Code", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/3-install", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.4-logical-database", - "title": "Discover the logical database and its objects", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/4-logical-database", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.5-extension", - "title": "Create a new Business Central extension in Visual Studio Code", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/5-extension", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.6-json", - "title": "Work with JSON configuration files", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/6-json", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.7-al-language", - "title": "Configure the AL language extension", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/7-al-language", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.8-multiple", - "title": "Manage multiple AL extensions in one workspace", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/8-multiple", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.9-exercise", - "title": "Exercise - Create and run an AL extension", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/9-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.differentiate-apps", - "title": "Differentiate the base app and the system app", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/differentiate-apps", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.open-visual-studio-code-web-client", - "title": "Open Visual Studio Code from the web client", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/open-visual-studio-code-web-client", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.intro-development-environment.segment-code-reduce-conflicts", - "title": "Segment AL code and reduce naming conflicts with namespaces", - "url": "https://learn.microsoft.com/en-us/training/modules/intro-development-environment/segment-code-reduce-conflicts", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.intro-development-environment" - ] - }, - { - "uid": "learn-dynamics.introduction-basics-file-management.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/1-introduction", - "lastModified": "2026-08-18T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.introduction-basics-file-management" - ] - }, - { - "uid": "learn-dynamics.introduction-basics-file-management.2-file-object", - "title": "Work with the File object", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/2-file-object", - "lastModified": "2026-08-18T17:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.introduction-basics-file-management" - ] - }, - { - "uid": "learn-dynamics.introduction-basics-file-management.3-temp-blob-object", - "title": "Work with the Temp Blob object", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/3-temp-blob-object", - "lastModified": "2026-08-18T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.introduction-basics-file-management" - ] - }, - { - "uid": "learn-dynamics.introduction-basics-file-management.4-read-write-stream", - "title": "Read and write files by using streaming in Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/4-read-write-stream", - "lastModified": "2026-08-18T17:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.introduction-basics-file-management" - ] - }, - { - "uid": "learn-dynamics.introduction-basics-file-management.5-exercise", - "title": "Exercise - Read and write files", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/5-exercise", - "lastModified": "2026-08-18T17:07:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.introduction-basics-file-management" - ] - }, - { - "uid": "learn-dynamics.introduction-basics-file-management.6-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/6-check", - "lastModified": "2026-08-18T17:07:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.introduction-basics-file-management" - ] - }, - { - "uid": "learn-dynamics.introduction-basics-file-management.7-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/7-summary", - "lastModified": "2026-08-18T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.introduction-basics-file-management" - ] - }, - { - "uid": "learn-dynamics.introduction-basics-file-management.uploads-drop-zones", - "title": "Handle multiple file uploads and file drop zones", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-basics-file-management/uploads-drop-zones", - "lastModified": "2026-08-18T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.introduction-basics-file-management" - ] - }, - { - "uid": "learn-dynamics.introduction-development-process.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/1-introduction", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.introduction-development-process" - ] - }, - { - "uid": "learn-dynamics.introduction-development-process.2-process", - "title": "Introduction to the development process", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/2-process", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.introduction-development-process" - ] - }, - { - "uid": "learn-dynamics.introduction-development-process.3-setup", - "title": "Set up the development environment", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/3-setup", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.introduction-development-process" - ] - }, - { - "uid": "learn-dynamics.introduction-development-process.4-rewards", - "title": "Build a customer rewards extension", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/4-rewards", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.introduction-development-process" - ] - }, - { - "uid": "learn-dynamics.introduction-development-process.5-exercise", - "title": "Exercise - Set up the development environment", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/5-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.introduction-development-process" - ] - }, - { - "uid": "learn-dynamics.introduction-development-process.6-exercise", - "title": "Exercise - Create the reward level table object", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/6-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.introduction-development-process" - ] - }, - { - "uid": "learn-dynamics.introduction-development-process.7-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/7-check", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.introduction-development-process" - ] - }, - { - "uid": "learn-dynamics.introduction-development-process.8-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-development-process/8-summary", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.introduction-development-process" - ] - }, - { - "uid": "learn-dynamics.introduction-power-platform-business-central.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.introduction-power-platform-business-central" - ] - }, - { - "uid": "learn-dynamics.introduction-power-platform-business-central.2-data-connectors", - "title": "Explore data connectors", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/2-data-connectors", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.introduction-power-platform-business-central" - ] - }, - { - "uid": "learn-dynamics.introduction-power-platform-business-central.3-business-central-connectors", - "title": "Explore Business Central connectors", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/3-business-central-connectors", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.introduction-power-platform-business-central" - ] - }, - { - "uid": "learn-dynamics.introduction-power-platform-business-central.4-power-apps", - "title": "Get started with Power Apps", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/4-power-apps", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.introduction-power-platform-business-central" - ] - }, - { - "uid": "learn-dynamics.introduction-power-platform-business-central.5-power-automate", - "title": "Get started with Power Automate", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/5-power-automate", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.introduction-power-platform-business-central" - ] - }, - { - "uid": "learn-dynamics.introduction-power-platform-business-central.6-embed-power-platform", - "title": "Embed Microsoft Power Platform in Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/6-embed-power-platform", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.introduction-power-platform-business-central" - ] - }, - { - "uid": "learn-dynamics.introduction-power-platform-business-central.7-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/7-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.introduction-power-platform-business-central" - ] - }, - { - "uid": "learn-dynamics.introduction-power-platform-business-central.8-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/8-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.introduction-power-platform-business-central" - ] - }, - { - "uid": "learn-dynamics.introduction-power-platform-business-central.use-cases", - "title": "Power Platform integration use cases", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-power-platform-business-central/use-cases", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.introduction-power-platform-business-central" - ] - }, - { - "uid": "learn-dynamics.introduction-report-components.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.introduction-report-components" - ] - }, - { - "uid": "learn-dynamics.introduction-report-components.2-learn-overview-report-description", - "title": "Report descriptions", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/2-learn-overview-report-description", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.introduction-report-components" - ] - }, - { - "uid": "learn-dynamics.introduction-report-components.3-report-properties-triggers", - "title": "Report properties and triggers", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/3-report-properties-triggers", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.introduction-report-components" - ] - }, - { - "uid": "learn-dynamics.introduction-report-components.4-data-items-columns", - "title": "Data items and columns", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/4-data-items-columns", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.introduction-report-components" - ] - }, - { - "uid": "learn-dynamics.introduction-report-components.5-request-page", - "title": "Request page", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/5-request-page", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.introduction-report-components" - ] - }, - { - "uid": "learn-dynamics.introduction-report-components.6-work-report-labels", - "title": "Work with report labels", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/6-work-report-labels", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.introduction-report-components" - ] - }, - { - "uid": "learn-dynamics.introduction-report-components.7-report-layouts", - "title": "Report layouts", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/7-report-layouts", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.introduction-report-components" - ] - }, - { - "uid": "learn-dynamics.introduction-report-components.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/8-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.introduction-report-components" - ] - }, - { - "uid": "learn-dynamics.introduction-report-components.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-report-components/9-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.introduction-report-components" - ] - }, - { - "uid": "learn-dynamics.introduction-test-automation.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/1-introduction", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.introduction-test-automation" - ] - }, - { - "uid": "learn-dynamics.introduction-test-automation.2-test-applications", - "title": "Test applications in Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/2-test-applications", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 18, - "moduleUids": [ - "learn-dynamics.introduction-test-automation" - ] - }, - { - "uid": "learn-dynamics.introduction-test-automation.3-install-run-test-toolkit", - "title": "Install and run the Test Toolkit", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/3-install-run-test-toolkit", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.introduction-test-automation" - ] - }, - { - "uid": "learn-dynamics.introduction-test-automation.4-run-standard-tests", - "title": "Run the standard tests", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/4-run-standard-tests", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.introduction-test-automation" - ] - }, - { - "uid": "learn-dynamics.introduction-test-automation.5-create-test-code-units", - "title": "Create your own test codeunits with a test app", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/5-create-test-code-units", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.introduction-test-automation" - ] - }, - { - "uid": "learn-dynamics.introduction-test-automation.6-check", - "title": "Module assessment", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/6-check", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.introduction-test-automation" - ] - }, - { - "uid": "learn-dynamics.introduction-test-automation.7-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/introduction-test-automation/7-summary", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.introduction-test-automation" - ] - }, - { - "uid": "learn-dynamics.manage-multilanguage-development.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.manage-multilanguage-development" - ] - }, - { - "uid": "learn-dynamics.manage-multilanguage-development.2-translation-files", - "title": "Create and use translation files", - "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/2-translation-files", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.manage-multilanguage-development" - ] - }, - { - "uid": "learn-dynamics.manage-multilanguage-development.3-automate", - "title": "Use Azure Translator Text to automate translation", - "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/3-automate", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.manage-multilanguage-development" - ] - }, - { - "uid": "learn-dynamics.manage-multilanguage-development.4-exercise", - "title": "Exercise - Multilanguage development", - "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/4-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.manage-multilanguage-development" - ] - }, - { - "uid": "learn-dynamics.manage-multilanguage-development.5-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/5-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.manage-multilanguage-development" - ] - }, - { - "uid": "learn-dynamics.manage-multilanguage-development.6-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/manage-multilanguage-development/6-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.manage-multilanguage-development" - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.manipulate-data-via-code" - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code.2-retrieve-data", - "title": "Retrieve data from the database with AL statements", - "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/2-retrieve-data", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.manipulate-data-via-code" - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code.3-sort-filter", - "title": "Sort and filter data in code", - "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/3-sort-filter", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.manipulate-data-via-code" - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code.4-add-modify-delete", - "title": "Add, modify, or delete data with AL", - "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/4-add-modify-delete", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.manipulate-data-via-code" - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code.5-field-functions", - "title": "Use different field functions in AL", - "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/5-field-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.manipulate-data-via-code" - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code.6-exercise", - "title": "Exercise - Use data manipulation statements", - "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/6-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.manipulate-data-via-code" - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code.7-exercise", - "title": "Exercise - Custom functions", - "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/7-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.manipulate-data-via-code" - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/8-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.manipulate-data-via-code" - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/9-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.manipulate-data-via-code" - ] - }, - { - "uid": "learn-dynamics.manipulate-data-via-code.control-database-locking-behavior", - "title": "Control database locking behavior", - "url": "https://learn.microsoft.com/en-us/training/modules/manipulate-data-via-code/control-database-locking-behavior", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.manipulate-data-via-code" - ] - }, - { - "uid": "learn-dynamics.report-extensibility.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.report-extensibility" - ] - }, - { - "uid": "learn-dynamics.report-extensibility.2-anatomy", - "title": "Anatomy of a report extension", - "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility/2-anatomy", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 20, - "moduleUids": [ - "learn-dynamics.report-extensibility" - ] - }, - { - "uid": "learn-dynamics.report-extensibility.3-report-extension-example", - "title": "Examples of report extensions", - "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility/3-report-extension-example", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 24, - "moduleUids": [ - "learn-dynamics.report-extensibility" - ] - }, - { - "uid": "learn-dynamics.report-extensibility.4-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility/4-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.report-extensibility" - ] - }, - { - "uid": "learn-dynamics.report-extensibility.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/report-extensibility/5-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.report-extensibility" - ] - }, - { - "uid": "learn-dynamics.send-notifications.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.send-notifications" - ] - }, - { - "uid": "learn-dynamics.send-notifications.2-notifications", - "title": "Work with notifications", - "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/2-notifications", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.send-notifications" - ] - }, - { - "uid": "learn-dynamics.send-notifications.3-create-send", - "title": "Create and send notifications", - "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/3-create-send", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.send-notifications" - ] - }, - { - "uid": "learn-dynamics.send-notifications.4-exercise", - "title": "Exercise - Create a notification", - "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/4-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.send-notifications" - ] - }, - { - "uid": "learn-dynamics.send-notifications.5-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/5-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.send-notifications" - ] - }, - { - "uid": "learn-dynamics.send-notifications.6-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/send-notifications/6-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.send-notifications" - ] - }, - { - "uid": "learn-dynamics.set-up-onboarding-experiences.check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.set-up-onboarding-experiences" - ] - }, - { - "uid": "learn-dynamics.set-up-onboarding-experiences.get-users-started-checklist", - "title": "Get users started with the checklist", - "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/get-users-started-checklist", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.set-up-onboarding-experiences" - ] - }, - { - "uid": "learn-dynamics.set-up-onboarding-experiences.implement-tips-tour", - "title": "Implement teaching tips and in-app tours", - "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/implement-tips-tour", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 20, - "moduleUids": [ - "learn-dynamics.set-up-onboarding-experiences" - ] - }, - { - "uid": "learn-dynamics.set-up-onboarding-experiences.introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.set-up-onboarding-experiences" - ] - }, - { - "uid": "learn-dynamics.set-up-onboarding-experiences.onboard-new-users", - "title": "Greet new users with the welcome banner", - "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/onboard-new-users", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.set-up-onboarding-experiences" - ] - }, - { - "uid": "learn-dynamics.set-up-onboarding-experiences.summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/set-up-onboarding-experiences/summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.set-up-onboarding-experiences" - ] - }, - { - "uid": "learn-dynamics.test-automation.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/1-introduction", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.test-automation" - ] - }, - { - "uid": "learn-dynamics.test-automation.2-test-automation-responsibilities", - "title": "Understand test automation responsibilities", - "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/2-test-automation-responsibilities", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.test-automation" - ] - }, - { - "uid": "learn-dynamics.test-automation.3-documentation-examples", - "title": "Find test automation documentation and examples", - "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/3-documentation-examples", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.test-automation" - ] - }, - { - "uid": "learn-dynamics.test-automation.4-answers-about-testing", - "title": "Frequently asked questions about testing your Business Central App", - "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/4-answers-about-testing", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.test-automation" - ] - }, - { - "uid": "learn-dynamics.test-automation.5-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/5-check", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.test-automation" - ] - }, - { - "uid": "learn-dynamics.test-automation.6-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/6-summary", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.test-automation" - ] - }, - { - "uid": "learn-dynamics.test-automation.page-scripting-tool", - "title": "Use the in-client page scripting tool for acceptance testing", - "url": "https://learn.microsoft.com/en-us/training/modules/test-automation/page-scripting-tool", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 50, - "moduleUids": [ - "learn-dynamics.test-automation" - ] - }, - { - "uid": "learn-dynamics.understand-report-design-process.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.understand-report-design-process" - ] - }, - { - "uid": "learn-dynamics.understand-report-design-process.2-report-design-process", - "title": "Report design process", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process/2-report-design-process", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.understand-report-design-process" - ] - }, - { - "uid": "learn-dynamics.understand-report-design-process.3-use-snippet-create-report", - "title": "Use a snippet to create a report object", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process/3-use-snippet-create-report", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.understand-report-design-process" - ] - }, - { - "uid": "learn-dynamics.understand-report-design-process.4-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process/4-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.understand-report-design-process" - ] - }, - { - "uid": "learn-dynamics.understand-report-design-process.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-design-process/5-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.understand-report-design-process" - ] - }, - { - "uid": "learn-dynamics.understand-report-triggers-functions.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/1-introduction", - "lastModified": "2026-08-27T23:37:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.understand-report-triggers-functions" - ] - }, - { - "uid": "learn-dynamics.understand-report-triggers-functions.2-understand-report-trigger-execution", - "title": "Report trigger implementation flow", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/2-understand-report-trigger-execution", - "lastModified": "2026-08-27T23:37:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.understand-report-triggers-functions" - ] - }, - { - "uid": "learn-dynamics.understand-report-triggers-functions.3-use-report-functions", - "title": "Use report functions", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/3-use-report-functions", - "lastModified": "2026-08-27T23:37:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.understand-report-triggers-functions" - ] - }, - { - "uid": "learn-dynamics.understand-report-triggers-functions.4-create-processing-only-report", - "title": "Create a processing-only report", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/4-create-processing-only-report", - "lastModified": "2026-08-27T23:37:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.understand-report-triggers-functions" - ] - }, - { - "uid": "learn-dynamics.understand-report-triggers-functions.4a-adding-barcodes", - "title": "Adding barcodes to a report", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/4a-adding-barcodes", - "lastModified": "2026-08-27T23:37:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.understand-report-triggers-functions" - ] - }, - { - "uid": "learn-dynamics.understand-report-triggers-functions.5-exercise", - "title": "Exercise - Create a processing-only report", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/5-exercise", - "lastModified": "2026-08-27T23:37:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.understand-report-triggers-functions" - ] - }, - { - "uid": "learn-dynamics.understand-report-triggers-functions.6-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/6-check", - "lastModified": "2026-08-27T23:37:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.understand-report-triggers-functions" - ] - }, - { - "uid": "learn-dynamics.understand-report-triggers-functions.7-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/understand-report-triggers-functions/7-summary", - "lastModified": "2026-08-27T23:37:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.understand-report-triggers-functions" - ] - }, - { - "uid": "learn-dynamics.use-al-controlled-barcode-scanning.add-button", - "title": "Add a barcode-scanning UI button on a field", - "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/add-button", - "lastModified": "2026-08-20T23:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-al-controlled-barcode-scanning" - ] - }, - { - "uid": "learn-dynamics.use-al-controlled-barcode-scanning.check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/check", - "lastModified": "2026-08-20T23:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.use-al-controlled-barcode-scanning" - ] - }, - { - "uid": "learn-dynamics.use-al-controlled-barcode-scanning.integrate-scanners", - "title": "Integrate dedicated barcode scanners", - "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/integrate-scanners", - "lastModified": "2026-08-20T23:07:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.use-al-controlled-barcode-scanning" - ] - }, - { - "uid": "learn-dynamics.use-al-controlled-barcode-scanning.introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/introduction", - "lastModified": "2026-08-20T23:07:00Z", - "durationInMinutes": 1, - "moduleUids": [ - "learn-dynamics.use-al-controlled-barcode-scanning" - ] - }, - { - "uid": "learn-dynamics.use-al-controlled-barcode-scanning.invoke-scanning", - "title": "Invoke barcode scanning programmatically from AL", - "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/invoke-scanning", - "lastModified": "2026-08-20T23:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.use-al-controlled-barcode-scanning" - ] - }, - { - "uid": "learn-dynamics.use-al-controlled-barcode-scanning.scenarios-barcodes", - "title": "Supported scenarios and barcodes", - "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/scenarios-barcodes", - "lastModified": "2026-08-20T23:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.use-al-controlled-barcode-scanning" - ] - }, - { - "uid": "learn-dynamics.use-al-controlled-barcode-scanning.summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/use-al-controlled-barcode-scanning/summary", - "lastModified": "2026-08-20T23:07:00Z", - "durationInMinutes": 1, - "moduleUids": [ - "learn-dynamics.use-al-controlled-barcode-scanning" - ] - }, - { - "uid": "learn-dynamics.use-application-lifecycle.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/1-introduction", - "lastModified": "2026-08-28T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.use-application-lifecycle" - ] - }, - { - "uid": "learn-dynamics.use-application-lifecycle.2-create-azure-devops", - "title": "Create an Azure DevOps organization", - "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/2-create-azure-devops", - "lastModified": "2026-08-28T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.use-application-lifecycle" - ] - }, - { - "uid": "learn-dynamics.use-application-lifecycle.3-manage-azure-devops", - "title": "Manage Azure DevOps Projects", - "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/3-manage-azure-devops", - "lastModified": "2026-08-28T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.use-application-lifecycle" - ] - }, - { - "uid": "learn-dynamics.use-application-lifecycle.4-introduction-devops-services", - "title": "Introduction to Azure DevOps Services", - "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/4-introduction-devops-services", - "lastModified": "2026-08-28T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.use-application-lifecycle" - ] - }, - { - "uid": "learn-dynamics.use-application-lifecycle.5-connect-devops-tokens", - "title": "Connect to Azure DevOps via Personal Access Tokens", - "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/5-connect-devops-tokens", - "lastModified": "2026-08-28T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-application-lifecycle" - ] - }, - { - "uid": "learn-dynamics.use-application-lifecycle.6-introduction-github", - "title": "Introduction to GitHub", - "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/6-introduction-github", - "lastModified": "2026-08-28T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-application-lifecycle" - ] - }, - { - "uid": "learn-dynamics.use-application-lifecycle.7-exercise", - "title": "Exercise - Create an Azure DevOps organization and project", - "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/7-exercise", - "lastModified": "2026-08-28T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.use-application-lifecycle" - ] - }, - { - "uid": "learn-dynamics.use-application-lifecycle.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/8-check", - "lastModified": "2026-08-28T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-application-lifecycle" - ] - }, - { - "uid": "learn-dynamics.use-application-lifecycle.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/use-application-lifecycle/9-summary", - "lastModified": "2026-08-28T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-application-lifecycle" - ] - }, - { - "uid": "learn-dynamics.use-azure-functions.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/1-introduction", - "lastModified": "2026-08-17T18:23:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-azure-functions" - ] - }, - { - "uid": "learn-dynamics.use-azure-functions.2-create-basic", - "title": "Create basic Azure functions", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/2-create-basic", - "lastModified": "2026-08-17T18:23:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.use-azure-functions" - ] - }, - { - "uid": "learn-dynamics.use-azure-functions.3-create-existing", - "title": "Create an Azure function", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/3-create-existing", - "lastModified": "2026-08-17T18:23:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.use-azure-functions" - ] - }, - { - "uid": "learn-dynamics.use-azure-functions.4-use", - "title": "Use an Azure function in Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/4-use", - "lastModified": "2026-08-17T18:23:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.use-azure-functions" - ] - }, - { - "uid": "learn-dynamics.use-azure-functions.6-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/6-check", - "lastModified": "2026-08-17T18:23:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.use-azure-functions" - ] - }, - { - "uid": "learn-dynamics.use-azure-functions.7-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-functions/7-summary", - "lastModified": "2026-08-17T18:23:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-azure-functions" - ] - }, - { - "uid": "learn-dynamics.use-azure-pipelines.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/1-introduction", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.use-azure-pipelines" - ] - }, - { - "uid": "learn-dynamics.use-azure-pipelines.2-what-continuous-integration", - "title": "What is CI/CD?", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/2-what-continuous-integration", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-azure-pipelines" - ] - }, - { - "uid": "learn-dynamics.use-azure-pipelines.3-configure-build-agents", - "title": "Configure build agents", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/3-configure-build-agents", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.use-azure-pipelines" - ] - }, - { - "uid": "learn-dynamics.use-azure-pipelines.4-create-build-pipeline", - "title": "Create a build pipeline", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/4-create-build-pipeline", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.use-azure-pipelines" - ] - }, - { - "uid": "learn-dynamics.use-azure-pipelines.5-work-yaml-files", - "title": "Work with YAML files", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/5-work-yaml-files", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.use-azure-pipelines" - ] - }, - { - "uid": "learn-dynamics.use-azure-pipelines.6-create-release-pipeline", - "title": "Create a release pipeline", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/6-create-release-pipeline", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.use-azure-pipelines" - ] - }, - { - "uid": "learn-dynamics.use-azure-pipelines.7-exercise", - "title": "Exercise - Use Azure Pipelines for CI/CD with Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/7-exercise", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.use-azure-pipelines" - ] - }, - { - "uid": "learn-dynamics.use-azure-pipelines.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/8-check", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.use-azure-pipelines" - ] - }, - { - "uid": "learn-dynamics.use-azure-pipelines.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/use-azure-pipelines/9-summary", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-azure-pipelines" - ] - }, - { - "uid": "learn-dynamics.use-branch-merge-git.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/1-introduction", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-branch-merge-git" - ] - }, - { - "uid": "learn-dynamics.use-branch-merge-git.2-branch-commands", - "title": "Branch commands in Git", - "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/2-branch-commands", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 18, - "moduleUids": [ - "learn-dynamics.use-branch-merge-git" - ] - }, - { - "uid": "learn-dynamics.use-branch-merge-git.3-use-rebase", - "title": "Use Rebase in Git", - "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/3-use-rebase", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.use-branch-merge-git" - ] - }, - { - "uid": "learn-dynamics.use-branch-merge-git.4-set-branch-policies", - "title": "Set branch policies in Azure DevOps", - "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/4-set-branch-policies", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 14, - "moduleUids": [ - "learn-dynamics.use-branch-merge-git" - ] - }, - { - "uid": "learn-dynamics.use-branch-merge-git.5-use-pull-requests", - "title": "Use pull requests in Git with Azure DevOps", - "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/5-use-pull-requests", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.use-branch-merge-git" - ] - }, - { - "uid": "learn-dynamics.use-branch-merge-git.6-work-branch-strategies", - "title": "Work with different branching strategies", - "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/6-work-branch-strategies", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.use-branch-merge-git" - ] - }, - { - "uid": "learn-dynamics.use-branch-merge-git.7-exercise", - "title": "Exercise - Use branching and merging with Git", - "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/7-exercise", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.use-branch-merge-git" - ] - }, - { - "uid": "learn-dynamics.use-branch-merge-git.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/8-check", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.use-branch-merge-git" - ] - }, - { - "uid": "learn-dynamics.use-branch-merge-git.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/use-branch-merge-git/9-summary", - "lastModified": "2026-08-28T17:09:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.use-branch-merge-git" - ] - }, - { - "uid": "learn-dynamics.use-docker-business-central.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/1-introduction", - "lastModified": "2026-08-13T17:20:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-docker-business-central" - ] - }, - { - "uid": "learn-dynamics.use-docker-business-central.2-docker-images-containers", - "title": "Work with Docker images and containers", - "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/2-docker-images-containers", - "lastModified": "2026-08-13T17:20:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.use-docker-business-central" - ] - }, - { - "uid": "learn-dynamics.use-docker-business-central.3-install-docker-windows", - "title": "Install Docker on Windows", - "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/3-install-docker-windows", - "lastModified": "2026-08-13T17:20:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.use-docker-business-central" - ] - }, - { - "uid": "learn-dynamics.use-docker-business-central.4-process-isolation-hyper-v", - "title": "Process Isolation vs. Hyper-V Isolation", - "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/4-process-isolation-hyper-v", - "lastModified": "2026-08-13T17:20:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.use-docker-business-central" - ] - }, - { - "uid": "learn-dynamics.use-docker-business-central.5-basic-docker-commands", - "title": "Basic Docker commands", - "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/5-basic-docker-commands", - "lastModified": "2026-08-13T17:20:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.use-docker-business-central" - ] - }, - { - "uid": "learn-dynamics.use-docker-business-central.6-use-bc-container-helper", - "title": "Use the BcContainerHelper", - "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/6-use-bc-container-helper", - "lastModified": "2026-08-13T17:20:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.use-docker-business-central" - ] - }, - { - "uid": "learn-dynamics.use-docker-business-central.7-exercise", - "title": "Exercise - Use Docker to test the latest Business Central version", - "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/7-exercise", - "lastModified": "2026-08-13T17:20:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.use-docker-business-central" - ] - }, - { - "uid": "learn-dynamics.use-docker-business-central.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/8-check", - "lastModified": "2026-08-13T17:20:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.use-docker-business-central" - ] - }, - { - "uid": "learn-dynamics.use-docker-business-central.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/use-docker-business-central/9-summary", - "lastModified": "2026-08-13T17:20:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-docker-business-central" - ] - }, - { - "uid": "learn-dynamics.use-document-standards-business-central.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.use-document-standards-business-central" - ] - }, - { - "uid": "learn-dynamics.use-document-standards-business-central.2-create-document-page", - "title": "Create a document page", - "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/2-create-document-page", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.use-document-standards-business-central" - ] - }, - { - "uid": "learn-dynamics.use-document-standards-business-central.3-use-initrecord-function", - "title": "Use the InitRecord function", - "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/3-use-initrecord-function", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.use-document-standards-business-central" - ] - }, - { - "uid": "learn-dynamics.use-document-standards-business-central.4-use-testfield-function", - "title": "Use the TestField function", - "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/4-use-testfield-function", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.use-document-standards-business-central" - ] - }, - { - "uid": "learn-dynamics.use-document-standards-business-central.4a-use-round-function", - "title": "Use the Round function", - "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/4a-use-round-function", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.use-document-standards-business-central" - ] - }, - { - "uid": "learn-dynamics.use-document-standards-business-central.4b-exercise", - "title": "Exercise - Create example documents", - "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/4b-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.use-document-standards-business-central" - ] - }, - { - "uid": "learn-dynamics.use-document-standards-business-central.5-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/5-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.use-document-standards-business-central" - ] - }, - { - "uid": "learn-dynamics.use-document-standards-business-central.6-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/6-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.use-document-standards-business-central" - ] - }, - { - "uid": "learn-dynamics.use-model-driven-apps-common-data-service.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/1-introduction", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.use-model-driven-apps-common-data-service" - ] - }, - { - "uid": "learn-dynamics.use-model-driven-apps-common-data-service.2-entities-fields-common-data-service", - "title": "Identify tables and columns in Microsoft Dataverse", - "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/2-entities-fields-common-data-service", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.use-model-driven-apps-common-data-service" - ] - }, - { - "uid": "learn-dynamics.use-model-driven-apps-common-data-service.3-create-entity-import-data", - "title": "Create a table and connection to import data from Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/3-create-entity-import-data", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 25, - "moduleUids": [ - "learn-dynamics.use-model-driven-apps-common-data-service" - ] - }, - { - "uid": "learn-dynamics.use-model-driven-apps-common-data-service.4-model-driven-apps", - "title": "Get started with model-driven apps", - "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/4-model-driven-apps", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.use-model-driven-apps-common-data-service" - ] - }, - { - "uid": "learn-dynamics.use-model-driven-apps-common-data-service.5-user-interface-logic", - "title": "Explore the user interface and logic in model-driven apps", - "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/5-user-interface-logic", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.use-model-driven-apps-common-data-service" - ] - }, - { - "uid": "learn-dynamics.use-model-driven-apps-common-data-service.6-create-model-driven-app", - "title": "Create a model-driven app based on Business Central data", - "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/6-create-model-driven-app", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 17, - "moduleUids": [ - "learn-dynamics.use-model-driven-apps-common-data-service" - ] - }, - { - "uid": "learn-dynamics.use-model-driven-apps-common-data-service.7-exercise", - "title": "Exercise - Create a table in Microsoft Dataverse", - "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/7-exercise", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.use-model-driven-apps-common-data-service" - ] - }, - { - "uid": "learn-dynamics.use-model-driven-apps-common-data-service.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/8-check", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.use-model-driven-apps-common-data-service" - ] - }, - { - "uid": "learn-dynamics.use-model-driven-apps-common-data-service.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/use-model-driven-apps-common-data-service/9-summary", - "lastModified": "2026-08-14T23:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.use-model-driven-apps-common-data-service" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/1-introduction", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.10-troubleshoot", - "title": "Troubleshoot and monitor flows", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/10-troubleshoot", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.11-exercise", - "title": "Exercise - Create a flow for sending emails when a new item is created in Power Automate", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/11-exercise", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.12-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/12-check", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.13-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/13-summary", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.2-triggers", - "title": "Explore Business Central triggers in Power Automate", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/2-triggers", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.3-create-power-automate", - "title": "Create a Power Automate flow from scratch", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/3-create-power-automate", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.4-item-approvals", - "title": "Create customer, vendor, and item approvals in Power Automate", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/4-item-approvals", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 20, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.5-general-journal-approvals", - "title": "Create approvals in a general journal with Power Automate", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/5-general-journal-approvals", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.6-document-approvals", - "title": "Create documents approvals with Power Automate", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/6-document-approvals", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 9, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.7-business-scenarios", - "title": "Create Power Automate flows for different business scenarios", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/7-business-scenarios", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 22, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.8-create-button", - "title": "Create a button or instant flow in Power Automate", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/8-create-button", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.use-power-automate.9-schedule", - "title": "Run a Power Automate flow on a schedule", - "url": "https://learn.microsoft.com/en-us/training/modules/use-power-automate/9-schedule", - "lastModified": "2026-08-10T18:36:00Z", - "durationInMinutes": 9, - "moduleUids": [ - "learn-dynamics.use-power-automate" - ] - }, - { - "uid": "learn-dynamics.work-azure-boards.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/1-introduction", - "lastModified": "2026-08-11T23:09:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-azure-boards" - ] - }, - { - "uid": "learn-dynamics.work-azure-boards.2-work-process-templates", - "title": "Work with the process templates", - "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/2-work-process-templates", - "lastModified": "2026-08-11T23:09:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.work-azure-boards" - ] - }, - { - "uid": "learn-dynamics.work-azure-boards.3-configure-iterations", - "title": "Configure iterations in Azure Boards", - "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/3-configure-iterations", - "lastModified": "2026-08-11T23:09:00Z", - "durationInMinutes": 13, - "moduleUids": [ - "learn-dynamics.work-azure-boards" - ] - }, - { - "uid": "learn-dynamics.work-azure-boards.4-create-work-items", - "title": "Create work items and link to code changes", - "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/4-create-work-items", - "lastModified": "2026-08-11T23:09:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-azure-boards" - ] - }, - { - "uid": "learn-dynamics.work-azure-boards.5-project-dashboards-wikis", - "title": "Work with project dashboards and wikis", - "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/5-project-dashboards-wikis", - "lastModified": "2026-08-11T23:09:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-azure-boards" - ] - }, - { - "uid": "learn-dynamics.work-azure-boards.6-teams-integrate-devops", - "title": "Use Microsoft Teams to integrate with Azure DevOps", - "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/6-teams-integrate-devops", - "lastModified": "2026-08-11T23:09:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.work-azure-boards" - ] - }, - { - "uid": "learn-dynamics.work-azure-boards.7-exercise", - "title": "Exercise - Work with Azure Boards", - "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/7-exercise", - "lastModified": "2026-08-11T23:09:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-azure-boards" - ] - }, - { - "uid": "learn-dynamics.work-azure-boards.8-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/8-check", - "lastModified": "2026-08-11T23:09:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.work-azure-boards" - ] - }, - { - "uid": "learn-dynamics.work-azure-boards.9-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-azure-boards/9-summary", - "lastModified": "2026-08-11T23:09:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-azure-boards" - ] - }, - { - "uid": "learn-dynamics.work-entitlements-permission-sets.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/1-introduction", - "lastModified": "2026-09-03T23:14:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.work-entitlements-permission-sets" - ] - }, - { - "uid": "learn-dynamics.work-entitlements-permission-sets.2-get-started", - "title": "Get started with entitlements and permissions", - "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/2-get-started", - "lastModified": "2026-09-03T23:14:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-entitlements-permission-sets" - ] - }, - { - "uid": "learn-dynamics.work-entitlements-permission-sets.3-create-entitlements-permission-sets", - "title": "Create entitlements and permission sets", - "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/3-create-entitlements-permission-sets", - "lastModified": "2026-09-03T23:14:00Z", - "durationInMinutes": 18, - "moduleUids": [ - "learn-dynamics.work-entitlements-permission-sets" - ] - }, - { - "uid": "learn-dynamics.work-entitlements-permission-sets.4-check", - "title": "Module assessment", - "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/4-check", - "lastModified": "2026-09-03T23:14:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-entitlements-permission-sets" - ] - }, - { - "uid": "learn-dynamics.work-entitlements-permission-sets.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/5-summary", - "lastModified": "2026-09-03T23:14:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-entitlements-permission-sets" - ] - }, - { - "uid": "learn-dynamics.work-entitlements-permission-sets.override-entitlements", - "title": "Override entitlements with inherent permissions", - "url": "https://learn.microsoft.com/en-us/training/modules/work-entitlements-permission-sets/override-entitlements", - "lastModified": "2026-09-03T23:14:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-entitlements-permission-sets" - ] - }, - { - "uid": "learn-dynamics.work-power-bi.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/1-introduction", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-power-bi" - ] - }, - { - "uid": "learn-dynamics.work-power-bi.2-deploy-dashboards", - "title": "Deploy Business Central dashboards in Power BI", - "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/2-deploy-dashboards", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-power-bi" - ] - }, - { - "uid": "learn-dynamics.work-power-bi.3-set-up-power-bi", - "title": "Set up Power BI in Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/3-set-up-power-bi", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.work-power-bi" - ] - }, - { - "uid": "learn-dynamics.work-power-bi.4-view-list-data", - "title": "View list data in Power BI reports in Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/4-view-list-data", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-power-bi" - ] - }, - { - "uid": "learn-dynamics.work-power-bi.5-exercise", - "title": "Exercise - Embed your report in Business Central", - "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/5-exercise", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-power-bi" - ] - }, - { - "uid": "learn-dynamics.work-power-bi.6-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/6-check", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-power-bi" - ] - }, - { - "uid": "learn-dynamics.work-power-bi.7-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/7-summary", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-power-bi" - ] - }, - { - "uid": "learn-dynamics.work-power-bi.add-report-parts", - "title": "Adding Power BI Report Parts to pages", - "url": "https://learn.microsoft.com/en-us/training/modules/work-power-bi/add-report-parts", - "lastModified": "2026-09-02T17:07:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.work-power-bi" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/1-introduction", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.10-work-remote-git", - "title": "Work with a remote Git Repository", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/10-work-remote-git", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.11-exercise", - "title": "Exercise - Source control with Git", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/11-exercise", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.12-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/12-check", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.13-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/13-summary", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.2-configure-git", - "title": "Configure Git", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/2-configure-git", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.3-local-remote-repositories", - "title": "Compare and contrast local and remote repositories", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/3-local-remote-repositories", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.4-create-local-git", - "title": "Create a local Git repository", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/4-create-local-git", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.5-use-git-ignore", - "title": "Use the .gitignore file", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/5-use-git-ignore", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.6-add-remove-files", - "title": "Add and remove files in Git", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/6-add-remove-files", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.7-get-status-repository", - "title": "Get the status of your Git repository", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/7-get-status-repository", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.8-use-remote-git", - "title": "Use a remote Git repository", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/8-use-remote-git", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-source-control-git.9-clone-remote-git", - "title": "Clone a remote Git repository", - "url": "https://learn.microsoft.com/en-us/training/modules/work-source-control-git/9-clone-remote-git", - "lastModified": "2026-08-24T17:14:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.work-source-control-git" - ] - }, - { - "uid": "learn-dynamics.work-with-api.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-api" - ] - }, - { - "uid": "learn-dynamics.work-with-api.10-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/10-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-api" - ] - }, - { - "uid": "learn-dynamics.work-with-api.2-access", - "title": "Access Business Central API", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/2-access", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-api" - ] - }, - { - "uid": "learn-dynamics.work-with-api.3-tips", - "title": "Tips for working with APIs", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/3-tips", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-api" - ] - }, - { - "uid": "learn-dynamics.work-with-api.4-limits", - "title": "Work with API limits", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/4-limits", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-api" - ] - }, - { - "uid": "learn-dynamics.work-with-api.5-create-new", - "title": "Create new APIs", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/5-create-new", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-with-api" - ] - }, - { - "uid": "learn-dynamics.work-with-api.6-read", - "title": "Read, create, and update by using API", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/6-read", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-api" - ] - }, - { - "uid": "learn-dynamics.work-with-api.7-bound-actions", - "title": "API and OData bound actions", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/7-bound-actions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-api" - ] - }, - { - "uid": "learn-dynamics.work-with-api.9-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-api/9-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-with-api" - ] - }, - { - "uid": "learn-dynamics.work-with-codeunits.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-codeunits" - ] - }, - { - "uid": "learn-dynamics.work-with-codeunits.2-create-codeunit", - "title": "Create and access a codeunit", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits/2-create-codeunit", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.work-with-codeunits" - ] - }, - { - "uid": "learn-dynamics.work-with-codeunits.3-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits/3-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-codeunits" - ] - }, - { - "uid": "learn-dynamics.work-with-codeunits.4-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits/4-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-codeunits" - ] - }, - { - "uid": "learn-dynamics.work-with-codeunits.keyword", - "title": "Use keywords for codeunit self-reference", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-codeunits/keyword", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-codeunits" - ] - }, - { - "uid": "learn-dynamics.work-with-master-data-business-central.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/1-introduction", - "lastModified": "2026-08-18T23:35:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-master-data-business-central" - ] - }, - { - "uid": "learn-dynamics.work-with-master-data-business-central.2-setup-table-signature-design", - "title": "Use the setup table and Singleton Design Pattern", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/2-setup-table-signature-design", - "lastModified": "2026-08-18T23:35:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.work-with-master-data-business-central" - ] - }, - { - "uid": "learn-dynamics.work-with-master-data-business-central.3-master-table-page-standards", - "title": "Use master table and page standards", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/3-master-table-page-standards", - "lastModified": "2026-08-18T23:35:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.work-with-master-data-business-central" - ] - }, - { - "uid": "learn-dynamics.work-with-master-data-business-central.4-implement-series-pattern", - "title": "Implement the number series pattern", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/4-implement-series-pattern", - "lastModified": "2026-08-18T23:35:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-master-data-business-central" - ] - }, - { - "uid": "learn-dynamics.work-with-master-data-business-central.5-use-blocked-field", - "title": "Use the Blocked field and the Blocked Entity Design Pattern", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/5-use-blocked-field", - "lastModified": "2026-08-18T23:35:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-with-master-data-business-central" - ] - }, - { - "uid": "learn-dynamics.work-with-master-data-business-central.6-frequently-used-table-properties", - "title": "Implement frequently used table properties", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/6-frequently-used-table-properties", - "lastModified": "2026-08-18T23:35:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.work-with-master-data-business-central" - ] - }, - { - "uid": "learn-dynamics.work-with-master-data-business-central.6a-exercise", - "title": "Exercise - Add tables and pages for master data", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/6a-exercise", - "lastModified": "2026-08-18T23:35:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.work-with-master-data-business-central" - ] - }, - { - "uid": "learn-dynamics.work-with-master-data-business-central.7-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/7-check", - "lastModified": "2026-08-18T23:35:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-with-master-data-business-central" - ] - }, - { - "uid": "learn-dynamics.work-with-master-data-business-central.8-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-master-data-business-central/8-summary", - "lastModified": "2026-08-18T23:35:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-master-data-business-central" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.10-actions", - "title": "Define actions on a page and its properties", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/10-actions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 18, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.11-exercise", - "title": "Exercise - Create a Card page", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/11-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.12-exercise", - "title": "Exercise - Create a List page", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/12-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.13-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/13-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.14-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/14-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.2-get-started", - "title": "Get started with pages", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/2-get-started", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.3-types", - "title": "Identify different page types", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/3-types", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.4-properties", - "title": "Configure the different page properties", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/4-properties", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.5-layout", - "title": "Build a page layout by using controls", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/5-layout", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.6-link", - "title": "Link pages with page parts", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/6-link", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.7-snippets", - "title": "Use snippets in Visual Studio Code to create pages", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/7-snippets", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.8-controls", - "title": "Set different control properties", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/8-controls", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.9-search", - "title": "Enable users to search for a page", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/9-search", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.hidden-fields", - "title": "Add existing hidden fields to pages", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/hidden-fields", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.scan-barcodes", - "title": "Scan barcodes in the mobile app", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/scan-barcodes", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-pages.use-rich-text-editor", - "title": "Use the built-in rich text editor to enter data", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-pages/use-rich-text-editor", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-pages" - ] - }, - { - "uid": "learn-dynamics.work-with-queries.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-queries" - ] - }, - { - "uid": "learn-dynamics.work-with-queries.10-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/10-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-queries" - ] - }, - { - "uid": "learn-dynamics.work-with-queries.2-get-started", - "title": "Get started with queries", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/2-get-started", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-queries" - ] - }, - { - "uid": "learn-dynamics.work-with-queries.3-create", - "title": "Create a query in Visual Studio Code", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/3-create", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-with-queries" - ] - }, - { - "uid": "learn-dynamics.work-with-queries.4-join-data", - "title": "Join data in queries", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/4-join-data", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-with-queries" - ] - }, - { - "uid": "learn-dynamics.work-with-queries.5-filter-data", - "title": "Filter data in queries", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/5-filter-data", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-with-queries" - ] - }, - { - "uid": "learn-dynamics.work-with-queries.6-aggregate-data", - "title": "Aggregate and sort data in a query", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/6-aggregate-data", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-queries" - ] - }, - { - "uid": "learn-dynamics.work-with-queries.7-al-function", - "title": "Access queries from AL", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/7-al-function", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.work-with-queries" - ] - }, - { - "uid": "learn-dynamics.work-with-queries.8-advanced-concepts", - "title": "Advanced query concepts", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/8-advanced-concepts", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-queries" - ] - }, - { - "uid": "learn-dynamics.work-with-queries.9-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/9-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-with-queries" - ] - }, - { - "uid": "learn-dynamics.work-with-queries.optimize-performance", - "title": "Optimize performance in Query objects", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-queries/optimize-performance", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-queries" - ] - }, - { - "uid": "learn-dynamics.work-with-reports.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/1-introduction", - "lastModified": "2026-08-20T17:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-reports" - ] - }, - { - "uid": "learn-dynamics.work-with-reports.2-preview-run", - "title": "Preview and run reports", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/2-preview-run", - "lastModified": "2026-08-20T17:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.work-with-reports" - ] - }, - { - "uid": "learn-dynamics.work-with-reports.3-printers", - "title": "Set up printers", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/3-printers", - "lastModified": "2026-08-20T17:11:00Z", - "durationInMinutes": 16, - "moduleUids": [ - "learn-dynamics.work-with-reports" - ] - }, - { - "uid": "learn-dynamics.work-with-reports.4-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/4-check", - "lastModified": "2026-08-20T17:11:00Z", - "durationInMinutes": 4, - "moduleUids": [ - "learn-dynamics.work-with-reports" - ] - }, - { - "uid": "learn-dynamics.work-with-reports.5-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/5-summary", - "lastModified": "2026-08-20T17:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-reports" - ] - }, - { - "uid": "learn-dynamics.work-with-reports.set-layout", - "title": "Set the layout used by a report", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-reports/set-layout", - "lastModified": "2026-08-20T17:11:00Z", - "durationInMinutes": 20, - "moduleUids": [ - "learn-dynamics.work-with-reports" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.10-snippets", - "title": "Create a table in Visual Studio Code with snippets", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/10-snippets", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.11-exercise", - "title": "Exercise - Create a table", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/11-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.12-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/12-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.13-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/13-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.2-get-started", - "title": "Get started with tables", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/2-get-started", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 2, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.3-types", - "title": "Identify the different table types", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/3-types", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.4-properties", - "title": "Configure the different table properties", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/4-properties", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 12, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.5-fields", - "title": "Define fields and their properties", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/5-fields", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.6-linking", - "title": "Link different tables with table relations", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/6-linking", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.7-keys", - "title": "Create primary and secondary keys on a table", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/7-keys", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 15, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.8-dropdown", - "title": "Work with DropDown and Brick field groups", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/8-dropdown", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.9-calculated", - "title": "Create and use calculated fields", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/9-calculated", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-tables.text-search", - "title": "Enable optimized text search on table fields", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-tables/text-search", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-tables" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/1-introduction", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.10-use-soap", - "title": "Use SOAP web services", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/10-use-soap", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.11-request-records-soap", - "title": "Request records with SOAP", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/11-request-records-soap", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.12-ui-interaction", - "title": "Handle UI interaction when working with web services", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/12-ui-interaction", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.14-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/14-check", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.15-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/15-summary", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.2-differences", - "title": "Define differences between SOAP, OData, REST, and API", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/2-differences", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.3-authentication", - "title": "Web service authentication", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/3-authentication", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.4-enable-access", - "title": "Enable access to OData web services", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/4-enable-access", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.5-return-documents", - "title": "Work with different OData return documents", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/5-return-documents", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.6-request-records-odata", - "title": "Request records with OData", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/6-request-records-odata", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.7-query-options", - "title": "Work with OData query options", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/7-query-options", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.8-new-records", - "title": "Create new records with OData", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/8-new-records", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-web-services.9-update-records", - "title": "Update records with OData", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-web-services/9-update-records", - "lastModified": "2026-08-19T17:07:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-web-services" - ] - }, - { - "uid": "learn-dynamics.work-with-xmlports.1-introduction", - "title": "Introduction", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/1-introduction", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-xmlports" - ] - }, - { - "uid": "learn-dynamics.work-with-xmlports.10-summary", - "title": "Summary", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/10-summary", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-xmlports" - ] - }, - { - "uid": "learn-dynamics.work-with-xmlports.2-get-started", - "title": "Get started with XMLports", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/2-get-started", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 3, - "moduleUids": [ - "learn-dynamics.work-with-xmlports" - ] - }, - { - "uid": "learn-dynamics.work-with-xmlports.3-configure", - "title": "Configure different XMLport properties", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/3-configure", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 8, - "moduleUids": [ - "learn-dynamics.work-with-xmlports" - ] - }, - { - "uid": "learn-dynamics.work-with-xmlports.4-define-nodes", - "title": "Define nodes and their properties", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/4-define-nodes", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 6, - "moduleUids": [ - "learn-dynamics.work-with-xmlports" - ] - }, - { - "uid": "learn-dynamics.work-with-xmlports.5-design", - "title": "Design XMLports", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/5-design", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 7, - "moduleUids": [ - "learn-dynamics.work-with-xmlports" - ] - }, - { - "uid": "learn-dynamics.work-with-xmlports.6-create", - "title": "Create an XMLport by using snippets in Visual Studio Code", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/6-create", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-with-xmlports" - ] - }, - { - "uid": "learn-dynamics.work-with-xmlports.7-al-functions", - "title": "Use XMLports in AL code", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/7-al-functions", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 5, - "moduleUids": [ - "learn-dynamics.work-with-xmlports" - ] - }, - { - "uid": "learn-dynamics.work-with-xmlports.8-exercise", - "title": "Exercise - Create an XMLport to export XML data", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/8-exercise", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-with-xmlports" - ] - }, - { - "uid": "learn-dynamics.work-with-xmlports.9-check", - "title": "Check your knowledge", - "url": "https://learn.microsoft.com/en-us/training/modules/work-with-xmlports/9-check", - "lastModified": "2026-06-11T23:11:00Z", - "durationInMinutes": 10, - "moduleUids": [ - "learn-dynamics.work-with-xmlports" - ] - } - ] -} diff --git a/evaluation/README.md b/evaluation/README.md index a3445fe4..f71d52ed 100644 --- a/evaluation/README.md +++ b/evaluation/README.md @@ -1,4 +1,4 @@ -# AL review evaluation +# AL review and guidance evaluation The evaluation is convention-driven. The harness discovers every `/skills/review/al--review.md` leaf across the enabled `microsoft`, `community`, and `custom` layers. Duplicate domains resolve with `custom > community > microsoft` precedence. For each selected leaf, the harness finds paired knowledge across the same layers, applies the same precedence to duplicate article slugs, selects the first article (by filename) with both `.bad.al` and `.good.al` companions, and derives the expected positive and clean control automatically. Adding a conforming leaf requires no scoring-contract edit. @@ -55,50 +55,116 @@ This credential-free check proves every selected leaf maps to a same-named knowl The committed gate requires full expected recall, the exact convention-derived article ID, and no findings on clean controls. -## AL development evaluation +## Read-only plan guidance -`development-fixtures.json` defines end-to-end development requests rather than -prewritten good/bad snippets. Each case declares its execution mode, the -Business Central capabilities it exercises, the knowledge that should shape -the implementation, acceptance criteria, and the real checks an external -runner must perform. +`development-guidance-fixtures.json` evaluates the planning interface used by +existing workflows. It supplies an existing plan and expects referenced +constraints without target-repository changes. The initial-plan fixture is +anonymized and synthetic: a full document with metadata and a markdown body +covering root cause, proposed fix, affected files, tests, and acceptance +criteria. It is integration-shaped input, not private consumer content, a +continuation checkpoint, or proof that any production consumer is integrated. + +### Credential-free contract and scorer coverage + +CI validates the manifest, prepares opaque model requests, and runs +deterministic scorer regressions with controlled reports and temporary Git +repositories. These checks cover report shape, outcomes, reference paths, and +the evaluator's pre/post read-only comparison. They do **not** run an agent, +compile AL, run Business Central tests, or establish better code authoring. -Validate the fixture and capability manifests and prepare opaque requests: +Prepare requests in a runner-owned artifact directory outside every target +workspace: ```powershell -pwsh ./tools/Test-DevelopmentFixtures.ps1 -Root . -PrepareDirectory ./.development-evaluation +$run = Join-Path ([IO.Path]::GetTempPath()) 'bcquality-guidance-run' +pwsh ./tools/Test-DevelopmentGuidanceFixtures.ps1 -Root . -PrepareDirectory $run ``` -Each request runs `al-development` in a fresh writable AL repository. Cases -may exercise feature, bug, refactor, upgrade, or maintenance mode. -The runner compiles the generated project, runs its tests, invokes the review -quality gate, and stores the resulting implementation report using the opaque -`caseId` from its request, for example `result-case-a1b2c3d4.json`. It keeps the -generated repository available at the wrapper's `workspaceRoot` so scoring can -verify reported changed paths. Score all results with: +Preparation is not a model run. A scorer can validate a citation's path and +required fields, but only external agent traces and expert evaluation can +establish that the article was opened and its normative constraints faithfully +applied. Expected knowledge recall/precision is fixture-specific, not a corpus +coverage or authoring capability percentage. + +`no-knowledge` means no additional applicable BCQuality constraints, not unsafe +work. It requires empty `knowledge`. Partial evaluation, failed retrieval, +unknown context, and materially unresolved applicability must remain visible +and distinct; they cannot be counted as successful enrichment merely because +the JSON is parseable. + +Run the deterministic regression suite without an agent or AL environment: ```powershell -pwsh ./tools/Test-DevelopmentFixtures.ps1 -Root . -ResultsDirectory ./.development-evaluation +pwsh ./tools/Test-DevelopmentGuidanceEvaluator.ps1 ``` -The initial fixtures cover setup-backed master data, document header/line -workflows, versioned API integrations, and surgical diagnosis and repair of a -batch-processing bug, plus a rerunnable data upgrade. The capability manifest -enforces a minimum fixture-backed coverage ratio; the broader roadmap lives in -`coverage/development-capabilities.json`. +### Runner-owned read-only evidence -### Read-only plan guidance +For an external guidance run, first provision a representative, standalone Git +repository for each manifest case. The runner supplies a JSON workspace map +whose keys are the manifest IDs (not the hashed model IDs) and whose values are +absolute workspace roots. It may pass `-WorkspaceMapPath` during preparation +to bind the generated requests to those roots. The model must not select its +own workspace for scoring. -`development-guidance-fixtures.json` evaluates the planning interface used by -specialized orchestrators. It supplies an existing development plan and expects -a referenced set of implementation constraints without any target-repository -changes. +Capture evidence **before** invoking the agent, with the manifest, workspace +map, and source checkout already finalized: ```powershell -pwsh ./tools/Test-DevelopmentGuidanceFixtures.ps1 -Root . -PrepareDirectory ./.development-guidance-evaluation +# Runner-selected paths, all outside the targets and BCQuality checkout. +$map = Join-Path $evidenceDirectory 'workspace-map.json' +$baseline = Join-Path $evidenceDirectory 'baseline.json' +pwsh ./tools/Test-DevelopmentGuidanceFixtures.ps1 -Root . ` + -CaptureBaseline -WorkspaceMapPath $map -BaselinePath $baseline + +# Retain the printed SHA256 in runner-only state BEFORE agent invocation. +# After the external agent writes result-case-.json files: +pwsh ./tools/Test-DevelopmentGuidanceFixtures.ps1 -Root . ` + -ResultsDirectory $resultsDirectory -BaselinePath $baseline ` + -BaselineSha256 $preRunDigest ``` -An external runner stores `result-.json` beside the generated request -and retains the clean fixture repository at `workspaceRoot`. Score the result -with `-ResultsDirectory`; the scorer verifies knowledge recall and precision -and fails if the planning pass changed the repository. +`$evidenceDirectory`, `$resultsDirectory`, and `$preRunDigest` are supplied by +the runner; the digest must not be recomputed from potentially modified evidence +after the agent runs. Protect the baseline, digest, evaluator, and invocation +from agent changes. Capture refuses to overwrite an existing baseline. Results +contain only `caseId` and `guidanceReport`; a legacy `workspaceRoot`, if present, +must agree with the independently captured binding and never overrides it. +Missing baselines or digests, malformed reports, and escaped reference paths +fail scoring. + +The comparison checks target identity, Git HEAD, refs and index, filesystem +content and stable metadata, including tracked, untracked, ignored files and +empty directories. Committing edits or making an empty commit does not evade +the check. It also compares the actual knowledge checkout and manifest identity. +Targets must have internal Git storage; linked target worktrees, submodules, +sparse checkouts, links/junctions/reparse points, hard links, and alternate data +streams are unsupported and rejected rather than silently excluded. A linked +**knowledge** checkout is supported with its Git storage identity recorded. +Use quiescent, isolated repositories; concurrent changes also fail the gate. + +This is before/after evidence, not an OS sandbox or a complete write monitor. +It cannot prove that no transient write was reverted, that articles were opened, +or that constraints are semantically faithful. Reports and generated artifacts +must stay outside all target workspaces and the knowledge checkout. The +regression suite creates and removes its own uniquely named fixture directory; +it does not run against or clean a caller's target. + +### External agent/runtime pilot (follow-up) + +Consumer uptake and a real before-authoring pilot are not implemented by these +fixtures. The consumer must normalize its normal initial plan, persist guidance +after state initialization, inject it into existing phases, re-enrich on +material changes, and run an independent final review. See +[the integration boundary](../agent-consumption.md#repository-specific-development-orchestrators). + +Before claiming improved repairs, run an independent pinned baseline without +enrichment and a matched enriched run. Hold starting code, task, model, tools, +runtime, and gates constant; record actual immutable BCQuality checkout and +policy identities rather than trusting a configured ref. Use that same +recorded checkout for enrichment and final review. Retain external logs, +article-read traces, resulting diffs, compile/test outcomes, and independent +review evidence, including failures, no-knowledge, partial, and unresolved +results. No compile/run or authoring-quality claim follows from the +credential-free checks above. diff --git a/evaluation/development-fixtures.json b/evaluation/development-fixtures.json deleted file mode 100644 index b0c9f024..00000000 --- a/evaluation/development-fixtures.json +++ /dev/null @@ -1,237 +0,0 @@ -{ - "version": 1, - "skill": "microsoft/skills/development/al-development.md", - "maximumReviewRounds": 3, - "minimumKnowledgeRecall": 1.0, - "minimumKnowledgePrecision": 0.5, - "cases": [ - { - "id": "setup-backed-master-data", - "title": "Build setup-backed loyalty member master data", - "capabilities": [ - "setup-and-master-data" - ], - "expectedKind": "feature", - "development-request": { - "kind": "feature", - "description": "Add a Loyalty Member feature to an existing Business Central AL app. Administrators configure the member number series on a singleton setup card. Users create members through list and card pages, the table assigns numbers, and blocked members cannot be selected by consuming records. Include least-privilege permission sets and automated tests.", - "acceptance-criteria": [ - "The setup is a blank-key singleton surfaced by a Card page.", - "Member numbers use the current No. Series codeunit and support manual numbers according to setup.", - "Blocked validation occurs where a member is consumed, not only on the member table.", - "The feature includes assignable least-privilege permissions and automated tests." - ] - }, - "context": { - "technologies": [ - "al" - ], - "countries": [ - "w1" - ], - "application-area": [ - "all" - ] - }, - "requiredKnowledge": [ - "microsoft/knowledge/data-modeling/setup-table-is-a-singleton.md", - "microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.md", - "microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.md", - "microsoft/knowledge/security/permission-set-avoid-wildcard-grants.md", - "microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md" - ], - "optionalKnowledge": [ - "microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.md", - "microsoft/knowledge/security/compose-permission-sets-with-included-sets.md", - "microsoft/knowledge/style/applicationarea-required-on-page-controls.md", - "microsoft/knowledge/style/tooltip-required-on-page-fields.md", - "microsoft/knowledge/ui/showmandatory-on-code-required-page-fields.md" - ], - "requiredChecks": [ - "compile", - "tests", - "review" - ] - }, - { - "id": "document-header-and-lines", - "title": "Build a document header and lines workflow", - "capabilities": [ - "document-workflows" - ], - "expectedKind": "feature", - "development-request": { - "kind": "feature", - "description": "Implement a Service Quote feature with a header, lines, document page, number series, posting and document dates, calculated totals, and tests. Line edits must refresh the total shown on the header. Structure initialization so API, UI, and test creation paths behave consistently.", - "acceptance-criteria": [ - "The header assigns its number before InitRecord establishes document defaults.", - "The document page links lines correctly and refreshes parent totals after edits.", - "Tests exercise creation outside the UI as well as the document-page behavior.", - "The implementation contains no obsolete NoSeriesManagement dependency." - ] - }, - "context": { - "technologies": [ - "al" - ], - "countries": [ - "w1" - ], - "application-area": [ - "service" - ] - }, - "requiredKnowledge": [ - "microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md", - "microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.md", - "microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md", - "microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md" - ], - "optionalKnowledge": [ - "microsoft/knowledge/events/publish-thin-onbefore-onafter-integration-events.md", - "microsoft/knowledge/style/applicationarea-required-on-page-controls.md", - "microsoft/knowledge/style/tooltip-required-on-page-fields.md" - ], - "requiredChecks": [ - "compile", - "tests", - "review" - ] - }, - { - "id": "versioned-master-data-api", - "title": "Expose master data through a versioned API", - "capabilities": [ - "api-integrations" - ], - "expectedKind": "feature", - "development-request": { - "kind": "auto", - "plan": "Expose Loyalty Member master data through a Business Central API page. Use a stable v1.0 contract, address records by SystemId, support insert and update, use conventional entity naming, and include permissions and automated API-oriented tests.", - "acceptance-criteria": [ - "The API declares all routing properties and a stable APIVersion.", - "ODataKeyFields uses SystemId and the exposed SystemId field is not editable.", - "EntityName is singular, EntitySetName is plural, and both are lower camel case.", - "The API is covered by least-privilege permissions and automated tests." - ] - }, - "context": { - "technologies": [ - "al" - ], - "countries": [ - "w1" - ], - "application-area": [ - "all" - ] - }, - "requiredKnowledge": [ - "microsoft/knowledge/web-services/set-required-api-page-properties.md", - "microsoft/knowledge/web-services/expose-systemid-as-the-api-key.md", - "microsoft/knowledge/style/api-page-delayedinsert-true.md", - "microsoft/knowledge/style/api-page-entity-naming-singular-plural.md", - "microsoft/knowledge/security/permission-set-avoid-wildcard-grants.md" - ], - "optionalKnowledge": [ - "microsoft/knowledge/style/api-page-camelcase-properties.md", - "microsoft/knowledge/web-services/version-apis-by-adding-not-mutating-published-versions.md" - ], - "requiredChecks": [ - "compile", - "tests", - "review" - ] - }, - { - "id": "fix-filtered-batch-processing", - "title": "Fix a batch routine that processes only one record", - "capabilities": [ - "bug-diagnosis-and-fix" - ], - "expectedKind": "bug", - "development-request": { - "kind": "bug", - "description": "Users report that an existing filtered batch routine updates only the first matching record. Reproduce the defect, identify why iteration stops, make the smallest safe correction, and add a regression test that selects multiple records and proves every selected record is processed.", - "acceptance-criteria": [ - "The defect is reproduced or demonstrated by a failing regression test before the fix.", - "The root cause is corrected without widening the supplied record filters.", - "The routine processes every selected record with the appropriate update locking behavior.", - "A regression test covers more than one selected record." - ] - }, - "context": { - "technologies": [ - "al" - ], - "countries": [ - "w1" - ], - "application-area": [ - "all" - ] - }, - "requiredKnowledge": [ - "microsoft/knowledge/performance/pair-findset-with-next-loop.md", - "microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md" - ], - "optionalKnowledge": [ - "microsoft/knowledge/performance/findset-true-applies-updlock-on-read.md", - "microsoft/knowledge/performance/pass-var-record-to-preserve-partial-load-enumerator.md" - ], - "requiredChecks": [ - "compile", - "tests", - "review" - ] - }, - { - "id": "versioned-data-upgrade", - "title": "Implement a rerunnable data upgrade", - "capabilities": [ - "install-and-upgrade" - ], - "expectedKind": "upgrade", - "development-request": { - "kind": "upgrade", - "description": "Upgrade an existing app from a text-based Customer Tier field to a new enum-backed Tier field. Preserve existing customer data, support tenants that skip intermediate app versions, keep fresh installation separate from migration, and add upgrade tests.", - "acceptance-criteria": [ - "Existing tier values are migrated without running field validation triggers.", - "The migration is guarded by an upgrade tag and is safe when the upgrade runs again.", - "Check and validation triggers do not write data.", - "Fresh installation does not run version-upgrade migration.", - "Tests cover more than one historical source data version." - ] - }, - "context": { - "technologies": [ - "al" - ], - "countries": [ - "w1" - ], - "application-area": [ - "all" - ] - }, - "requiredKnowledge": [ - "microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md", - "microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md", - "microsoft/knowledge/upgrade/use-upgrade-tags-not-version-checks.md", - "microsoft/knowledge/upgrade/check-only-triggers-do-not-migrate-data.md", - "microsoft/knowledge/upgrade/install-code-does-not-run-on-version-upgrade.md" - ], - "optionalKnowledge": [ - "microsoft/knowledge/upgrade/datatransfer-skips-triggers-and-subscribers.md", - "microsoft/knowledge/upgrade/datatransfer-for-bulk-init.md", - "microsoft/knowledge/upgrade/register-upgrade-tags-with-subscribers.md", - "microsoft/knowledge/testing/transactionmodel-attribute-governs-test-transactions.md" - ], - "requiredChecks": [ - "compile", - "tests", - "review" - ] - } - ] -} diff --git a/evaluation/development-guidance-fixtures.json b/evaluation/development-guidance-fixtures.json index 29417fab..e34d2ea4 100644 --- a/evaluation/development-guidance-fixtures.json +++ b/evaluation/development-guidance-fixtures.json @@ -5,36 +5,18 @@ "minimumKnowledgePrecision": 0.67, "cases": [ { - "id": "bcapps-filtered-batch-bug-plan", - "title": "Select guidance from a BCFIX-HANDOFF v1 payload", + "id": "synthetic-normal-initial-plan", + "title": "Anonymized normal initial-plan consumer boundary", + "evidenceType": "integration-shaped-synthetic", + "boundary": "A synthetic consumer produces metadata plus a markdown plan body, serializes the full document, and passes that document as the generic existing development-plan. This is not an external real pilot and contains no consumer workflow-state schema.", "expectedKind": "bug", - "development-plan": { - "format": "BCFIX-HANDOFF", - "version": 1, - "issue": 4312, - "phase": "implement", - "status": "paused", - "baton": 2, - "rootCause": "The routine calls FindFirst and updates the current record without entering an enumerator loop, so only the first record in the supplied filtered set is modified.", - "harnessMap": { - "testCodeunit": "Update Selected Entries Tests", - "libraries": [ - "Library - Random" - ], - "pages": [], - "handlers": [] - }, - "iterationsUsed": 1, - "filesCommitted": [ - "test/Batch/UpdateSelectedEntries.Codeunit.al" - ], - "lastTestResult": "1 failing, 4 passing; the red test shows only the first of three selected records is updated.", - "deadEnds": [ - "Changing the page selection did not help because the codeunit discarded the supplied enumerator." - ], - "nextStep": "Replace the single-record read with an update-safe FindSet/Next loop that preserves the supplied filters, then rerun the red test." - }, + "expectedOutcome": "completed", + "expectedUnknown": [], + "requiresUnresolved": false, + "requiresMaterialUnresolved": false, + "development-plan": "{\"metadata\":{\"kind\":\"bug\",\"request\":\"Update every entry in the supplied filtered record set while preserving the caller's selection.\",\"origin\":\"anonymized synthetic initial plan\"},\"body\":\"## Root cause and design\\nThe routine reads and updates only the first record rather than iterating the supplied filtered set. Preserve the supplied filters and update each selected row.\\n\\n## Proposed fix\\nUse an update-safe FindSet/Next loop with an explicit update on each selected entry.\\n\\n## Affected files\\n- src/Batch/UpdateSelectedEntries.Codeunit.al\\n- test/Batch/UpdateSelectedEntriesTests.Codeunit.al\\n\\n## Test strategy\\nUse existing AL test library codeunits to arrange three selected rows and an excluded row. Assert all selected rows are updated and the excluded row is unchanged. This is a proposed test, not a reported result.\\n\\n## Acceptance criteria\\n- Every selected entry is updated exactly once.\\n- The supplied filters remain effective.\\n- No excluded entry changes.\\n- Empty selections cause no changes.\\n\"}", "context": { + "bc-version": "28", "technologies": [ "al" ], @@ -43,7 +25,8 @@ ], "application-area": [ "all" - ] + ], + "unknown": [] }, "requiredKnowledge": [ "microsoft/knowledge/performance/pair-findset-with-next-loop.md", @@ -58,6 +41,10 @@ "id": "versioned-upgrade-plan-guidance", "title": "Select guidance for a versioned data upgrade", "expectedKind": "upgrade", + "expectedOutcome": "completed", + "expectedUnknown": [], + "requiresUnresolved": false, + "requiresMaterialUnresolved": false, "development-plan": { "kind": "upgrade", "request": "Migrate existing customer tier text values to a new enum field in an app upgrade.", @@ -78,6 +65,7 @@ ] }, "context": { + "bc-version": "28", "technologies": [ "al" ], @@ -86,7 +74,8 @@ ], "application-area": [ "all" - ] + ], + "unknown": [] }, "requiredKnowledge": [ "microsoft/knowledge/upgrade/use-upgrade-tags-not-version-checks.md", @@ -97,6 +86,90 @@ "microsoft/knowledge/upgrade/datatransfer-skips-triggers-and-subscribers.md", "microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md" ] + }, + { + "id": "no-additional-knowledge", + "title": "Honest empty enrichment does not prohibit ordinary work", + "expectedKind": "maintenance", + "expectedOutcome": "no-knowledge", + "expectedUnknown": [], + "requiresUnresolved": false, + "requiresMaterialUnresolved": false, + "development-plan": { + "kind": "maintenance", + "request": "Correct a spelling error in an existing internal explanatory comment in an AL procedure. Do not change the explanation, executable code, UI captions, schema, diagnostic text, configuration or behavior.", + "affected-files": ["src/Batch/EntryProcessor.Codeunit.al"], + "proposed-changes": ["Replace the misspelled word in the existing comment; add no new advice."], + "test-strategy": "Inspect the diff to confirm that only the comment spelling changes.", + "acceptance-criteria": ["Only the intended comment spelling changes; executable AL remains identical."] + }, + "context": { + "bc-version": "28", + "technologies": ["al"], + "countries": ["w1"], + "application-area": ["all"], + "unknown": [] + }, + "requiredKnowledge": [], + "optionalKnowledge": [] + }, + { + "id": "unknown-material-version", + "title": "Materially unresolved version-sensitive guidance stays partial", + "expectedKind": "feature", + "expectedOutcome": "partial", + "expectedUnknown": ["bc-version"], + "requiresUnresolved": true, + "requiresMaterialUnresolved": true, + "development-plan": { + "kind": "feature", + "request": "Add an expensive Sum FlowField as the source of a usually-hidden page control. The proposed design relies on visibility suppressing calculation.", + "affected-files": ["src/Pages/EntryOverview.Page.al"], + "proposed-changes": ["Bind the page control directly to the FlowField and set Visible to a conditional expression."], + "test-strategy": "Verify aggregate queries are not executed while the control is hidden.", + "acceptance-criteria": ["Hidden controls do not cause expensive aggregate queries."], + "unknown": ["The deployment BC version and visible-only calculation feature state cannot be established from this fixture. Do not invent either."] + }, + "context": { + "bc-version": "unknown", + "technologies": ["al"], + "countries": ["w1"], + "application-area": ["all"], + "unknown": ["bc-version"] + }, + "requiredKnowledge": [ + "microsoft/knowledge/performance/hidden-flowfields-still-calculate-before-bc26-opt-in.md" + ], + "optionalKnowledge": [] + }, + { + "id": "partial-plan-decision", + "title": "Known platform context does not resolve an incomplete plan decision", + "expectedKind": "refactor", + "expectedOutcome": "partial", + "expectedUnknown": [], + "requiresUnresolved": true, + "requiresMaterialUnresolved": true, + "development-plan": { + "kind": "refactor", + "request": "Refactor a record read helper currently using FindFirst followed by Next. The caller contract does not establish whether to return one row or enumerate the entire filtered set.", + "affected-files": ["src/Queries/EntryReader.Codeunit.al"], + "proposed-changes": ["Choose the read method consistent with the intended cardinality after that decision is clarified."], + "test-strategy": "Add cardinality assertions after the caller contract is decided.", + "acceptance-criteria": ["The method and enumeration agree with the clarified caller contract."], + "unknown": ["Single-record versus multi-record caller intent remains materially unresolved."] + }, + "context": { + "bc-version": "28", + "technologies": ["al"], + "countries": ["w1"], + "application-area": ["all"], + "unknown": [] + }, + "requiredKnowledge": [ + "microsoft/knowledge/performance/pair-findset-with-next-loop.md" + ], + "optionalKnowledge": [] } ] } diff --git a/microsoft/skills/development/al-development-plan.md b/microsoft/skills/development/al-development-plan.md index 976a11cf..115e5fe7 100644 --- a/microsoft/skills/development/al-development-plan.md +++ b/microsoft/skills/development/al-development-plan.md @@ -18,9 +18,11 @@ Selects the BCQuality knowledge that should constrain an existing AL development Both a readable `repository` and a non-empty `development-plan` are required. The plan may be structured data or text, but it must identify the intended change. Return `not-applicable` without changing files when either input is absent or the repository is not an AL project. +The caller supplies its existing plan, not a request to generate one. Consumer-specific formats must be normalized by the consumer before invocation. This skill does not interpret issue records, continuation markers, batons, retries, or workflow state. A serialized document containing plan metadata and a markdown body is acceptable when it states the intended change, affected surfaces, proposed approach, test strategy, and acceptance criteria. Missing details remain unknown; do not invent them. + ## Source -Read the BCQuality knowledge index once. Use entries from every enabled layer and domain. The index supplies candidate paths, applicability dimensions, keywords, titles, and descriptions; it never substitutes for opening selected articles in full. +Read the BCQuality knowledge index once, using the external path supplied by Entry when present. If no index is available, use READ's path-based discovery across enabled layers; inability to read the corpus is `failed`, not `no-knowledge`. Use entries from every enabled layer and domain. The index supplies candidate paths, applicability dimensions, keywords, titles, and descriptions; it never substitutes for opening selected articles in full. Inspect the target repository read-only for `app.json`, affected files and symbols named by the plan, relevant tests, permission sets, dependencies, target/runtime versions, countries, application areas, and repository conventions. Do not create scratch or generated files inside the target repository. @@ -37,8 +39,7 @@ When a dimension cannot be resolved, retain conditionally applicable candidates ## Worklist -1. Normalize the plan into: request summary, development kind, assumptions, root cause or design intent, affected files and symbols, proposed changes, test strategy, and acceptance criteria. When the plan has no normalized kind, apply the same categories as `al-development`: new or expanded behavior is `feature`, a defect correction is `bug`, behavior-preserving restructuring is `refactor`, migration is `upgrade`, and other bounded work is `maintenance`. A repository-specific additive event or extensibility request maps to `feature`; retain its original work-item type in the request summary. Do not redesign the repository-specific workflow. - - For a `BCFIX-HANDOFF` v1 payload, map `rootCause` to root cause, `harnessMap` to test context, `filesCommitted` to affected files, `lastTestResult` to existing test evidence, `deadEnds` to rejected approaches, and `nextStep` to the immediate proposed change. Preserve `issue`, `phase`, `status`, `baton`, and `iterationsUsed` as workflow context only; they do not create Business Central constraints. A handoff with a non-empty root cause is `bug` unless the surrounding plan identifies an additive Event Request, which maps to `feature`. +1. Read the supplied plan for its request summary, development kind, assumptions, root cause or design intent, affected files and symbols, proposed changes, test strategy, and acceptance criteria. Preserve its intent; do not generate a replacement plan. If kind is not explicit, classify the stated intent: new or expanded behavior is `feature`, a defect correction is `bug`, behavior-preserving restructuring is `refactor`, migration is `upgrade`, and other bounded work is `maintenance`. Do not redesign the consumer's workflow. 2. Build retrieval vocabulary from the plan and confirmed repository symbols. Give exact object types, properties, methods, analyzers, errors, and affected domains more weight than broad business nouns. 3. Search the index in separate passes: - data ownership, keys, setup, numbering, validation, transactions, and upgrade; @@ -66,4 +67,6 @@ Do not change the target repository. Before emitting, verify every knowledge and ## Output -Return one `development-guidance-report` conforming to DO. `completed` requires that every selected article was opened and faithfully converted into constraints. `no-knowledge` is valid when the plan is applicable but BCQuality contains no relevant article. `partial` names every unevaluated candidate or unresolved applicability gap. +Return one `development-guidance-report` conforming to DO. `completed` requires that every selected article was opened and faithfully converted into constraints, with no material unresolved applicability. `no-knowledge` means there are no additional applicable BCQuality constraints for this plan; emit empty `knowledge`. It does not mean the work is unsafe or unimplementable, and the consumer can proceed under its ordinary gates. Never add generic or filler guidance to avoid this outcome. + +Return `partial` for incomplete evaluation or materially unresolved conditional guidance, naming every gap. Failed retrieval or reference-integrity checks are `failed`, never `no-knowledge`. Consumers own handling of partial, failed, and unresolved results, including clarification and re-enrichment; this read-only interface does not define a universal implementation gate. diff --git a/microsoft/skills/development/al-development.md b/microsoft/skills/development/al-development.md deleted file mode 100644 index 755d46ca..00000000 --- a/microsoft/skills/development/al-development.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -kind: action-skill -id: al-development -version: 1 -title: AL development -description: Implements Business Central AL features, bug fixes, refactors, upgrades, and maintenance changes using BCQuality knowledge. -inputs: [development-request, repository] -outputs: [implementation-report] -bc-version: [all] -technologies: [al] -countries: [w1] -application-area: [all] -guidance-skill: microsoft/skills/development/al-development-plan.md -quality-skill: microsoft/skills/review/al-code-review.md -quality-round-limit: 3 ---- - -# AL development - -Implements a Business Central change in an existing AL repository. Feature work, bug fixing, refactoring, upgrades, and maintenance share one public contract and one quality pipeline; their different investigation disciplines are execution modes within this skill. - -Both a writable `repository` and a `development-request` are required. A structured request has this shape: - -```yaml -development-request: - kind: auto # feature | bug | refactor | upgrade | maintenance - description: string # optional when plan states the requested outcome - plan: string # optional - acceptance-criteria: [string] # optional -``` - -A plain-text request is normalized to `kind: auto` with the text as `description`. A plan-only request is valid when the plan states the requested outcome. Return `not-applicable` without changing files when either input is absent, both description and plan are empty, or the repository is not an AL project. - -## Source - -Read the frontmatter `guidance-skill`; it owns BCQuality discovery and returns the knowledge constraints for the implementation plan. Inspect the target repository for `app.json`, existing objects, tests, permission sets, analyzers, build scripts, naming and object-ID conventions, dependencies, target/runtime versions, localization layout, and uncommitted user changes. For bugs, refactors, and upgrades, inspect enough history and surrounding code to establish the behavior being changed. - -## Relevance - -Resolve and pass this context to the guidance-skill: - -- `bc-version` from the target application's platform/application/runtime settings or supplied context. For an upgrade, distinguish source and target versions. -- `technologies: [al]`, plus any additional technology actually required by the request. -- `countries` from `app.json`, workspace configuration, or supplied context. -- `application-area` from the request and affected objects. - -Record unresolved dimensions in the development plan rather than silently substituting broad values. The guidance-skill applies READ's matching semantics and returns any conditional applicability in its report. - -## Worklist - -1. Normalize the request, deriving a concise description from a plan-only input, and classify `kind: auto` as: - - `feature` for new or intentionally expanded behavior; - - `bug` for observed behavior that contradicts an expected result; - - `refactor` for structural change with no intended behavior change; - - `upgrade` for schema, data, dependency, runtime, or application-version migration; - - `maintenance` for bounded development work that fits none of the above. - Preserve an explicit valid kind. When repository evidence conflicts with it, record the mismatch and ask for clarification before changing files rather than silently switching disciplines. -2. Establish the mode-specific implementation contract: - - **Feature:** define user-visible behavior and cover data lifecycle, UI/API, permissions, extensibility, upgrade impact, telemetry, and tests where applicable. - - **Bug:** state expected versus actual behavior, reproduce or otherwise prove the defect, trace the root cause, and define a regression test that fails for that cause. - - **Refactor:** identify the behavior and public contracts that must remain invariant, plus the checks that establish a before/after baseline. - - **Upgrade:** identify source and target states, data migration, compatibility, idempotency, and validation requirements. - - **Maintenance:** define the bounded outcome and the behavior that must not change. -3. Treat a supplied plan as an input constraint, not as proof. Reconcile it with repository reality and BCQuality; preserve its intent, correct unsafe assumptions, and record consequential deviations. -4. Discover existing implementation patterns and reusable objects before proposing new ones. Preserve repository conventions and current user changes. -5. Materialize a `development-plan` containing the classified kind, request, assumptions, affected files and symbols, design or root cause, proposed changes, validation strategy, and acceptance criteria. -6. Invoke the frontmatter `guidance-skill` with that plan, the repository, and the resolved context. It performs Source, Relevance, and knowledge worklisting independently and read-only. -7. Require a complete guidance result before editing product code: - - `completed` — use every returned constraint and validation consideration. - - `no-knowledge` — intentionally refuse to implement: return `no-knowledge` with no request changes, set `outcome-reason` to `No applicable BCQuality knowledge was found for this development plan.`, and add a `remaining` entry directing the caller to use a repository-specific workflow/general coding agent or contribute the missing BC-specific knowledge. - - `not-applicable`, `partial`, or `failed` — return the corresponding non-completed outcome without editing product code; preserve its reason in `remaining`. -8. Copy the guidance report's selected paths into the eventual implementation report only when the corresponding constraint materially shaped the implementation. Carry its suppression records forward. - -## Action - -1. Record the starting working-tree state so unrelated changes are preserved and excluded from `changes`. -2. Apply the execution mode: - - **Feature:** implement the smallest complete vertical slice; do not leave placeholder surfaces. - - **Bug:** reproduce first when feasible, fix the root cause rather than the symptom, keep the patch surgical, and add a regression test. - - **Refactor:** capture a behavioral baseline, avoid unrelated behavior changes, and prove the declared invariants afterward. - - **Upgrade:** make migrations rerunnable where required, preserve data and compatibility, and validate both upgraded and fresh-install paths when applicable. - - **Maintenance:** make only the bounded requested change and preserve surrounding behavior. -3. Produce a coherent design that satisfies the implementation contract and every constraint returned by the guidance-skill. Reuse existing abstractions and object ranges. Do not hard-code a Business Central fact in this skill or invent a rule absent from both the repository and reliable platform knowledge. -4. Implement the request end to end. Include all surfaces required by the mode, acceptance criteria, and repository conventions. Do not create success-shaped stubs. -5. Treat the guidance report as design constraints throughout implementation. Adapt its referenced companion samples to the target codebase; never copy demonstration IDs or names blindly. -6. Run the smallest existing build, analyzer, and test commands that cover the change. Fix failures caused by the implementation. Record every command and real outcome in `validation`; unavailable checks are `not-run`, never `passed`. -7. Invoke the frontmatter `quality-skill` against the final implementation diff, bounded by `quality-round-limit`: - - Record every invocation in `review-rounds`, including its gating `blocker` and `major` IDs. - - When no gating finding remains, mark the round `clean` and stop. - - Otherwise fix every justified, safely actionable gating finding, rerun affected validation, mark the round `fixing`, and start the next review round. - - Stop early as `stalled` when the gating ID set is unchanged from the preceding round, no gating finding can be fixed safely, or validation cannot be restored. - - When the final allowed round still has gating findings, mark it `limit-reached`. - Preserve the last complete findings-report in `review` and add a `validation` entry with `id: "review"`. A `stalled` or `limit-reached` loop returns `partial` with the unresolved gating findings in `remaining`. If review is disabled or unavailable, record `not-run` and return `partial`. -8. Verify the persisted files against the implementation contract, acceptance criteria, and mode-specific evidence. If behavior, validation, guidance, or review remains incomplete, return `partial` and list the exact gap in `remaining`. - -## Output - -Return one `implementation-report` conforming to DO. Set `plan.kind` to the classified execution mode. `knowledge` lists only articles opened in full and materially used. `changes` lists only files changed by this skill. `completed` requires a persisted implementation, passing required validation, and no unresolved `blocker` or `major` finding in `review`. `no-knowledge` is a visible coverage decision, not an error or silent fallback. diff --git a/plugin.json b/plugin.json index d74e69a2..edb78c9d 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "bcquality", - "description": "Quality skills and knowledge for Business Central development. Exposes AL development and code-review adapters backed by BCQuality's Entry protocol.", + "description": "Quality skills and knowledge for Business Central. Exposes read-only AL plan enrichment and code-review adapters backed by BCQuality's Entry protocol.", "version": "0.3.0", "author": { "name": "microsoft/BCQuality", @@ -13,7 +13,7 @@ "al", "business-central", "code-review", - "development", + "plan-guidance", "quality" ], "skills": [ diff --git a/skills/README.md b/skills/README.md index a5f78efc..3b9f2b4c 100644 --- a/skills/README.md +++ b/skills/README.md @@ -31,7 +31,7 @@ READ and DO are read on demand — typically by the first action skill the agent | Path | Role | |---|---| | [`al-code-review/SKILL.md`](al-code-review/SKILL.md) | Exposes BCQuality through the standard `SKILL.md` format when this repository is installed as a plugin. | -| [`al-development/SKILL.md`](al-development/SKILL.md) | Exposes knowledge-backed AL development through the standard `SKILL.md` format. | +| [`al-development-plan/SKILL.md`](al-development-plan/SKILL.md) | Enriches an existing AL plan read-only through the standard `SKILL.md` format; does not generate a plan or implement code. | Each adapter is deliberately thin. It translates the caller's request into an Entry task context, then follows Entry's dispatch without owning routing, @@ -42,15 +42,13 @@ by Entry, and should not accumulate behavior already defined by `entry.md`, This gives the two skill formats distinct roles: - `skills/al-code-review/SKILL.md` and - `skills/al-development/SKILL.md` are the public host integration + `skills/al-development-plan/SKILL.md` are the public host integration surfaces for a standalone plugin installation. - `microsoft/skills/review/al-code-review.md` is BCQuality's internal Microsoft-layer super-skill for coordinating a broad AL review. -- `microsoft/skills/development/al-development.md` is the internal - Microsoft-layer implementation skill for all supported development modes. - `microsoft/skills/development/al-development-plan.md` is the read-only - planning interface for repository-specific orchestrators that retain - implementation ownership. + knowledge-enrichment interface for existing plans. Consumers own format + normalization, planning, implementation, and delivery. Each host adapter deliberately shares its name with the internal action skill for the same operation. Their locations distinguish the host integration from diff --git a/skills/al-development-plan/SKILL.md b/skills/al-development-plan/SKILL.md new file mode 100644 index 00000000..04044913 --- /dev/null +++ b/skills/al-development-plan/SKILL.md @@ -0,0 +1,22 @@ +--- +name: al-development-plan +description: Enrich an existing Business Central AL development plan with read-only BCQuality knowledge constraints. Does not generate a plan or implement code. +--- + +# AL development plan guidance + +This host-native adapter translates an existing plan and repository into Entry's task context. It does not plan new work, edit the target repository, run an implementation or review/fix loop, stage, commit, or publish changes. + +## Execute + +1. Resolve `PLUGIN_ROOT` to the directory containing this plugin's root `plugin.json`, two levels above this file. +2. Preserve the caller's existing `development-plan` verbatim. Consumer-specific workflow payloads must be normalized by the consumer; do not interpret workflow state or manufacture a plan from a coding request. +3. Build the task context for `PLUGIN_ROOT/skills/entry.md`: + - Set `goal` to read-only BCQuality knowledge enrichment of the supplied plan, preserving the caller's intended change. + - List only actually supplied inputs from `[development-plan, repository]` in `inputs-available`. + - Set `technologies: [al]` only when established, and pass other applicability dimensions only when supplied or reliably determined. + - Apply `BCQUALITY_ENABLED_LAYERS` and `BCQUALITY_DISABLED_SKILLS` as described in the `al-code-review` adapter. +4. Read and execute Entry, including Preparation. Resolve its paths against `PLUGIN_ROOT`, not the target repository. Keep all generated index and scratch artifacts outside the target repository. Use READ's path-based fallback if index generation is unavailable; an unreadable corpus is a failure, not empty knowledge. +5. Follow Entry's dispatch, checking its output metadata against the referenced skill before invocation. This operation accepts only `development-guidance-report`; return `failed` rather than execute another output kind. Pass the supplied existing plan and readable repository, and return the report unchanged. Return Entry's `no-match` or `failed` record unchanged when nothing is dispatched. + +Missing inputs remain missing; the dispatched action skill returns `not-applicable` when it cannot proceed. A `no-knowledge` report is additive: it means no additional BCQuality constraints, not a refusal to let the consumer implement under its own gates. The consumer retains all implementation and delivery ownership. diff --git a/skills/al-development/SKILL.md b/skills/al-development/SKILL.md deleted file mode 100644 index 2d8873a8..00000000 --- a/skills/al-development/SKILL.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: al-development -description: Implement Business Central AL features, bug fixes, refactors, upgrades, and maintenance changes using BCQuality's curated platform knowledge. ---- - -# AL development - -This is BCQuality's host-native adapter for standalone plugin installations. It translates a coding request into Entry's task context; the internal action skill owns classification, investigation, design, implementation, validation, and review policy. - -When the target repository exposes a more specific local workflow for the request, such as an end-to-end bug-fix skill with its own environment and delivery gates, prefer that repository workflow unless the caller explicitly asks to use BCQuality's generic development skill. - -This adapter is deliberately knowledge-backed: if BCQuality has no applicable -guidance, it returns a visible `no-knowledge` result without changing code. Use -the repository's normal coding workflow for unbacked requests, or add the -missing BC-specific knowledge before expecting this skill to implement them. - -## Execute - -1. Resolve `PLUGIN_ROOT` to the directory containing this plugin's root `plugin.json`. This file is `PLUGIN_ROOT/skills/al-development/SKILL.md`; when the host does not expose the plugin root, resolve it two levels above this file. -2. Build the `task-context` required by `PLUGIN_ROOT/skills/entry.md`: - - Copy the caller's request verbatim into `goal`. - - Set `inputs-available: [development-request, repository]`. - - Set `technologies: [al]` when the repository is an AL project. - - Pass `bc-version`, `countries`, and `application-area` only when supplied or reliably determined. - - Apply `BCQUALITY_ENABLED_LAYERS` and `BCQUALITY_DISABLED_SKILLS` exactly as the `al-code-review` adapter does. -3. Read and execute `PLUGIN_ROOT/skills/entry.md`, including Preparation. Resolve every path it names against `PLUGIN_ROOT`, not the user's repository. If knowledge-index generation is unavailable, use READ's path-based fallback. -4. Follow Entry's dispatch exactly. The normal result is `microsoft/skills/development/al-development.md`; do not select it directly or duplicate its behavior in this adapter. -5. Normalize the caller's input as `development-request`: - - Plain text becomes `{ kind: auto, description: }`. - - Preserve an explicit `kind`, supplied plan, and `acceptance-criteria`. - - A plan-only input becomes `{ kind: auto, description: "Implement the supplied development plan.", plan: }`. - Pass the writable current workspace as `repository`, execute the dispatched skill, and return its `implementation-report` unchanged. - -The adapter never edits BCQuality itself unless BCQuality is the caller's target repository. The target of implementation is the repository supplied by the caller. diff --git a/skills/do.md b/skills/do.md index ddbd96d6..cc263fd8 100644 --- a/skills/do.md +++ b/skills/do.md @@ -7,7 +7,7 @@ title: Action Skill — the template every action skill follows # DO -An action skill is a markdown file that tells an agent how to do one concrete job — review a pull request, audit telemetry usage, generate a skeleton — using knowledge files from BCQuality. This document is the template every action skill follows. Orchestrators rely on the template to consume any skill without skill-specific parsing. +An action skill is a markdown file that tells an agent how to do one concrete job — review a pull request, audit telemetry usage, enrich an existing plan — using knowledge files from BCQuality. This document is the template every action skill follows. Orchestrators rely on the template to consume any skill without skill-specific parsing. This contract is stable. Changes require a PR approved by both maintainers. @@ -56,20 +56,15 @@ application-area: [all] `bc-version`, `technologies`, `countries`, `application-area` are optional filters that let an orchestrator pre-select applicable skills for a task. They follow the same semantics as in READ. -`inputs` is a list of abstract input types the skill **accepts**. Standard values: `pr-diff`, `object-list`, `file-path`, `repository`, `telemetry-query`, `development-request`, `development-plan`. Semantics are any-of: the orchestrator supplies whichever listed input types it has, and the skill is invoked with a non-empty subset of its declared `inputs`. A skill that cannot proceed with the supplied subset MUST return `outcome: "not-applicable"`. +`inputs` is a list of abstract input types the skill **accepts**. Standard values: `pr-diff`, `object-list`, `file-path`, `repository`, `telemetry-query`, `development-plan`. Semantics are any-of: the orchestrator supplies whichever listed input types it has, and the skill is invoked with a non-empty subset of its declared `inputs`. A skill that cannot proceed with the supplied subset MUST return `outcome: "not-applicable"`. `outputs` is always a single-element list naming the output kind: - `findings-report` — evaluates an input and reports defects or observations. -- `implementation-report` — changes a repository to satisfy a development request and reports the plan, knowledge used, changed files, validation, and post-implementation review. - `development-guidance-report` — selects and summarizes applicable BCQuality knowledge for an existing development plan without changing the target repository. `sub-skills` is an optional field. When present and non-empty, the skill is a **super-skill** that composes other action skills; see *Composition* below. Values are repo-relative paths to action-skill files. -`quality-skill` is optional on an action skill that emits an `implementation-report`. It names one repo-relative review action skill to run over the completed diff. It is a post-implementation gate, not a composed sub-skill: Entry does not route through it, and its final complete findings-report is returned in `review`. `quality-round-limit` is the required positive maximum number of review/fix rounds when a quality skill is declared. Consumer configuration still applies; if the named quality skill is disabled or unavailable, record its validation as `not-run` and do not claim `completed`. - -`guidance-skill` is optional on an action skill that emits an `implementation-report`. It names one repo-relative read-only action skill that accepts a `development-plan` and emits a `development-guidance-report`. The implementation skill invokes it after forming its plan and before editing product code. Consumer configuration still applies; when guidance is disabled or unavailable, the implementation skill must not claim knowledge-backed development. - ## Required sections Every action skill MUST contain these five sections, in order: @@ -86,13 +81,15 @@ Every action skill MUST contain these five sections, in order: **Relevance.** Apply frontmatter filters to the candidates. Typical filters: match `bc-version` against the target environment, match `technologies` against the languages in scope, match `countries` and `application-area` against the consuming codebase's context. The exact matching rules are defined in READ (*Frontmatter matching semantics*). Files that do not match are discarded. -**Worklist.** Narrow the relevant candidates to the subset that applies to the current task. This is where the task-specific signal enters: the objects changed in the PR, the queries being audited, the skeleton being generated. Typical moves: match `keywords` against task vocabulary, match file topics against changed objects, deduplicate by concern. +**Worklist.** Narrow the relevant candidates to the subset that applies to the current task. This is where the task-specific signal enters: the objects changed in the PR, the queries being audited, the existing plan being enriched. Typical moves: match `keywords` against task vocabulary, match file topics against changed objects, deduplicate by concern. **Action.** Execute the skill's work against the worklist. Evaluate each item in the worklist against the task input and emit findings. The action step is where skill behavior differs; the preceding three steps are uniform. + + ## Findings-report contract -Every action skill emits a single JSON document that conforms to this schema: +An action skill with `outputs: [findings-report]` emits a single JSON document that conforms to this schema: ```json { @@ -300,96 +297,27 @@ An action skill with `outputs: [development-guidance-report]` emits one JSON doc } ``` -The skill is read-only with respect to the target repository. `completed` means every selected article was opened and converted into faithful implementation constraints. `no-knowledge` means no applicable article survived filtering; `knowledge` is empty. `partial` means candidate evaluation stopped early, with the gap named in `outcome-reason` and `unresolved`. - -`knowledge[].constraints` summarizes only normative `## Best Practice` and `## Anti Pattern` content from the referenced article. It must not introduce a Business Central fact absent from that article. `sample-paths` contains only sibling samples that exist and were opened. Every path is subject to the reference-integrity gate. - -`validation-considerations` states evidence the implementation workflow should obtain; it does not claim that a command or test has run. `unresolved` records missing repository context or plan decisions that prevent a reliable constraint. Unknown applicability dimensions must appear in both `context.unknown` and a relevant unresolved entry. - -## Implementation-report contract - -An action skill with `outputs: [implementation-report]` emits one JSON document: - -```json -{ - "skill": { "id": "string", "version": 1 }, - "outcome": "completed | not-applicable | no-knowledge | partial | failed", - "outcome-reason": "string", - "summary": { - "request": "string", - "files-created": 0, - "files-modified": 0, - "files-deleted": 0 - }, - "plan": { - "kind": "feature | bug | refactor | upgrade | maintenance", - "assumptions": ["string"], - "decisions": ["string"], - "objects": ["string"] - }, - "knowledge": [ - { "path": "string", "sha": "string", "used-for": "string" } - ], - "changes": [ - { - "path": "string", - "action": "created | modified | deleted", - "purpose": "string" - } - ], - "validation": [ - { - "id": "string", - "command": "string", - "status": "passed | failed | not-run", - "details": "string" - } - ], - "review": { "...full findings-report from the post-implementation review..." : null }, - "review-rounds": [ - { - "round": 1, - "outcome": "clean | fixing | stalled | limit-reached", - "gating-finding-ids": ["string"] - } - ], - "suppressed": [ - { - "reference": { "path": "string", "sha": "string" }, - "reason": "layer-precedence | configuration" - } - ], - "remaining": ["string"] -} -``` - -### Implementation outcome semantics - -- `completed` — the requested change is persisted in the repository, required validation passed, and the post-implementation review has no unresolved `blocker` or `major` finding. -- `not-applicable` — the request is not an implementation task accepted by the skill, or the supplied repository does not contain the required technology. -- `no-knowledge` — no applicable BCQuality knowledge survived filtering and the skill cannot safely implement the Business Central-specific request. No request changes are made. -- `partial` — useful changes were persisted, but part of the requested scope, validation, or post-implementation review could not be completed. `outcome-reason` and `remaining` identify the unfinished work. -- `failed` — the skill could not produce a reliable implementation. `outcome-reason` is required. Any working-tree changes remain visible and MUST still be listed in `changes`. - -### Implementation field semantics - -**`summary.request`** is a concise statement of the implemented change. File counts describe only changes made by this skill; pre-existing user changes are excluded. +The skill is read-only with respect to the target repository: no edits, generated files, staging, commits, or publication. Keep index, report, and scratch artifacts outside that repository. The report is strict JSON with no surrounding commentary. The caller supplies an existing plan and repository; consumer-specific input normalization and workflow state are outside this contract. -**`plan`** records the implementation decisions needed to understand the result. `kind` is the classified development mode: `feature`, `bug`, `refactor`, `upgrade`, or `maintenance`. `assumptions` contains only assumptions actually made; `decisions` captures consequential design choices; `objects` names the Business Central objects or other artifacts created or changed. +### Guidance outcome semantics -**`knowledge`** lists every knowledge file whose normative guidance materially shaped the implementation. `path` and optional `sha` follow the same reference format as a findings-report. `used-for` briefly names the design or implementation decision. The reference-integrity gate applies: every path must exist in the live checkout, be copied verbatim from discovery, and have been opened in full. Applicability alone is not enough to list an article. +- `completed` — evaluation finished, at least one article was selected, every selected article was opened and faithfully converted into constraints, and no materially unresolved conditional guidance remains. +- `not-applicable` — the required existing plan or readable repository is absent, or the task is outside the skill's applicability. No constraints are claimed. +- `no-knowledge` — evaluation finished and there are **no additional applicable BCQuality constraints** for this plan. `knowledge` is empty. This is not a statement that the work is unsafe or unimplementable; the consuming workflow can proceed under its ordinary gates. Do not add generic or filler articles to avoid this outcome. +- `partial` — evaluation is incomplete or conditional guidance remains materially unresolved. Name each gap in `outcome-reason` and `unresolved`; do not silently treat an unknown dimension as a match. +- `failed` — retrieval, reference integrity, or another error prevents a reliable report. Set `outcome-reason`; consumers must not treat the result as reliable constraints or as `no-knowledge`. -**`changes`** is an exhaustive list of files created, modified, or deleted by the skill. Paths are repository-relative and use forward slashes. Do not include unrelated pre-existing changes. +`outcome-reason` is required for `partial` and `failed`, optional otherwise. These outcomes describe enrichment only, not permission to implement or deliver. The consumer owns handling of partial, failed, and unresolved guidance, including escalation, clarification, and re-enrichment; BCQuality does not impose a universal implementation gate. -**`validation`** records commands actually run. `passed` and `failed` require a real command result; unavailable tooling or an intentionally skipped check is `not-run` with `details`. A skill MUST NOT manufacture a successful check or replace a failed command with a success-shaped fallback. +### Guidance field semantics -**`review`** is optional for generic implementation skills and required when a skill's instructions mandate post-implementation review. When present, it is the complete findings-report returned by that review skill, not a rewritten summary. +`summary.request` preserves the planned intent and `kind` classifies it without replacing the plan. `candidates` and `selected` are non-negative integer counts: selected equals the number of unique `knowledge` entries and cannot exceed candidates. Counts are retrieval diagnostics, not capability or authoring-quality scores. -**`review-rounds`** records every quality-skill invocation in order. `gating-finding-ids` contains the `blocker` and `major` IDs from that round. `clean` ends successfully; `fixing` means the skill applied justified fixes before another round; `stalled` means the same gating set persisted or no safe progress was possible; `limit-reached` means the configured round cap was exhausted. The array length MUST NOT exceed `quality-round-limit`. `stalled` or `limit-reached` requires implementation outcome `partial`, the final findings-report in `review`, and every unresolved gating item in `remaining`. +`knowledge[].constraints` is a non-empty list summarizing only normative `## Best Practice` and `## Anti Pattern` content from the referenced article. It must not introduce a Business Central fact absent from that article. `used-for` names the concrete plan decision. `sample-paths` contains only sibling samples that exist and were opened. All paths use forward slashes, are repository-relative, and must resolve inside the recorded BCQuality checkout; absolute paths, traversal, and links escaping that checkout are invalid. Every reference is subject to the reference-integrity gate. -**`suppressed`** has the same semantics as in a findings-report and records applicable knowledge excluded by layer precedence or configuration. +`validation-considerations` states evidence the implementation workflow should obtain; it does not claim that a command or test has run. `suppressed` has the same shape and semantics as in a findings-report. `unresolved` records missing repository context or plan decisions that prevent a reliable constraint. Unknown applicability dimensions must appear in both `context.unknown` and a relevant unresolved entry, explaining whether they materially affect a candidate. An unknown dimension is not itself a failure or proof that relevant knowledge exists. -**`remaining`** contains concrete unfinished work only. It is empty for `completed`. +Reference SHAs, when present, identify the files read; they do not prove runtime pinning on their own. The consumer records and verifies the actual immutable BCQuality checkout used for both enrichment and final review, plus its filtering policy and run provenance outside the target repository. See [agent-consumption.md](../agent-consumption.md). ## Composition (super-skills) @@ -465,4 +393,4 @@ Conforms to the DO output contract. ## How orchestrators consume output -An orchestrator invokes an action skill with an input appropriate to the skill's declared `inputs` and uses the single output kind declared in frontmatter. It maps a `findings-report` to PR comments, build gates, or IDE diagnostics; a `development-guidance-report` to constraints for a downstream implementation workflow; and an `implementation-report` to a coding-session summary, changed-file view, validation status, and any remaining work. The orchestrator MUST NOT interpret fields beyond the three schemas above. +An orchestrator invokes an action skill with an input appropriate to the skill's declared `inputs` and uses the single output kind declared in frontmatter. It maps a `findings-report` to PR comments, build gates, or IDE diagnostics, and a `development-guidance-report` to additional constraints for its existing implementation workflow. These are the two output schemas defined by this contract; the consumer retains ownership of implementation and delivery. diff --git a/skills/entry.md b/skills/entry.md index aefab7d9..ffc7231d 100644 --- a/skills/entry.md +++ b/skills/entry.md @@ -21,7 +21,6 @@ The agent invokes Entry with a **task context** supplied by the orchestrator: task-context: goal: string # free-text description of what needs doing inputs-available: # values the orchestrator has ready to pass to a chosen skill - - development-request - development-plan - repository - pr-diff @@ -38,7 +37,7 @@ task-context: ## Preparation — knowledge index -Before routing, ensure the knowledge index is current for the **live** clone. The dispatched review skills read `knowledge-index.json` (at the clone root) at their Source step instead of opening every knowledge file — see READ's [Retrieval workflow](read.md). When a consumer prunes its clone to policy *before* the agent runs, the index MUST be built over the clone as it exists now, so it lists exactly the articles that survived pruning and never an article the consumer denied: +Before routing, ensure the knowledge index is current for the **live** clone. The dispatched skills read `knowledge-index.json` (by default at the clone root) at their Source step instead of opening every knowledge file — see READ's [Retrieval workflow](read.md). When a consumer prunes its clone to policy *before* the agent runs, the index MUST be built over the clone as it exists now, so it lists exactly the articles that survived pruning and never an article the consumer denied: - If `knowledge-index.json` is absent — or you cannot confirm it reflects the current knowledge tree — regenerate it by running, from the checkout root: @@ -48,6 +47,7 @@ Before routing, ensure the knowledge index is current for the **live** clone. Th It defaults to indexing this checkout and writes `knowledge-index.json` at the root in well under a second. When in doubt, rebuild: a sub-second rebuild is always cheaper than a stale or over-listing index, which is a correctness risk. - The paths above assume the checkout root is the current directory. A caller that enters Entry from elsewhere — a plugin host, whose working directory is the user's own project — MUST resolve them against the BCQuality root it already knows instead. The generator resolves its own root, so invoking it by absolute path indexes and writes the right tree. +- For read-only plan enrichment, generated artifacts MUST remain outside the target repository. When the target contains the BCQuality checkout, or that checkout is immutable, pass the generator's `-IndexPath` to an external runner-owned artifact location and supply that resolved index path to the dispatched skill. Do not regenerate inside the target or modify the immutable checkout. If generation is unavailable, use READ's path-based discovery; retrieval failure is not an empty corpus. - Pruning is the consumer's job, not Entry's, and not every consumer does it: an installation that ships the whole tree gets no deny guarantee from this step. There, `enabled-layers` narrows discovery only, and the unlisted layers' files remain on disk. - This is a side step. It MUST NOT change Entry's output — the dispatch record below is the only thing Entry emits, and build logs are never part of the dispatch JSON. @@ -123,11 +123,11 @@ Emit a single JSON document conforming to the output contract below. Entry does **`dispatch[]`** — each entry names one action skill to invoke. -- `skill.path` — repo-relative, forward slashes. The agent fetches and executes the file directly from this path. +- `skill.path` — repo-relative, forward slashes, copied from discovery. Resolve it inside the live BCQuality checkout; reject absolute paths, traversal, and links escaping that checkout. The agent reads the action skill at this exact path rather than constructing a plausible filename. - `skill.version` — copied from the dispatched skill's frontmatter so the orchestrator can detect drift between dispatch time and execution. - `rationale` — short human-readable string, for logs and traceability. - `inputs` — the intersection of `task-context.inputs-available` and the skill's declared `inputs`. The agent MUST pass exactly this subset when invoking the skill. Sending a strict intersection avoids accidental information leakage between skills. -- `outputs` — the dispatched skill's complete, single-element `outputs` value copied from frontmatter. This lets an orchestrator distinguish read-only `findings-report` and `development-guidance-report` work from repository-changing `implementation-report` work before invoking the skill. An orchestrator MAY require an additional write confirmation for `implementation-report`; it MUST NOT infer side effects from the skill ID or title. +- `outputs` — the dispatched skill's complete, single-element `outputs` value copied from frontmatter. This lets an orchestrator distinguish `findings-report` from read-only `development-guidance-report` before invocation. Check the declared contract and actual skill; output metadata is not a sandbox or proof of side effects. Unknown output kinds must not be silently treated as a supported report. Ordering of `dispatch[]` is not significant. @@ -180,7 +180,7 @@ Populated example (PR review on a repo where only `al-performance-review` is ena 1. Invoke Entry with the orchestrator-supplied task context. 2. Receive the dispatch record. -3. For each entry in `dispatch[]`, inspect `outputs` before invocation, read the referenced action skill, execute its Source → Relevance → Worklist → Action steps per DO, and produce the declared report kind. Verify the file's frontmatter output still equals the dispatch value; return `failed` on drift rather than executing an unexpectedly mutating skill. +3. For each entry in `dispatch[]`, inspect `outputs` before invocation, read the referenced action skill, execute its Source → Relevance → Worklist → Action steps per DO, and produce the declared report kind. Verify the file's frontmatter output still equals the dispatch value; return `failed` on drift rather than executing a different contract. 4. Return the action-skill reports to the orchestrator. When Entry's `outcome` is `no-match` or `failed`, return the dispatch record itself so the orchestrator can log the reason. READ and DO are the contracts that govern what the dispatched skills do. An agent that has not yet read READ and DO reads them when it executes the first dispatched skill — they are not prerequisites for invoking Entry. diff --git a/tools/DevelopmentGuidance.Evidence.ps1 b/tools/DevelopmentGuidance.Evidence.ps1 new file mode 100644 index 00000000..c450f647 --- /dev/null +++ b/tools/DevelopmentGuidance.Evidence.ps1 @@ -0,0 +1,496 @@ +# Helpers for Test-DevelopmentGuidanceFixtures.ps1 and its deterministic regressions. +function Get-GuidanceDiagnostic { + param($ErrorRecord) + if ($ErrorRecord.Exception -is [Management.Automation.RuntimeException] -and + $ErrorRecord.FullyQualifiedErrorId -eq $ErrorRecord.Exception.Message) { + return $ErrorRecord.Exception.Message + } + return 'Evidence or report could not be read safely (missing, malformed, inaccessible or unsupported state).' +} + +function Assert-GuidanceObject { + param($Value, [string] $Label, [string[]] $Fields = @()) + if ($Value -isnot [Collections.IDictionary]) { throw "$Label must be a JSON object." } + foreach ($field in $Fields) { + if (-not $Value.Contains($field)) { throw "$Label is missing required field '$field'." } + } +} + +function Assert-GuidanceString { + param($Value, [string] $Label) + if ($Value -isnot [string] -or [string]::IsNullOrWhiteSpace($Value)) { throw "$Label must be a non-empty string." } +} + +function Assert-GuidanceArray { + param($Value, [string] $Label, [switch] $Strings, [switch] $NonEmpty) + if ($Value -isnot [array]) { throw "$Label must be a JSON array." } + if ($NonEmpty -and $Value.Count -eq 0) { throw "$Label must not be empty." } + if ($Strings) { foreach ($item in $Value) { Assert-GuidanceString $item "$Label entry" } } +} + +function Assert-GuidanceInteger { + param($Value, [string] $Label) + if (($Value -isnot [long] -and $Value -isnot [int] -and $Value -isnot [bigint]) -or $Value -lt 0) { + throw "$Label must be a non-negative integer." + } +} + +function Read-GuidanceJson { + param([string] $Path) + function Assert-JsonMembers($Element) { + if ($Element.ValueKind -eq [Text.Json.JsonValueKind]::Object) { + $names = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) + foreach ($property in $Element.EnumerateObject()) { + if (-not $names.Add($property.Name)) { throw 'JSON contains duplicate or case-ambiguous members.' } + Assert-JsonMembers $property.Value + } + } elseif ($Element.ValueKind -eq [Text.Json.JsonValueKind]::Array) { + foreach ($item in $Element.EnumerateArray()) { Assert-JsonMembers $item } + } + } + try { + if ((Get-Item -LiteralPath $Path -Force).Length -gt 32MB) { throw 'Oversized JSON.' } + $text = [IO.File]::ReadAllText($Path) + $document = [Text.Json.JsonDocument]::Parse($text) + try { Assert-JsonMembers $document.RootElement } finally { $document.Dispose() } + return ConvertFrom-Json -InputObject $text -AsHashtable -Depth 64 -NoEnumerate + } catch { throw 'Input is not readable, strict JSON with unique members.' } +} + +function Get-GuidanceHash { + param([string] $Path) + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash +} + +function Get-GuidanceCaseId { + param([string] $Id) + $bytes = [Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($Id)) + return "case-$([Convert]::ToHexString($bytes).Substring(0, 8).ToLowerInvariant())" +} + +function Test-GuidanceWithin { + param([string] $Path, [string] $Parent) + $comparison = if ($IsWindows) { [StringComparison]::OrdinalIgnoreCase } else { [StringComparison]::Ordinal } + $prefix = $Parent.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar + return $Path.Equals($Parent, $comparison) -or $Path.StartsWith($prefix, $comparison) +} + +function Assert-GuidanceItem { + param($Item) + if (($Item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $Item.LinkType -or $Item.LinkTarget) { + throw 'Links, junctions, hard links and reparse points are not supported.' + } + if (-not $Item.PSIsContainer -and $IsWindows) { + $streams = @(Get-Item -LiteralPath $Item.FullName -Stream '*' -Force -ErrorAction Stop) + if (@($streams | Where-Object Stream -ne ':$DATA').Count) { + throw 'Alternate data streams are not supported.' + } + } +} + +function Get-GuidanceSafePath { + param([string] $Path, [switch] $AllowMissing, [switch] $Directory, [switch] $File) + Assert-GuidanceString $Path 'Filesystem path' + $full = [IO.Path]::GetFullPath($Path) + $driveRoot = [IO.Path]::GetPathRoot($full) + if ($IsWindows -and ($driveRoot.StartsWith('\\') -or $full.Substring($driveRoot.Length).Contains(':'))) { + throw 'Network paths and alternate stream paths are not supported.' + } + $cursor = $driveRoot + $components = $full.Substring($driveRoot.Length).Split([IO.Path]::DirectorySeparatorChar, [StringSplitOptions]::RemoveEmptyEntries) + foreach ($component in $components) { + if ($component -match '[\. ]$') { throw 'Ambiguous filesystem path components are not supported.' } + $cursor = Join-Path $cursor $component + # Get-Item sees dangling links that Test-Path may treat as missing. + $item = Get-Item -LiteralPath $cursor -Force -ErrorAction SilentlyContinue + if ($null -ne $item) { Assert-GuidanceItem $item } + elseif (-not $AllowMissing) { throw 'Required filesystem path is missing.' } + } + if ($Directory -and -not (Test-Path -LiteralPath $full -PathType Container)) { throw 'Required directory is missing.' } + if ($File -and -not (Test-Path -LiteralPath $full -PathType Leaf)) { throw 'Required file is missing.' } + return $full.TrimEnd([IO.Path]::DirectorySeparatorChar) +} + +function Assert-GuidanceTree { + param([string] $Root) + $pending = [Collections.Generic.Stack[string]]::new() + $pending.Push($Root) + while ($pending.Count) { + foreach ($item in Get-ChildItem -LiteralPath $pending.Pop() -Force) { + Assert-GuidanceItem $item + if ($item.PSIsContainer) { $pending.Push($item.FullName) } + } + } +} + +function Invoke-GuidanceGit { + param([string] $Root, [string[]] $Arguments, [switch] $RawOutput) + $start = [Diagnostics.ProcessStartInfo]::new('git') + $start.UseShellExecute = $false + $start.RedirectStandardOutput = $true + $start.RedirectStandardError = $true + foreach ($variable in @('GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE', + 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CONFIG', + 'GIT_CONFIG_COUNT', 'GIT_CONFIG_PARAMETERS', 'GIT_NAMESPACE')) { + $null = $start.Environment.Remove($variable) + } + $start.Environment['GIT_CONFIG_NOSYSTEM'] = '1' + $start.Environment['GIT_CONFIG_GLOBAL'] = '' + $start.Environment['GIT_TERMINAL_PROMPT'] = '0' + # In particular, do not execute a repository-supplied fsmonitor hook on reads. + foreach ($argument in (@('--no-optional-locks', '-c', 'core.fsmonitor=false', '-C', $Root) + $Arguments)) { + $start.ArgumentList.Add($argument) + } + $process = [Diagnostics.Process]::Start($start) + try { + $output = $process.StandardOutput.ReadToEndAsync() + $errors = $process.StandardError.ReadToEndAsync() + $process.WaitForExit() + $null = $errors.GetAwaiter().GetResult() + if ($process.ExitCode -ne 0) { throw 'Git evidence could not be read.' } + $text = $output.GetAwaiter().GetResult() + if ($RawOutput) { return $text } + return $text.TrimEnd("`r", "`n") + } finally { $process.Dispose() } +} + +function Get-GuidanceSnapshot { + param([string] $Root, [switch] $Target) + $Root = Get-GuidanceSafePath $Root -Directory + $dotGit = Get-GuidanceSafePath (Join-Path $Root '.git') + if (Test-Path -LiteralPath $dotGit -PathType Container) { + $gitDir = $dotGit + } else { + if ($Target) { throw 'Target workspaces must be standalone repositories with internal Git storage.' } + $pointer = [IO.File]::ReadAllText($dotGit) + if ($pointer -notmatch '\Agitdir: ([^\r\n]+)\r?\n?\z') { throw 'Unsupported Git worktree pointer.' } + $gitPath = $Matches[1] + if (-not [IO.Path]::IsPathFullyQualified($gitPath)) { $gitPath = Join-Path $Root $gitPath } + $gitDir = Get-GuidanceSafePath $gitPath -Directory + } + $commonDir = $gitDir + $commonPointer = Get-GuidanceSafePath (Join-Path $gitDir 'commondir') -AllowMissing + if (Test-Path -LiteralPath $commonPointer) { + $commonPath = [IO.File]::ReadAllText($commonPointer).Trim() + if (-not [IO.Path]::IsPathFullyQualified($commonPath)) { $commonPath = Join-Path $gitDir $commonPath } + $commonDir = Get-GuidanceSafePath $commonPath -Directory + } + if ($Target -and ($gitDir -cne (Join-Path $Root '.git') -or $commonDir -cne $gitDir)) { + throw 'Target workspaces must be standalone repositories with internal Git storage.' + } + $files = [Collections.Generic.List[object]]::new() + $pending = [Collections.Generic.Stack[string]]::new() + $pending.Push($Root) + while ($pending.Count) { + foreach ($item in @(Get-ChildItem -LiteralPath $pending.Pop() -Force | Sort-Object Name -CaseSensitive)) { + Assert-GuidanceItem $item + $relative = [IO.Path]::GetRelativePath($Root, $item.FullName).Replace('\', '/') + $entry = [ordered]@{ + path = $relative + kind = if ($item.PSIsContainer) { 'directory' } else { 'file' } + attributes = [int]$item.Attributes + unixMode = [int]$item.UnixFileMode + creationUtcTicks = $item.CreationTimeUtc.Ticks + } + if ($item.PSIsContainer) { + $pending.Push($item.FullName) + } else { + $entry.length = $item.Length + $entry.lastWriteUtcTicks = $item.LastWriteTimeUtc.Ticks + $entry.sha256 = Get-GuidanceHash $item.FullName + } + $files.Add($entry) + } + } + # Linked knowledge worktrees have explicitly identified Git metadata outside + # the content root. Record its meaningful configuration as well as HEAD/refs. + $gitMetadata = [ordered]@{} + foreach ($storage in @($gitDir, $commonDir) | Sort-Object -Unique) { + foreach ($name in @('HEAD', 'commondir', 'config', 'config.worktree', 'packed-refs', 'refs', 'objects', 'index', 'info\exclude', 'shallow')) { + $path = Get-GuidanceSafePath (Join-Path $storage $name) -AllowMissing + if ((Test-Path -LiteralPath $path -PathType Container) -and -not (Test-GuidanceWithin $path $Root)) { + Assert-GuidanceTree $path + } + if (Test-Path -LiteralPath $path -PathType Leaf) { + $gitMetadata[$path] = Get-GuidanceHash $path + } + if ($name -in @('config', 'config.worktree') -and (Test-Path -LiteralPath $path) -and + [IO.File]::ReadAllText($path) -match '(?im)^\s*\[include(?:If)?\b') { + throw 'External Git configuration includes are not supported.' + } + } + foreach ($name in @('objects\info\alternates', 'objects\info\http-alternates')) { + if (Test-Path -LiteralPath (Join-Path $storage $name)) { throw 'External Git object stores are not supported.' } + } + } + $top = Get-GuidanceSafePath (Invoke-GuidanceGit $Root @('rev-parse', '--show-toplevel')) -Directory + if ($top -cne $Root) { throw 'Evidence requires the exact Git worktree root, not a subdirectory.' } + $indexPath = Get-GuidanceSafePath (Join-Path $gitDir 'index') -AllowMissing + $tracked = Invoke-GuidanceGit $Root @('ls-files', '--stage') + if ($tracked -match '(?m)^160000 ') { throw 'Submodule workspaces are not supported.' } + if ((Invoke-GuidanceGit $Root @('ls-files', '-t')) -match '(?m)^S ') { throw 'Sparse workspaces are not supported.' } + $head = Invoke-GuidanceGit $Root @('rev-parse', '--verify', 'HEAD') + $headFile = Get-GuidanceSafePath (Join-Path $gitDir 'HEAD') -File + # Access times, Git status refreshes, and index-builder generatedAt are not evidence. + return [ordered]@{ + version = 1 + root = $Root + rootCreationUtcTicks = (Get-Item -LiteralPath $Root -Force).CreationTimeUtc.Ticks + gitDir = $gitDir + commonDir = $commonDir + gitMetadata = $gitMetadata + head = $head + headFileSha256 = Get-GuidanceHash $headFile + references = Invoke-GuidanceGit $Root @('for-each-ref', '--format=%(refname) %(objectname) %(symref)') + indexPath = $indexPath + indexSha256 = if (Test-Path -LiteralPath $indexPath) { Get-GuidanceHash $indexPath } else { $null } + files = @($files | Sort-Object { $_.path } -CaseSensitive) + } +} + +function Test-GuidanceSnapshotEqual { + param($Before, $After) + return ($Before | ConvertTo-Json -Depth 64 -Compress) -ceq ($After | ConvertTo-Json -Depth 64 -Compress) +} + +function Write-GuidanceNewJson { + param([string] $Path, $Value) + $parent = Split-Path -Parent $Path + [IO.Directory]::CreateDirectory($parent) | Out-Null + $bytes = [Text.Encoding]::UTF8.GetBytes(($Value | ConvertTo-Json -Depth 64)) + $stream = [IO.File]::Open($Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) + try { $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Dispose() } +} + +function Resolve-GuidanceReference { + param([string] $Root, $Reference, [switch] $Knowledge, [string] $Article) + Assert-GuidanceString $Reference 'Reference path' + if ($Reference -cnotmatch '^[a-zA-Z0-9_-]+(?:/[a-zA-Z0-9_.-]+)+$' -or + @($Reference.Split('/') | Where-Object { $_ -in @('.', '..') -or $_ -match '[\. ]$' }).Count) { + throw 'Reference must be an unambiguous forward-slash repository-relative path without traversal.' + } + if ($Knowledge -and $Reference -cnotmatch '^(microsoft|community|custom)/knowledge/[a-z0-9-]+/(?:[a-z0-9-]+/)*[a-z0-9-]+\.md$') { + throw 'Knowledge references must identify actual layered knowledge articles.' + } + if ($Article) { + $stem = $Article.Substring(0, $Article.Length - 3) + if ($Reference -cnotmatch ('^' + [regex]::Escape($stem) + '\.(good|bad)\.[a-zA-Z0-9]+$')) { + throw 'Sample references must be good/bad siblings of their knowledge article.' + } + } + $cursor = $Root + foreach ($component in $Reference.Split('/')) { + $cursor = Join-Path $cursor $component + $cursor = Get-GuidanceSafePath $cursor + if ((Get-Item -LiteralPath $cursor -Force).Name -cne $component) { throw 'Reference path casing must match the actual file.' } + } + if (-not (Test-GuidanceWithin $cursor $Root) -or -not (Test-Path -LiteralPath $cursor -PathType Leaf)) { + throw 'Reference must resolve to an existing file inside the knowledge checkout.' + } + if ($Knowledge) { + $text = [IO.File]::ReadAllText($cursor) + if ($text -notmatch '(?s)^---\r?\n.*?\r?\n---' -or + $text -notmatch '(?m)^domain:\s*\S+' -or $text -notmatch '(?m)^## (Best Practice|Anti Pattern)\s*$') { + throw 'Knowledge reference does not contain a normative knowledge article.' + } + } + return $cursor +} + +function Get-GuidancePlanRequest { + param($Plan) + if ($Plan -is [string]) { Assert-GuidanceString $Plan 'development-plan'; return $Plan } + Assert-GuidanceObject $Plan 'development-plan' @('request') + Assert-GuidanceString $Plan.request 'development-plan.request' + return $Plan.request +} + +function Assert-GuidanceContext { + param($Context) + Assert-GuidanceObject $Context 'context' @('bc-version', 'technologies', 'countries', 'application-area', 'unknown') + Assert-GuidanceString $Context.'bc-version' 'context.bc-version' + foreach ($key in @('technologies', 'countries', 'application-area', 'unknown')) { + Assert-GuidanceArray $Context[$key] "context.$key" -Strings + if (@($Context[$key] | Sort-Object -Unique).Count -ne $Context[$key].Count) { throw "context.$key contains duplicates." } + } + foreach ($key in $Context.unknown) { + if ($key -cnotin @('bc-version', 'technologies', 'countries', 'application-area')) { throw 'context.unknown contains an invalid dimension.' } + } + if ($Context.'bc-version' -eq 'unknown' -and 'bc-version' -cnotin $Context.unknown) { + throw 'Unknown BC version must be recorded in context.unknown.' + } + foreach ($key in @('technologies', 'countries', 'application-area')) { + if ((-not $Context[$key].Count -or 'unknown' -in $Context[$key]) -and $key -cnotin $Context.unknown) { + throw 'Unavailable applicability dimensions must be recorded in context.unknown.' + } + } +} + +function Assert-GuidanceManifest { + param($Manifest, [string] $Root) + Assert-GuidanceObject $Manifest 'manifest' @('version', 'minimumKnowledgeRecall', 'minimumKnowledgePrecision', 'skill', 'cases') + if ($Manifest.version -ne 1) { throw 'Unsupported guidance fixture manifest version.' } + foreach ($name in @('minimumKnowledgeRecall', 'minimumKnowledgePrecision')) { + $value = $Manifest[$name] + if (($value -isnot [double] -and $value -isnot [long] -and $value -isnot [int] -and $value -isnot [decimal]) -or + $value -lt 0 -or $value -gt 1) { throw 'Manifest thresholds must be numbers between zero and one.' } + } + $null = Resolve-GuidanceReference $Root $Manifest.skill + Assert-GuidanceArray $Manifest.cases 'manifest.cases' -NonEmpty + $ids = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + $modelIds = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + foreach ($case in $Manifest.cases) { + Assert-GuidanceObject $case 'manifest case' @('id', 'expectedKind', 'expectedOutcome', 'development-plan', 'context', 'requiredKnowledge', 'optionalKnowledge', 'expectedUnknown', 'requiresUnresolved', 'requiresMaterialUnresolved') + if ($case.id -isnot [string] -or $case.id -cnotmatch '^[a-z0-9]+(?:-[a-z0-9]+)*$') { throw 'Fixture id must be kebab-case.' } + if (-not $ids.Add($case.id) -or -not $modelIds.Add((Get-GuidanceCaseId $case.id))) { throw 'Duplicate fixture or model case identity.' } + if ($case.expectedKind -cnotin @('feature', 'bug', 'refactor', 'upgrade', 'maintenance')) { throw 'Fixture expectedKind is invalid.' } + if ($case.expectedOutcome -cnotin @('completed', 'not-applicable', 'no-knowledge', 'partial', 'failed')) { throw 'Fixture expectedOutcome is invalid.' } + Assert-GuidanceString $case.expectedKind 'fixture.expectedKind' + Assert-GuidanceString $case.expectedOutcome 'fixture.expectedOutcome' + $null = Get-GuidancePlanRequest $case.'development-plan' + Assert-GuidanceContext $case.context + foreach ($name in @('requiredKnowledge', 'optionalKnowledge', 'expectedUnknown')) { + Assert-GuidanceArray $case[$name] "fixture.$name" -Strings + } + if ($case.requiresUnresolved -isnot [bool]) { throw 'Fixture requiresUnresolved must be boolean.' } + if ($case.requiresMaterialUnresolved -isnot [bool]) { throw 'Fixture requiresMaterialUnresolved must be boolean.' } + if ($case.requiresMaterialUnresolved -and ($case.expectedOutcome -ne 'partial' -or -not $case.requiresUnresolved)) { + throw 'Materially unresolved fixtures must expect partial and unresolved evidence.' + } + foreach ($key in $case.expectedUnknown) { + if ($key -cnotin @('bc-version', 'technologies', 'countries', 'application-area')) { throw 'Fixture expectedUnknown is invalid.' } + } + $references = @($case.requiredKnowledge) + @($case.optionalKnowledge) + if (@($references | Sort-Object -Unique).Count -ne $references.Count) { throw 'Fixture knowledge references contain duplicates.' } + foreach ($reference in $references) { $null = Resolve-GuidanceReference $Root $reference -Knowledge } + if ($case.expectedOutcome -in @('no-knowledge', 'not-applicable') -and $references.Count) { + throw 'Empty-knowledge outcomes cannot require or accept knowledge.' + } + } +} + +function Assert-GuidanceResult { + param($Result, $Case, $Manifest, [string] $Root, [string] $Workspace) + Assert-GuidanceObject $Result 'result' @('caseId', 'guidanceReport') + Assert-GuidanceString $Result.caseId 'result.caseId' + if ($Result.caseId -cne (Get-GuidanceCaseId $Case.id)) { throw 'Result caseId mismatch.' } + if ($Result.Contains('workspaceRoot')) { + Assert-GuidanceString $Result.workspaceRoot 'result.workspaceRoot' + if (-not [IO.Path]::IsPathFullyQualified($Result.workspaceRoot) -or + (Get-GuidanceSafePath $Result.workspaceRoot -Directory) -cne $Workspace) { + throw 'Result workspaceRoot disagrees with the runner binding.' + } + } + $report = $Result.guidanceReport + Assert-GuidanceObject $report 'guidanceReport' @('skill', 'outcome', 'summary', 'context', 'knowledge', 'validation-considerations', 'suppressed', 'unresolved') + Assert-GuidanceObject $report.skill 'skill' @('id', 'version') + Assert-GuidanceString $report.skill.id 'skill.id' + if ($report.skill.id -cne 'al-development-plan' -or $report.skill.version -isnot [long] -or $report.skill.version -ne 1) { + throw 'Report skill identity/version is invalid.' + } + Assert-GuidanceString $report.outcome 'outcome' + if ($report.outcome -cnotin @('completed', 'not-applicable', 'no-knowledge', 'partial', 'failed')) { throw 'Report outcome enum is invalid.' } + if ($report.outcome -cne $Case.expectedOutcome) { throw 'Report outcome does not match fixture expectedOutcome.' } + if ($report.outcome -in @('partial', 'failed') -or $report.Contains('outcome-reason')) { + Assert-GuidanceString $report['outcome-reason'] 'outcome-reason' + } + Assert-GuidanceObject $report.summary 'summary' @('request', 'kind', 'candidates', 'selected') + Assert-GuidanceString $report.summary.request 'summary.request' + Assert-GuidanceString $report.summary.kind 'summary.kind' + if ($report.summary.kind -cne $Case.expectedKind) { throw 'summary.kind does not match the intended change.' } + Assert-GuidanceInteger $report.summary.candidates 'summary.candidates' + Assert-GuidanceInteger $report.summary.selected 'summary.selected' + Assert-GuidanceContext $report.context + foreach ($name in @('knowledge', 'validation-considerations', 'suppressed', 'unresolved')) { + Assert-GuidanceArray $report[$name] $name + } + Assert-GuidanceArray $report.unresolved 'unresolved' -Strings + if ($report.summary.selected -ne $report.knowledge.Count -or $report.summary.selected -gt $report.summary.candidates) { + throw 'Summary counts disagree with selected knowledge/candidates.' + } + if ($report.outcome -in @('no-knowledge', 'not-applicable') -and $report.knowledge.Count) { throw 'This outcome requires empty knowledge.' } + if ($report.outcome -eq 'completed' -and -not $report.knowledge.Count) { throw 'Completed requires selected knowledge; empty evaluation is no-knowledge.' } + if (($report.outcome -eq 'partial' -or $Case.requiresUnresolved) -and -not $report.unresolved.Count) { + throw 'Partial/incomplete evaluation must explain unresolved gaps.' + } + foreach ($dimension in $Case.expectedUnknown) { + if ($dimension -cnotin $report.context.unknown) { throw 'Expected unknown context was silently resolved.' } + } + foreach ($dimension in $report.context.unknown) { + if (-not @($report.unresolved | Where-Object { $_ -match [regex]::Escape($dimension) }).Count) { + throw 'Unknown dimensions require a corresponding unresolved explanation.' + } + } + # Free-form unresolved text has no machine-readable materiality field in DO. + # Known material fixture conditions are runner expectations, not model claims. + if ($Case.requiresMaterialUnresolved -and $report.outcome -ne 'partial') { throw 'Material unknown guidance must remain partial.' } + $used = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + foreach ($entry in $report.knowledge) { + Assert-GuidanceObject $entry 'knowledge entry' @('path', 'used-for', 'constraints', 'sample-paths') + $null = Resolve-GuidanceReference $Root $entry.path -Knowledge + if (-not $used.Add($entry.path)) { throw 'Duplicate knowledge reference.' } + Assert-GuidanceString $entry.'used-for' 'knowledge.used-for' + Assert-GuidanceArray $entry.constraints 'knowledge.constraints' -Strings -NonEmpty + Assert-GuidanceArray $entry.'sample-paths' 'knowledge.sample-paths' -Strings + if (@($entry.'sample-paths' | Sort-Object -Unique).Count -ne $entry.'sample-paths'.Count) { throw 'Duplicate sample reference.' } + foreach ($sample in $entry.'sample-paths') { $null = Resolve-GuidanceReference $Root $sample -Article $entry.path } + Assert-GuidanceReferenceSha $entry $Root + } + $validationIds = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + foreach ($entry in $report.'validation-considerations') { + Assert-GuidanceObject $entry 'validation consideration' @('id', 'reason', 'evidence') + foreach ($key in @('id', 'reason', 'evidence')) { Assert-GuidanceString $entry[$key] "validation-considerations.$key" } + if (-not $validationIds.Add($entry.id)) { throw 'Duplicate validation consideration id.' } + } + $suppressed = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + foreach ($entry in $report.suppressed) { + Assert-GuidanceObject $entry 'suppressed entry' @('reference', 'reason') + Assert-GuidanceObject $entry.reference 'suppressed.reference' @('path') + $null = Resolve-GuidanceReference $Root $entry.reference.path -Knowledge + Assert-GuidanceReferenceSha $entry.reference $Root + Assert-GuidanceString $entry.reason 'suppression reason' + if ($entry.reason -cnotin @('layer-precedence', 'configuration')) { throw 'Suppression reason is invalid.' } + if (-not $suppressed.Add($entry.reference.path) -or $used.Contains($entry.reference.path)) { throw 'Duplicate or selected suppressed reference.' } + } + $matched = @($Case.requiredKnowledge | Where-Object { $used.Contains($_) }).Count + $recall = if ($Case.requiredKnowledge.Count) { $matched / $Case.requiredKnowledge.Count } else { 1.0 } + $accepted = @($Case.requiredKnowledge) + @($Case.optionalKnowledge) + $acceptedCount = @($used | Where-Object { $_ -cin $accepted }).Count + $precision = if ($used.Count) { $acceptedCount / $used.Count } elseif (-not $Case.requiredKnowledge.Count) { 1.0 } else { 0.0 } + if ($recall -lt $Manifest.minimumKnowledgeRecall) { throw 'Knowledge recall is below the manifest threshold.' } + if ($precision -lt $Manifest.minimumKnowledgePrecision) { throw 'Knowledge precision is below the manifest threshold.' } +} + +function Assert-GuidanceReferenceSha { + param($Entry, [string] $Root) + if ($Entry.Contains('sha')) { + if ($Entry.sha -isnot [string] -or $Entry.sha -cnotmatch '^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$') { + throw 'Reference SHA must be a full commit object id.' + } + $head = Invoke-GuidanceGit $Root @('rev-parse', '--verify', 'HEAD') + if ($Entry.sha -ne $head) { throw 'Reference SHA does not identify the recorded live checkout.' } + $committed = Invoke-GuidanceGit $Root @('cat-file', 'blob', "$head`:$($Entry.path)") -RawOutput + $live = [IO.File]::ReadAllText((Resolve-GuidanceReference $Root $Entry.path -Knowledge)) + if ($committed.Replace("`r`n", "`n") -cne $live.Replace("`r`n", "`n")) { + throw 'Reference SHA content differs from the live knowledge article.' + } + } +} + +function Get-GuidanceResultSchema { + param([string] $CaseId) + return [ordered]@{ + caseId = $CaseId + guidanceReport = [ordered]@{ + skill = [ordered]@{ id = 'al-development-plan'; version = 1 } + outcome = 'completed | not-applicable | no-knowledge | partial | failed' + 'outcome-reason' = 'required for partial or failed' + summary = [ordered]@{ request = 'planned intent'; kind = 'feature | bug | refactor | upgrade | maintenance'; candidates = 0; selected = 0 } + context = [ordered]@{ 'bc-version' = 'resolved target or unknown'; technologies = @('al'); countries = @('w1'); 'application-area' = @('all'); unknown = @() } + knowledge = @([ordered]@{ path = 'repo-relative knowledge article'; sha = 'optional full checkout commit id'; 'used-for' = 'plan decision'; constraints = @('faithful normative constraint'); 'sample-paths' = @() }) + 'validation-considerations' = @([ordered]@{ id = 'stable id'; reason = 'why needed'; evidence = 'evidence implementation should obtain' }) + suppressed = @([ordered]@{ reference = [ordered]@{ path = 'suppressed knowledge path' }; reason = 'layer-precedence | configuration' }) + unresolved = @('Missing context/decision, affected candidate, and materiality; name unknown dimensions exactly.') + } + } +} diff --git a/tools/Test-DevelopmentFixtures.ps1 b/tools/Test-DevelopmentFixtures.ps1 deleted file mode 100644 index b5dd29ca..00000000 --- a/tools/Test-DevelopmentFixtures.ps1 +++ /dev/null @@ -1,584 +0,0 @@ -<# -.SYNOPSIS - Validates and prepares BCQuality AL development fixtures. - -.DESCRIPTION - Static validation checks fixture IDs, capability links, knowledge references, - and the development skill. -PrepareDirectory emits opaque requests for model - runs. -ResultsDirectory scores implementation reports produced by an - external runner that performed the declared compile, test, and review checks. -#> -[CmdletBinding()] -param( - [string] $Root = (Resolve-Path (Join-Path $PSScriptRoot '..')), - [string] $ManifestPath, - [string] $CapabilitiesPath, - [string] $PrepareDirectory, - [string] $ResultsDirectory -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$Root = (Resolve-Path -LiteralPath $Root).Path -if (-not $ManifestPath) { - $ManifestPath = Join-Path $Root 'evaluation/development-fixtures.json' -} -if (-not $CapabilitiesPath) { - $CapabilitiesPath = Join-Path $Root 'coverage/development-capabilities.json' -} - -$manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json -$capabilityManifest = Get-Content -LiteralPath $CapabilitiesPath -Raw | ConvertFrom-Json -$problems = [System.Collections.Generic.List[string]]::new() - -function Get-ModelCaseId { - param([string] $ManifestId) - - $sha = [System.Security.Cryptography.SHA256]::Create() - try { - $bytes = [System.Text.Encoding]::UTF8.GetBytes($ManifestId) - $hash = $sha.ComputeHash($bytes) - $token = ([System.BitConverter]::ToString($hash) -replace '-', '').Substring(0, 8).ToLowerInvariant() - return "case-$token" - } finally { - $sha.Dispose() - } -} - -if ($manifest.version -ne 1) { - $problems.Add("Unsupported development fixture version: $($manifest.version)") | Out-Null -} -if ($capabilityManifest.version -ne 1) { - $problems.Add("Unsupported capability manifest version: $($capabilityManifest.version)") | Out-Null -} -$minimumFixtureCoverage = if ($capabilityManifest.PSObject.Properties.Name -contains 'minimumFixtureCoverage') { - [double]$capabilityManifest.minimumFixtureCoverage -} else { - -1 -} -if ($minimumFixtureCoverage -lt 0 -or $minimumFixtureCoverage -gt 1) { - $problems.Add("minimumFixtureCoverage must be between 0 and 1.") | Out-Null -} -$maximumReviewRounds = if ($manifest.PSObject.Properties.Name -contains 'maximumReviewRounds') { - [int]$manifest.maximumReviewRounds -} else { - 0 -} -if ($maximumReviewRounds -le 0) { - $problems.Add("maximumReviewRounds must be a positive integer.") | Out-Null -} -foreach ($thresholdName in @('minimumKnowledgeRecall', 'minimumKnowledgePrecision')) { - $threshold = [double]$manifest.$thresholdName - if ($threshold -lt 0 -or $threshold -gt 1) { - $problems.Add("$thresholdName must be between 0 and 1.") | Out-Null - } -} - -$skillPath = [string]$manifest.skill -if (-not (Test-Path -LiteralPath (Join-Path $Root $skillPath) -PathType Leaf)) { - $problems.Add("Development skill does not exist: $skillPath") | Out-Null -} else { - $skillText = Get-Content -LiteralPath (Join-Path $Root $skillPath) -Raw - $limitMatch = [regex]::Match($skillText, '(?m)^quality-round-limit:\s*(\d+)\s*$') - if (-not $limitMatch.Success -or [int]$limitMatch.Groups[1].Value -ne $maximumReviewRounds) { - $problems.Add("maximumReviewRounds must match the development skill quality-round-limit.") | Out-Null - } -} - -$validChecks = @('compile', 'tests', 'review') -$validInputKinds = @('auto', 'feature', 'bug', 'refactor', 'upgrade', 'maintenance') -$validOutputKinds = @('feature', 'bug', 'refactor', 'upgrade', 'maintenance') -$caseById = @{} -foreach ($case in @($manifest.cases)) { - $id = [string]$case.id - if ($id -notmatch '^[a-z0-9]+(?:-[a-z0-9]+)*$') { - $problems.Add("Fixture id must be kebab-case: '$id'.") | Out-Null - } elseif ($caseById.ContainsKey($id)) { - $problems.Add("Duplicate fixture id: $id") | Out-Null - } else { - $caseById[$id] = $case - } - - if ($case.PSObject.Properties.Name -notcontains 'development-request') { - $problems.Add("${id}: development-request is required.") | Out-Null - continue - } - $request = $case.'development-request' - if ($validInputKinds -notcontains [string]$request.kind) { - $problems.Add("${id}: development-request.kind must be one of $($validInputKinds -join ', ').") | Out-Null - } - $description = if ($request.PSObject.Properties.Name -contains 'description') { - [string]$request.description - } else { - '' - } - $planText = if ($request.PSObject.Properties.Name -contains 'plan') { - [string]$request.plan - } else { - '' - } - if ([string]::IsNullOrWhiteSpace($description) -and [string]::IsNullOrWhiteSpace($planText)) { - $problems.Add("${id}: development-request requires description or plan.") | Out-Null - } - if ($request.PSObject.Properties.Name -notcontains 'acceptance-criteria' -or - -not @($request.'acceptance-criteria').Count) { - $problems.Add("${id}: development-request.acceptance-criteria must not be empty.") | Out-Null - } - $expectedKind = if ($case.PSObject.Properties.Name -contains 'expectedKind') { - [string]$case.expectedKind - } else { - '' - } - if ($validOutputKinds -notcontains $expectedKind) { - $problems.Add("${id}: expectedKind must be one of $($validOutputKinds -join ', ').") | Out-Null - } - if ([string]$request.kind -ne 'auto' -and [string]$request.kind -ne $expectedKind) { - $problems.Add("${id}: explicit request kind '$($request.kind)' must equal expectedKind '$expectedKind'.") | Out-Null - } - - $expectedKnowledge = @($case.requiredKnowledge) + @($case.optionalKnowledge) - if (@($expectedKnowledge | Sort-Object -Unique).Count -ne $expectedKnowledge.Count) { - $problems.Add("${id}: requiredKnowledge and optionalKnowledge contain duplicates.") | Out-Null - } - foreach ($reference in $expectedKnowledge) { - $reference = [string]$reference - if ($reference.Contains('\') -or -not $reference.EndsWith('.md')) { - $problems.Add("${id}: invalid knowledge path: $reference") | Out-Null - } elseif (-not (Test-Path -LiteralPath (Join-Path $Root $reference) -PathType Leaf)) { - $problems.Add("${id}: knowledge article does not exist: $reference") | Out-Null - } - } - foreach ($check in @($case.requiredChecks)) { - if ($validChecks -notcontains [string]$check) { - $problems.Add("${id}: unsupported required check '$check'.") | Out-Null - } - } -} - -$validCapabilityStatuses = @('planned', 'fixture', 'validated') -$capabilityIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) -foreach ($capability in @($capabilityManifest.capabilities)) { - $id = [string]$capability.id - if (-not $capabilityIds.Add($id)) { - $problems.Add("Duplicate capability id: $id") | Out-Null - } - if ($validCapabilityStatuses -notcontains [string]$capability.status) { - $problems.Add("${id}: invalid capability status '$($capability.status)'.") | Out-Null - } - $fixtureIds = @($capability.fixtureIds) - if ($capability.status -in @('fixture', 'validated') -and -not $fixtureIds.Count) { - $problems.Add("${id}: status '$($capability.status)' requires at least one fixture.") | Out-Null - } - foreach ($fixtureId in $fixtureIds) { - if (-not $caseById.ContainsKey([string]$fixtureId)) { - $problems.Add("${id}: unknown fixture id '$fixtureId'.") | Out-Null - } elseif (@($caseById[[string]$fixtureId].capabilities) -notcontains $id) { - $problems.Add("${id}: fixture '$fixtureId' does not link back to the capability.") | Out-Null - } - } -} - -foreach ($case in @($manifest.cases)) { - foreach ($capabilityId in @($case.capabilities)) { - if (-not $capabilityIds.Contains([string]$capabilityId)) { - $problems.Add("$($case.id): unknown capability '$capabilityId'.") | Out-Null - continue - } - $capability = @( - $capabilityManifest.capabilities | - Where-Object id -eq ([string]$capabilityId) - )[0] - if (@($capability.fixtureIds) -notcontains [string]$case.id) { - $problems.Add("$($case.id): capability '$capabilityId' does not link back to the fixture.") | Out-Null - } - } -} - -$fixtureBackedCount = @( - $capabilityManifest.capabilities | - Where-Object status -in @('fixture', 'validated') -).Count -$capabilityCount = @($capabilityManifest.capabilities).Count -$fixtureCoverage = if ($capabilityCount) { $fixtureBackedCount / $capabilityCount } else { 0.0 } -if ($fixtureCoverage -lt $minimumFixtureCoverage) { - $problems.Add("Fixture-backed capability coverage $fixtureCoverage is below minimumFixtureCoverage $minimumFixtureCoverage.") | Out-Null -} - -if ($problems.Count) { - Write-Host "Development fixture validation FAILED ($($problems.Count) problem(s)):" -ForegroundColor Red - $problems | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } - exit 1 -} - -if ($PrepareDirectory) { - $markerPath = Join-Path $PrepareDirectory '.bcquality-development-evaluation' - if (Test-Path -LiteralPath $PrepareDirectory) { - $existing = @(Get-ChildItem -LiteralPath $PrepareDirectory -Force) - if ($existing.Count -and -not (Test-Path -LiteralPath $markerPath -PathType Leaf)) { - throw "PrepareDirectory is not empty and is not a BCQuality development evaluation directory: $PrepareDirectory" - } - if (Test-Path -LiteralPath $markerPath -PathType Leaf) { - Get-ChildItem -LiteralPath $PrepareDirectory -File | - Where-Object { - $_.Name -eq 'knowledge-index.json' -or - $_.Name -like 'request-*.json' -or - $_.Name -like 'result-*.json' - } | - Remove-Item -Force - } - } else { - New-Item -ItemType Directory -Force -Path $PrepareDirectory | Out-Null - } - Set-Content -LiteralPath $markerPath -Value 'BCQuality generated development evaluation directory' -Encoding UTF8 - - $indexPath = Join-Path $PrepareDirectory 'knowledge-index.json' - & (Join-Path $Root 'tools/Build-KnowledgeIndex.ps1') -BCQualityRoot $Root -IndexPath $indexPath | Out-Null - $skillInstructions = Get-Content -LiteralPath (Join-Path $Root $skillPath) -Raw - - foreach ($case in @($manifest.cases)) { - $modelId = Get-ModelCaseId ([string]$case.id) - $request = [ordered]@{ - protocol = 'Run the supplied AL development skill in a fresh writable fixture repository. Persist the implementation, run real checks, and return only implementationReport using the supplied schema.' - caseId = $modelId - skill = $skillPath - skillInstructions = $skillInstructions - knowledgeIndex = 'knowledge-index.json' - 'task-context' = [ordered]@{ - goal = if ($case.'development-request'.PSObject.Properties.Name -contains 'description') { - [string]$case.'development-request'.description - } else { - [string]$case.'development-request'.plan - } - 'inputs-available' = @('development-request', 'repository') - technologies = @($case.context.technologies) - countries = @($case.context.countries) - 'application-area' = @($case.context.'application-area') - } - 'development-request' = [ordered]@{ - kind = [string]$case.'development-request'.kind - description = if ($case.'development-request'.PSObject.Properties.Name -contains 'description') { - [string]$case.'development-request'.description - } else { - $null - } - plan = if ($case.'development-request'.PSObject.Properties.Name -contains 'plan') { - [string]$case.'development-request'.plan - } else { - $null - } - 'acceptance-criteria' = @($case.'development-request'.'acceptance-criteria') - } - resultSchema = [ordered]@{ - caseId = $modelId - workspaceRoot = 'absolute path to the retained fixture repository' - implementationReport = [ordered]@{ - skill = [ordered]@{ id = 'al-development'; version = 1 } - outcome = 'completed | not-applicable | no-knowledge | partial | failed' - 'outcome-reason' = 'required for partial or failed' - summary = [ordered]@{ - request = 'implemented change' - 'files-created' = 0 - 'files-modified' = 0 - 'files-deleted' = 0 - } - plan = [ordered]@{ - kind = 'feature | bug | refactor | upgrade | maintenance' - assumptions = @() - decisions = @() - objects = @() - } - knowledge = @([ordered]@{ path = 'repo-relative knowledge article path'; sha = 'optional commit sha'; 'used-for' = 'decision' }) - changes = @([ordered]@{ path = 'repo-relative changed file'; action = 'created | modified | deleted'; purpose = 'reason' }) - validation = @([ordered]@{ id = 'compile | tests | review'; command = 'command or quality-skill path'; status = 'passed | failed | not-run'; details = 'non-empty evidence' }) - review = [ordered]@{ - skill = [ordered]@{ id = 'al-code-review'; version = 1 } - outcome = 'completed | partial | failed' - summary = [ordered]@{ - counts = [ordered]@{ blocker = 0; major = 0; minor = 0; info = 0 } - coverage = [ordered]@{ 'worklist-size' = 0; 'items-evaluated' = 0 } - } - findings = @() - suppressed = @() - } - 'review-rounds' = @([ordered]@{ - round = 1 - outcome = 'clean | fixing | stalled | limit-reached' - 'gating-finding-ids' = @() - }) - suppressed = @() - remaining = @() - } - } - } - $request | ConvertTo-Json -Depth 12 | - Set-Content -LiteralPath (Join-Path $PrepareDirectory "request-$modelId.json") -Encoding UTF8 - } -} - -if ($ResultsDirectory) { - $failures = [System.Collections.Generic.List[string]]::new() - foreach ($case in @($manifest.cases)) { - $modelId = Get-ModelCaseId ([string]$case.id) - $resultPath = Join-Path $ResultsDirectory "result-$modelId.json" - if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) { - $failures.Add("$($case.id): missing result file.") | Out-Null - continue - } - try { - $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json - } catch { - $failures.Add("$($case.id): result is not valid JSON: $($_.Exception.Message)") | Out-Null - continue - } - if ($result.PSObject.Properties.Name -notcontains 'caseId' -or [string]$result.caseId -ne $modelId) { - $failures.Add("$($case.id): result caseId mismatch.") | Out-Null - continue - } - if ($result.PSObject.Properties.Name -notcontains 'implementationReport') { - $failures.Add("$($case.id): implementationReport is missing.") | Out-Null - continue - } - $report = $result.implementationReport - foreach ($requiredField in @('skill', 'outcome', 'summary', 'plan', 'knowledge', 'changes', 'validation', 'review', 'review-rounds', 'suppressed', 'remaining')) { - if ($report.PSObject.Properties.Name -notcontains $requiredField) { - $failures.Add("$($case.id): implementation report is missing '$requiredField'.") | Out-Null - } - } - $reportedSkillId = if ( - $report.PSObject.Properties.Name -contains 'skill' -and - $report.skill.PSObject.Properties.Name -contains 'id' - ) { - [string]$report.skill.id - } else { - '' - } - if ($reportedSkillId -ne 'al-development') { - $failures.Add("$($case.id): implementation report skill is '$reportedSkillId'.") | Out-Null - } - $summaryRequest = if ( - $report.PSObject.Properties.Name -contains 'summary' -and - $report.summary.PSObject.Properties.Name -contains 'request' - ) { - [string]$report.summary.request - } else { - '' - } - if ([string]::IsNullOrWhiteSpace($summaryRequest)) { - $failures.Add("$($case.id): summary.request is missing or empty.") | Out-Null - } - $reportedKind = if ( - $report.PSObject.Properties.Name -contains 'plan' -and - $report.plan.PSObject.Properties.Name -contains 'kind' - ) { - [string]$report.plan.kind - } else { - '' - } - if ($reportedKind -ne [string]$case.expectedKind) { - $failures.Add("$($case.id): plan.kind '$reportedKind' does not match expected mode '$($case.expectedKind)'.") | Out-Null - } - $outcome = if ($report.PSObject.Properties.Name -contains 'outcome') { [string]$report.outcome } else { '' } - if ($outcome -ne 'completed') { - $failures.Add("$($case.id): implementation outcome is '$outcome'.") | Out-Null - } - - [object[]]$knowledgeEntries = @() - if ($report.PSObject.Properties.Name -contains 'knowledge') { - $knowledgeEntries = @($report.knowledge) - } - if (-not $knowledgeEntries.Count) { - $failures.Add("$($case.id): implementation report contains no knowledge entries.") | Out-Null - } - $usedKnowledge = @( - $knowledgeEntries | - Where-Object { $_.PSObject.Properties.Name -contains 'path' } | - ForEach-Object { [string]$_.path } - ) - if (@($usedKnowledge | Sort-Object -Unique).Count -ne $usedKnowledge.Count) { - $failures.Add("$($case.id): knowledge contains duplicate paths.") | Out-Null - } - foreach ($entry in $knowledgeEntries) { - $path = if ($entry.PSObject.Properties.Name -contains 'path') { [string]$entry.path } else { '' } - $usedFor = if ($entry.PSObject.Properties.Name -contains 'used-for') { [string]$entry.'used-for' } else { '' } - if ([string]::IsNullOrWhiteSpace($path) -or - [System.IO.Path]::IsPathRooted($path) -or - @($path -split '/|\\') -contains '..' -or - -not (Test-Path -LiteralPath (Join-Path $Root $path) -PathType Leaf)) { - $failures.Add("$($case.id): knowledge entry has a missing or invalid path '$path'.") | Out-Null - } - if ([string]::IsNullOrWhiteSpace($usedFor)) { - $failures.Add("$($case.id): knowledge entry '$path' has no used-for explanation.") | Out-Null - } - } - $requiredKnowledge = @($case.requiredKnowledge | ForEach-Object { [string]$_ }) - $matched = @($requiredKnowledge | Where-Object { $usedKnowledge -contains $_ }).Count - $recall = if ($requiredKnowledge.Count) { $matched / $requiredKnowledge.Count } else { 1.0 } - if ($recall -lt [double]$manifest.minimumKnowledgeRecall) { - $failures.Add("$($case.id): knowledge recall $recall is below $($manifest.minimumKnowledgeRecall).") | Out-Null - } - $acceptedKnowledge = @( - @($case.requiredKnowledge) + @($case.optionalKnowledge) | - ForEach-Object { [string]$_ } | - Sort-Object -Unique - ) - $acceptedUsed = @($usedKnowledge | Where-Object { $acceptedKnowledge -contains $_ }).Count - $precision = if ($usedKnowledge.Count) { $acceptedUsed / $usedKnowledge.Count } else { 0.0 } - if ($precision -lt [double]$manifest.minimumKnowledgePrecision) { - $failures.Add("$($case.id): knowledge precision $precision is below $($manifest.minimumKnowledgePrecision).") | Out-Null - } - - [object[]]$validationEntries = @() - if ($report.PSObject.Properties.Name -contains 'validation') { - $validationEntries = @($report.validation) - } - foreach ($requiredCheck in @($case.requiredChecks)) { - $check = @($validationEntries | Where-Object { - $_.PSObject.Properties.Name -contains 'id' -and [string]$_.id -eq $requiredCheck - }) - $checkStatus = if ($check.Count -eq 1 -and $check[0].PSObject.Properties.Name -contains 'status') { - [string]$check[0].status - } else { - '' - } - if ($check.Count -ne 1 -or $checkStatus -ne 'passed') { - $failures.Add("$($case.id): required check '$requiredCheck' did not pass exactly once.") | Out-Null - continue - } - $command = if ($check[0].PSObject.Properties.Name -contains 'command') { [string]$check[0].command } else { '' } - $details = if ($check[0].PSObject.Properties.Name -contains 'details') { [string]$check[0].details } else { '' } - if ([string]::IsNullOrWhiteSpace($command) -or [string]::IsNullOrWhiteSpace($details)) { - $failures.Add("$($case.id): required check '$requiredCheck' lacks command or evidence details.") | Out-Null - } - } - - [object[]]$changes = @() - if ($report.PSObject.Properties.Name -contains 'changes') { - $changes = @($report.changes) - } - if (-not $changes.Count) { - $failures.Add("$($case.id): implementation report contains no changed files.") | Out-Null - } - - $workspaceRoot = if ($result.PSObject.Properties.Name -contains 'workspaceRoot') { [string]$result.workspaceRoot } else { '' } - if ([string]::IsNullOrWhiteSpace($workspaceRoot) -or - -not (Test-Path -LiteralPath $workspaceRoot -PathType Container)) { - $failures.Add("$($case.id): workspaceRoot is missing or unavailable.") | Out-Null - } else { - & git -C $workspaceRoot rev-parse --is-inside-work-tree 2>$null | Out-Null - if ($LASTEXITCODE -ne 0) { - $failures.Add("$($case.id): workspaceRoot is not a readable git worktree.") | Out-Null - continue - } - $actualChanges = @( - @(& git -C $workspaceRoot diff --name-only HEAD) + - @(& git -C $workspaceRoot ls-files --others --exclude-standard) | - Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | - ForEach-Object { ([string]$_).Replace('\', '/') } | - Sort-Object -Unique - ) - if ($LASTEXITCODE -ne 0) { - $failures.Add("$($case.id): unable to read the workspace diff.") | Out-Null - } else { - $reportedChanges = @( - $changes | - Where-Object { $_.PSObject.Properties.Name -contains 'path' } | - ForEach-Object { - $path = ([string]$_.path).Replace('\', '/') - if ([System.IO.Path]::IsPathRooted($path) -or @($path -split '/') -contains '..') { - $failures.Add("$($case.id): changed path is not repository-relative: $path") | Out-Null - } - $path - } | - Sort-Object -Unique - ) - foreach ($path in @($reportedChanges | Where-Object { $actualChanges -notcontains $_ })) { - $failures.Add("$($case.id): reported change is absent from the worktree diff: $path") | Out-Null - } - foreach ($path in @($actualChanges | Where-Object { $reportedChanges -notcontains $_ })) { - $failures.Add("$($case.id): worktree change is absent from the implementation report: $path") | Out-Null - } - } - } - - if ($report.PSObject.Properties.Name -notcontains 'review') { - $failures.Add("$($case.id): complete post-implementation review is missing.") | Out-Null - } else { - $review = $report.review - foreach ($requiredField in @('skill', 'outcome', 'summary', 'findings', 'suppressed')) { - if ($review.PSObject.Properties.Name -notcontains $requiredField) { - $failures.Add("$($case.id): review is missing '$requiredField'.") | Out-Null - } - } - [object[]]$reviewFindings = @() - if ($review.PSObject.Properties.Name -contains 'findings') { - $reviewFindings = @($review.findings) - } - $reviewOutcome = if ($review.PSObject.Properties.Name -contains 'outcome') { [string]$review.outcome } else { '' } - if ($reviewOutcome -ne 'completed') { - $failures.Add("$($case.id): post-implementation review outcome is '$reviewOutcome'.") | Out-Null - } - $gatingFindings = @( - $reviewFindings | - Where-Object { - $_.PSObject.Properties.Name -contains 'severity' -and - [string]$_.severity -in @('blocker', 'major') - } - ) - if ($gatingFindings.Count) { - $failures.Add("$($case.id): post-implementation review has $($gatingFindings.Count) gating finding(s).") | Out-Null - } - } - [object[]]$reviewRounds = @() - if ($report.PSObject.Properties.Name -contains 'review-rounds') { - $reviewRounds = @($report.'review-rounds') - } - if (-not $reviewRounds.Count -or $reviewRounds.Count -gt $maximumReviewRounds) { - $failures.Add("$($case.id): review-rounds must contain 1..$maximumReviewRounds entries.") | Out-Null - } else { - for ($index = 0; $index -lt $reviewRounds.Count; $index++) { - $round = $reviewRounds[$index] - $roundNumber = if ($round.PSObject.Properties.Name -contains 'round') { [int]$round.round } else { 0 } - $roundOutcome = if ($round.PSObject.Properties.Name -contains 'outcome') { [string]$round.outcome } else { '' } - if ($roundNumber -ne ($index + 1)) { - $failures.Add("$($case.id): review round numbering is not contiguous.") | Out-Null - } - if ($roundOutcome -notin @('clean', 'fixing', 'stalled', 'limit-reached')) { - $failures.Add("$($case.id): review round $roundNumber has invalid outcome '$roundOutcome'.") | Out-Null - } - [object[]]$roundGatingIds = @() - if ($round.PSObject.Properties.Name -contains 'gating-finding-ids') { - $roundGatingIds = @($round.'gating-finding-ids') - } - if ($roundOutcome -eq 'clean' -and $roundGatingIds.Count) { - $failures.Add("$($case.id): clean review round $roundNumber must have no gating IDs.") | Out-Null - } - if ($roundOutcome -in @('fixing', 'stalled', 'limit-reached') -and -not $roundGatingIds.Count) { - $failures.Add("$($case.id): review round $roundNumber outcome '$roundOutcome' requires gating IDs.") | Out-Null - } - if (@($roundGatingIds | Sort-Object -Unique).Count -ne $roundGatingIds.Count) { - $failures.Add("$($case.id): review round $roundNumber has duplicate gating IDs.") | Out-Null - } - } - if ([string]$reviewRounds[-1].outcome -ne 'clean') { - $failures.Add("$($case.id): completed implementation must end with a clean review round.") | Out-Null - } - } - [object[]]$remaining = @() - if ($report.PSObject.Properties.Name -contains 'remaining') { - $remaining = @($report.remaining) - } - if ($remaining.Count) { - $failures.Add("$($case.id): completed report still lists remaining work.") | Out-Null - } - } - - if ($failures.Count) { - Write-Host "Development fixture scoring FAILED ($($failures.Count) problem(s)):" -ForegroundColor Red - $failures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } - exit 1 - } - Write-Host "Development fixture scoring PASSED: $(@($manifest.cases).Count) case(s)." -} else { - Write-Host "Development fixture validation PASSED: $(@($manifest.cases).Count) cases; $fixtureBackedCount of $($capabilityIds.Count) capabilities have fixtures (minimum $minimumFixtureCoverage)." -} diff --git a/tools/Test-DevelopmentGuidanceEvaluator.ps1 b/tools/Test-DevelopmentGuidanceEvaluator.ps1 new file mode 100644 index 00000000..de9c7741 --- /dev/null +++ b/tools/Test-DevelopmentGuidanceEvaluator.ps1 @@ -0,0 +1,421 @@ +<# +.SYNOPSIS + Deterministic, offline regressions for the guidance evaluator (no model/AL run). +.DESCRIPTION + Creates only a uniquely named .guidance-evaluator-regression-* directory below + the current checkout, with standalone Git repositories and sibling runner + artifacts. Removes that exact directory in finally; never uses the OS temp + directory or cleans a caller-provided repository. +#> +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'DevelopmentGuidance.Evidence.ps1') +$evaluator = Join-Path $PSScriptRoot 'Test-DevelopmentGuidanceFixtures.ps1' +$sourceRoot = (Get-Item -LiteralPath (Join-Path $PSScriptRoot '..')).FullName +$scratch = Join-Path $sourceRoot ".guidance-evaluator-regression-$([guid]::NewGuid().ToString('N'))" +$root = Join-Path $scratch 'knowledge-checkout' +$article = 'microsoft/knowledge/performance/pair-findset-with-next-loop.md' +$otherArticle = 'microsoft/knowledge/performance/findset-true-applies-updlock-on-read.md' +$sample = 'microsoft/knowledge/performance/pair-findset-with-next-loop.good.al' +$tests = [Collections.Generic.List[string]]::new() +$script:scenarioNumber = 0 +$script:reportScenario = $null + +function Set-TestJson($Path, $Value) { + [IO.File]::WriteAllText($Path, ($Value | ConvertTo-Json -Depth 64)) +} + +function Invoke-TestGit([string] $Directory, [string[]] $Arguments) { + $output = @(& git --no-optional-locks -C $Directory @Arguments 2>&1) + if ($LASTEXITCODE -ne 0) { throw 'Regression Git setup failed.' } +} + +function Initialize-TestRepository([string] $Directory) { + [IO.Directory]::CreateDirectory($Directory) | Out-Null + Invoke-TestGit $Directory @('init', '--quiet') + Invoke-TestGit $Directory @('config', 'user.email', 'guidance-fixture@example.invalid') + Invoke-TestGit $Directory @('config', 'user.name', 'Guidance fixture') + Invoke-TestGit $Directory @('config', 'commit.gpgSign', 'false') + Invoke-TestGit $Directory @('config', 'core.autocrlf', 'false') + [IO.File]::WriteAllText((Join-Path $Directory 'app.json'), '{"name":"Synthetic AL fixture","application":"28.0.0.0"}') + [IO.File]::WriteAllText((Join-Path $Directory 'tracked.al'), 'codeunit 50100 Example {}') + [IO.File]::WriteAllText((Join-Path $Directory '.gitignore'), "ignored.txt`nignored-directory/`n") + Invoke-TestGit $Directory @('add', '.') + Invoke-TestGit $Directory @('commit', '--quiet', '-m', 'Synthetic fixture baseline') +} + +function Invoke-EvaluatorTest([string] $Name, [string[]] $Arguments, [bool] $ShouldPass, [string] $Diagnostic = '') { + $output = @(& pwsh -NoProfile -File $evaluator @Arguments 2>&1) -join "`n" + $code = $LASTEXITCODE + if (($code -eq 0) -ne $ShouldPass -or $output -notmatch $(if ($ShouldPass) { 'PASSED|captured' } else { 'FAILED' })) { + throw "Regression '$Name' unexpected exit $code. $output" + } + if ($Diagnostic -and $output -notmatch [regex]::Escape($Diagnostic)) { + throw "Regression '$Name' missing diagnostic '$Diagnostic'. $output" + } + if ($output -match 'MODEL_SECRET_SENTINEL') { throw "Regression '$Name' leaked model content." } + $tests.Add($Name) +} + +function New-TestScenario([string] $Outcome = 'completed', [switch] $Unknown, [switch] $SecondCase) { + $script:scenarioNumber++ + $directory = Join-Path $scratch "scenario-$script:scenarioNumber" + [IO.Directory]::CreateDirectory($directory) | Out-Null + $workspace = Join-Path $directory 'target' + Initialize-TestRepository $workspace + [IO.File]::WriteAllText((Join-Path $workspace 'untracked.txt'), 'existing untracked content') + [IO.File]::WriteAllText((Join-Path $workspace 'ignored.txt'), 'existing ignored content') + [IO.Directory]::CreateDirectory((Join-Path $workspace 'ignored-directory')) | Out-Null + [IO.File]::WriteAllText((Join-Path $workspace 'ignored-directory\child.txt'), 'ignored child') + $results = Join-Path $directory 'results' + [IO.Directory]::CreateDirectory($results) | Out-Null + $hasKnowledge = $Outcome -in @('completed', 'partial') + $case = [ordered]@{ + id = 'synthetic-case' + expectedKind = 'bug' + expectedOutcome = $Outcome + expectedUnknown = @($(if ($Unknown) { 'bc-version' })) + requiresUnresolved = ($Outcome -eq 'partial' -or $Unknown.IsPresent) + requiresMaterialUnresolved = ($Outcome -eq 'partial') + 'development-plan' = [ordered]@{ kind = 'bug'; request = 'Iterate the supplied filtered record set.' } + context = [ordered]@{ + 'bc-version' = $(if ($Unknown) { 'unknown' } else { '28' }) + technologies = @('al') + countries = @('w1') + 'application-area' = @('all') + unknown = @($(if ($Unknown) { 'bc-version' })) + } + requiredKnowledge = @($(if ($hasKnowledge) { $article })) + optionalKnowledge = @() + } + $manifest = [ordered]@{ + version = 1 + skill = 'microsoft/skills/development/al-development-plan.md' + minimumKnowledgeRecall = 1.0 + minimumKnowledgePrecision = 1.0 + cases = @($case) + } + $result = [ordered]@{ + caseId = Get-GuidanceCaseId $case.id + guidanceReport = [ordered]@{ + skill = [ordered]@{ id = 'al-development-plan'; version = 1 } + outcome = $Outcome + summary = [ordered]@{ request = 'Iterate all selected records.'; kind = 'bug'; candidates = [int]$hasKnowledge; selected = [int]$hasKnowledge } + context = $case.context + knowledge = @($(if ($hasKnowledge) { + [ordered]@{ path = $article; 'used-for' = 'Choose the multi-record reader.'; constraints = @('Use FindSet when iterating with Next.'); 'sample-paths' = @($sample) } + })) + 'validation-considerations' = @([ordered]@{ id = 'all-selected'; reason = 'Preserve selection.'; evidence = 'Test all selected records and an excluded record.' }) + suppressed = @() + unresolved = @($(if ($Outcome -eq 'partial') { + if ($Unknown) { 'bc-version is unknown and materially affects the candidate; clarify before completing guidance.' } + else { 'The caller cardinality decision remains materially unresolved.' } + } elseif ($Unknown) { 'bc-version is unknown but immaterial: selected loop guidance applies to all versions.' })) + } + } + if ($Outcome -in @('partial', 'failed')) { $result.guidanceReport.'outcome-reason' = 'Fixture intentionally leaves evaluation incomplete.' } + $manifestPath = Join-Path $directory 'manifest.json' + $mapPath = Join-Path $directory 'workspace-map.json' + $map = [ordered]@{ 'synthetic-case' = $workspace } + if ($SecondCase) { + $second = $case | ConvertTo-Json -Depth 64 | ConvertFrom-Json -AsHashtable + $second.id = 'second-case' + $manifest.cases += $second + $secondWorkspace = Join-Path $directory 'second-target' + Initialize-TestRepository $secondWorkspace + $map[$second.id] = $secondWorkspace + $secondResult = $result | ConvertTo-Json -Depth 64 | ConvertFrom-Json -AsHashtable + $secondResult.caseId = Get-GuidanceCaseId $second.id + Set-TestJson (Join-Path $results "result-$($secondResult.caseId).json") $secondResult + } + Set-TestJson $manifestPath $manifest + Set-TestJson $mapPath $map + $resultPath = Join-Path $results "result-$($result.caseId).json" + Set-TestJson $resultPath $result + $baselinePath = Join-Path $directory 'baseline.json' + $captureArgs = @('-Root', $root, '-ManifestPath', $manifestPath, '-CaptureBaseline', '-WorkspaceMapPath', $mapPath, '-BaselinePath', $baselinePath) + $output = @(& pwsh -NoProfile -File $evaluator @captureArgs 2>&1) -join "`n" + if ($LASTEXITCODE -ne 0) { throw "Baseline setup failed. $output" } + return [ordered]@{ + directory = $directory; workspace = $workspace; resultPath = $resultPath; result = $result + results = $results; manifest = $manifest; manifestPath = $manifestPath; mapPath = $mapPath + baselinePath = $baselinePath; captureArgs = $captureArgs + scoreArgs = @('-Root', $root, '-ManifestPath', $manifestPath, '-ResultsDirectory', $results, '-BaselinePath', $baselinePath, '-BaselineSha256', (Get-GuidanceHash $baselinePath)) + } +} + +function Test-ReportMutation([string] $Name, [scriptblock] $Change, [string] $Diagnostic) { + if ($null -eq $script:reportScenario) { $script:reportScenario = New-TestScenario } + $scenario = $script:reportScenario + $result = $scenario.result | ConvertTo-Json -Depth 64 | ConvertFrom-Json -AsHashtable + & $Change $result + Set-TestJson $scenario.resultPath $result + Invoke-EvaluatorTest $Name $scenario.scoreArgs $false $Diagnostic +} + +try { + [IO.Directory]::CreateDirectory($root) | Out-Null + foreach ($reference in @($article, $otherArticle, $sample, $otherArticle.Replace('.md', '.good.al'), + 'microsoft/skills/development/al-development-plan.md', 'tools/Build-KnowledgeIndex.ps1')) { + $destination = Join-Path $root $reference.Replace('/', [IO.Path]::DirectorySeparatorChar) + [IO.Directory]::CreateDirectory((Split-Path $destination -Parent)) | Out-Null + [IO.File]::Copy((Join-Path $sourceRoot $reference.Replace('/', [IO.Path]::DirectorySeparatorChar)), $destination) + } + Initialize-TestRepository $root + + $publicManifestPath = Join-Path $sourceRoot 'evaluation\development-guidance-fixtures.json' + $publicManifest = Read-GuidanceJson $publicManifestPath + $publicRoot = Join-Path $scratch 'public-fixture-checkout' + $publicReferences = @($publicManifest.skill, 'tools/Build-KnowledgeIndex.ps1', 'evaluation/development-guidance-fixtures.json') + + @($publicManifest.cases | ForEach-Object { $_.requiredKnowledge; $_.optionalKnowledge }) + foreach ($reference in @($publicReferences | Sort-Object -Unique)) { + $destination = Join-Path $publicRoot $reference.Replace('/', [IO.Path]::DirectorySeparatorChar) + [IO.Directory]::CreateDirectory((Split-Path $destination -Parent)) | Out-Null + [IO.File]::Copy((Join-Path $sourceRoot $reference.Replace('/', [IO.Path]::DirectorySeparatorChar)), $destination) + } + $publicPrepared = Join-Path $scratch 'public-prepared' + Invoke-EvaluatorTest 'public five-case manifest validation' @('-Root', $publicRoot) $true + Invoke-EvaluatorTest 'public five-case manifest preparation' @('-Root', $publicRoot, '-PrepareDirectory', $publicPrepared) $true + $initialCase = $publicManifest.cases | Where-Object id -eq 'synthetic-normal-initial-plan' + $initialRequest = Read-GuidanceJson (Join-Path $publicPrepared "request-$(Get-GuidanceCaseId $initialCase.id).json") + if ($initialRequest.'development-plan' -cne $initialCase.'development-plan') { throw 'Preparation truncated the serialized initial plan.' } + $document = $initialRequest.'development-plan' | ConvertFrom-Json -AsHashtable + if ($document.metadata.kind -ne 'bug' -or + @('Root cause and design', 'Proposed fix', 'Affected files', 'Test strategy', 'Acceptance criteria' | + Where-Object { $document.body -notmatch [regex]::Escape($_) }).Count) { + throw 'Synthetic consumer boundary lost metadata or markdown plan sections.' + } + $tests.Add('serialized synthetic initial-plan boundary preserves full metadata and markdown body') + + $scenario = New-TestScenario + Invoke-EvaluatorTest 'unchanged target with ignored and untracked files passes' $scenario.scoreArgs $true + Invoke-EvaluatorTest 'scoring itself leaves index and content unchanged' $scenario.scoreArgs $true + Invoke-EvaluatorTest 'existing baseline cannot silently recapture' $scenario.captureArgs $false 'Baseline already exists' + Invoke-EvaluatorTest 'scoring without baseline fails' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-ResultsDirectory', $scenario.results) $false 'require BaselinePath' + Invoke-EvaluatorTest 'scoring requires independently retained baseline digest' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-ResultsDirectory', $scenario.results, '-BaselinePath', $scenario.baselinePath) $false 'runner-retained pre-run BaselineSha256' + Invoke-EvaluatorTest 'tampered baseline digest fails' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-ResultsDirectory', $scenario.results, '-BaselinePath', $scenario.baselinePath, '-BaselineSha256', ('0' * 64)) $false 'digest mismatch' + + $prepare = Join-Path $scenario.directory 'prepared' + Invoke-EvaluatorTest 'prepare outside roots with runner workspace binding' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-PrepareDirectory', $prepare, '-WorkspaceMapPath', $scenario.mapPath) $true + $prepared = Read-GuidanceJson (Join-Path $prepare "request-$($scenario.result.caseId).json") + if ($prepared.repository -cne $scenario.workspace -or $prepared.Contains('expectedOutcome') -or $prepared.Contains('requiredKnowledge')) { + throw 'Prepared request lost runner binding or exposed answers.' + } + Invoke-EvaluatorTest 'preparation never overwrites requests' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-PrepareDirectory', $prepare) $false 'new or empty' + foreach ($option in @('PrepareDirectory', 'BaselinePath', 'ResultsDirectory')) { + $inside = Join-Path $scenario.workspace 'unsafe-artifact' + $arguments = @('-Root', $root, '-ManifestPath', $scenario.manifestPath) + if ($option -eq 'PrepareDirectory') { $arguments += @('-PrepareDirectory', $inside, '-WorkspaceMapPath', $scenario.mapPath) } + elseif ($option -eq 'BaselinePath') { $arguments += @('-CaptureBaseline', '-BaselinePath', $inside, '-WorkspaceMapPath', $scenario.mapPath) } + else { $arguments += @('-BaselinePath', $scenario.baselinePath, '-BaselineSha256', (Get-GuidanceHash $scenario.baselinePath), '-ResultsDirectory', $inside) } + Invoke-EvaluatorTest "$option inside target rejected" $arguments $false 'outside target workspaces' + } + + foreach ($mutation in @('uncommitted', 'committed', 'empty-commit', 'staged', 'index-only', 'untracked', 'ignored', 'ignored-child', 'added', 'deleted', 'directory', 'ref', 'metadata')) { + $scenario = New-TestScenario + switch ($mutation) { + 'uncommitted' { [IO.File]::AppendAllText((Join-Path $scenario.workspace 'tracked.al'), "`n// changed") } + 'committed' { + [IO.File]::AppendAllText((Join-Path $scenario.workspace 'tracked.al'), "`n// changed") + Invoke-TestGit $scenario.workspace @('add', 'tracked.al') + Invoke-TestGit $scenario.workspace @('commit', '--quiet', '-m', 'Committed forbidden edit') + } + 'staged' { + [IO.File]::AppendAllText((Join-Path $scenario.workspace 'tracked.al'), "`n// changed") + Invoke-TestGit $scenario.workspace @('add', 'tracked.al') + } + 'empty-commit' { Invoke-TestGit $scenario.workspace @('commit', '--quiet', '--allow-empty', '-m', 'Forbidden empty commit') } + 'index-only' { Invoke-TestGit $scenario.workspace @('update-index', '--assume-unchanged', 'tracked.al') } + 'untracked' { [IO.File]::AppendAllText((Join-Path $scenario.workspace 'untracked.txt'), 'changed') } + 'ignored' { [IO.File]::AppendAllText((Join-Path $scenario.workspace 'ignored.txt'), 'changed') } + 'ignored-child' { [IO.File]::AppendAllText((Join-Path $scenario.workspace 'ignored-directory\child.txt'), 'changed') } + 'added' { [IO.File]::WriteAllText((Join-Path $scenario.workspace 'new.txt'), 'new ignored/untracked payload') } + 'deleted' { [IO.File]::Delete((Join-Path $scenario.workspace 'untracked.txt')) } + 'directory' { [IO.Directory]::CreateDirectory((Join-Path $scenario.workspace 'new-empty-directory')) | Out-Null } + 'ref' { Invoke-TestGit $scenario.workspace @('branch', 'new-reference') } + 'metadata' { + $path = Join-Path $scenario.workspace 'tracked.al' + [IO.File]::SetLastWriteTimeUtc($path, [IO.File]::GetLastWriteTimeUtc($path).AddSeconds(5)) + } + } + Invoke-EvaluatorTest "$mutation mutation fails" $scenario.scoreArgs $false 'identity/content changed' + } + + $scenario = New-TestScenario -SecondCase + Invoke-EvaluatorTest 'two independently bound workspaces pass' $scenario.scoreArgs $true + $map = Read-GuidanceJson $scenario.mapPath + [IO.File]::AppendAllText((Join-Path $scenario.workspace 'tracked.al'), "`n// changed") + $scenario.result.workspaceRoot = $map['second-case'] + Set-TestJson $scenario.resultPath $scenario.result + Invoke-EvaluatorTest 'self-reported clean workspace cannot hide changed runner target' $scenario.scoreArgs $false 'identity/content changed' + $scenario = New-TestScenario -SecondCase + $scenario.result.workspaceRoot = (Read-GuidanceJson $scenario.mapPath)['second-case'] + Set-TestJson $scenario.resultPath $scenario.result + Invoke-EvaluatorTest 'result workspace swapping rejected even when both are clean' $scenario.scoreArgs $false 'runner binding' + $scenario = New-TestScenario -SecondCase + $scenario.result.caseId = Get-GuidanceCaseId 'second-case' + Set-TestJson $scenario.resultPath $scenario.result + Invoke-EvaluatorTest 'result case swapping rejected' $scenario.scoreArgs $false 'caseId mismatch' + $scenario = New-TestScenario + $scenario.manifest.cases[0].expectedKind = 'feature' + Set-TestJson $scenario.manifestPath $scenario.manifest + Invoke-EvaluatorTest 'changed manifest rejected' $scenario.scoreArgs $false 'root or manifest' + $scenario = New-TestScenario + $baseline = Read-GuidanceJson $scenario.baselinePath + $baseline.workspaces['synthetic-case'].caseId = 'case-00000000' + Set-TestJson $scenario.baselinePath $baseline + Invoke-EvaluatorTest 'baseline case binding checked even with matching digest' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-ResultsDirectory', $scenario.results, '-BaselinePath', $scenario.baselinePath, '-BaselineSha256', (Get-GuidanceHash $scenario.baselinePath)) $false 'case identity mismatch' + + foreach ($outcome in @('no-knowledge', 'partial', 'failed', 'not-applicable')) { + $scenario = New-TestScenario $outcome + Invoke-EvaluatorTest "honest $outcome is distinguishable and passes" $scenario.scoreArgs $true + } + $scenario = New-TestScenario 'partial' -Unknown + Invoke-EvaluatorTest 'material unknown stays partial' $scenario.scoreArgs $true + $scenario.result.guidanceReport.outcome = 'completed' + Set-TestJson $scenario.resultPath $scenario.result + Invoke-EvaluatorTest 'material unknown cannot silently complete' $scenario.scoreArgs $false 'expectedOutcome' + $scenario = New-TestScenario 'completed' -Unknown + Invoke-EvaluatorTest 'nonmaterial unknown can complete with explanation' $scenario.scoreArgs $true + $scenario.result.guidanceReport.unresolved = @() + Set-TestJson $scenario.resultPath $scenario.result + Invoke-EvaluatorTest 'unknown cannot disappear from unresolved evidence' $scenario.scoreArgs $false 'unresolved gaps' + $scenario = New-TestScenario 'no-knowledge' -Unknown + Invoke-EvaluatorTest 'immaterial unknown and no-knowledge are not failures' $scenario.scoreArgs $true + foreach ($outcome in @('partial', 'failed')) { + $scenario = New-TestScenario $outcome + $scenario.result.guidanceReport.Remove('outcome-reason') + Set-TestJson $scenario.resultPath $scenario.result + Invoke-EvaluatorTest "$outcome requires outcome-reason" $scenario.scoreArgs $false 'outcome-reason' + } + $scenario = New-TestScenario 'no-knowledge' + $scenario.result.guidanceReport.outcome = 'completed' + Set-TestJson $scenario.resultPath $scenario.result + Invoke-EvaluatorTest 'no-knowledge is not completed-empty' $scenario.scoreArgs $false 'expectedOutcome' + $scenario = New-TestScenario 'no-knowledge' + $scenario.result.guidanceReport.knowledge = @(@{ path = $article; 'used-for' = 'filler'; constraints = @('filler'); 'sample-paths' = @() }) + $scenario.result.guidanceReport.summary.candidates = 1 + $scenario.result.guidanceReport.summary.selected = 1 + Set-TestJson $scenario.resultPath $scenario.result + Invoke-EvaluatorTest 'no-knowledge cannot contain filler knowledge' $scenario.scoreArgs $false 'requires empty knowledge' + + Test-ReportMutation 'invalid outcome enum' { param($r) $r.guidanceReport.outcome = 'success' } 'outcome enum' + Test-ReportMutation 'missing report fields' { param($r) $r.guidanceReport.Remove('knowledge') } 'missing required field' + Test-ReportMutation 'null report object' { param($r) $r.guidanceReport = $null } 'JSON object' + Test-ReportMutation 'wrong skill version type' { param($r) $r.guidanceReport.skill.version = '1' } 'identity/version' + Test-ReportMutation 'wrong skill id type' { param($r) $r.guidanceReport.skill.id = @('al-development-plan') } 'non-empty string' + Test-ReportMutation 'wrong kind type' { param($r) $r.guidanceReport.summary.kind = @('bug') } 'non-empty string' + Test-ReportMutation 'wrong summary type' { param($r) $r.guidanceReport.summary = @() } 'JSON object' + Test-ReportMutation 'fractional count' { param($r) $r.guidanceReport.summary.candidates = 1.5 } 'non-negative integer' + Test-ReportMutation 'negative count' { param($r) $r.guidanceReport.summary.selected = -1 } 'non-negative integer' + Test-ReportMutation 'inconsistent selected count' { param($r) $r.guidanceReport.summary.selected = 0 } 'Summary counts' + Test-ReportMutation 'candidate count below selected' { param($r) $r.guidanceReport.summary.candidates = 0 } 'Summary counts' + Test-ReportMutation 'wrong context list type' { param($r) $r.guidanceReport.context.technologies = 'al' } 'JSON array' + Test-ReportMutation 'missing constraints' { param($r) $r.guidanceReport.knowledge[0].constraints = @() } 'must not be empty' + Test-ReportMutation 'non-string constraints' { param($r) $r.guidanceReport.knowledge[0].constraints = @(@{ body = 'MODEL_SECRET_SENTINEL' }) } 'non-empty string' + Test-ReportMutation 'missing sample array' { param($r) $r.guidanceReport.knowledge[0].Remove('sample-paths') } 'missing required field' + Test-ReportMutation 'duplicate knowledge' { + param($r) + $r.guidanceReport.knowledge += $r.guidanceReport.knowledge[0] + $r.guidanceReport.summary.selected = 2 + $r.guidanceReport.summary.candidates = 2 + } 'Duplicate knowledge' + Test-ReportMutation 'bad validation entry' { param($r) $r.guidanceReport.'validation-considerations'[0].evidence = $false } 'non-empty string' + Test-ReportMutation 'invalid suppression shape' { param($r) $r.guidanceReport.suppressed = @(@{ path = $article; reason = 'configuration' }) } 'missing required field' + Test-ReportMutation 'invalid unresolved shape' { param($r) $r.guidanceReport.unresolved = @(@{ candidate = $article }) } 'non-empty string' + Test-ReportMutation 'invalid SHA provenance' { param($r) $r.guidanceReport.knowledge[0].sha = '0' * 40 } 'recorded live checkout' + $scenario = New-TestScenario + $scenario.result.guidanceReport.knowledge[0].sha = Invoke-GuidanceGit $root @('rev-parse', 'HEAD') + Set-TestJson $scenario.resultPath $scenario.result + Invoke-EvaluatorTest 'actual pinned knowledge SHA accepted' $scenario.scoreArgs $true + + foreach ($badPath in @('../outside.md', '/absolute.md', 'C:/external.md', + 'microsoft\knowledge\performance\pair-findset-with-next-loop.md', + 'microsoft/knowledge/performance/../performance/pair-findset-with-next-loop.md', + 'https://example.invalid/article.md', 'microsoft/knowledge/performance/missing.md', + 'microsoft/skills/development/al-development-plan.md')) { + $scenario = New-TestScenario + $scenario.result.guidanceReport.knowledge[0].path = $badPath + Set-TestJson $scenario.resultPath $scenario.result + Invoke-EvaluatorTest 'unsafe/nonexistent/non-knowledge citation rejected' $scenario.scoreArgs $false + } + foreach ($badSample in @('../outside.al', $article, $otherArticle.Replace('.md', '.good.al'), 'microsoft/knowledge/performance/other.good.al', 'microsoft\knowledge\performance\pair-findset-with-next-loop.good.al')) { + $scenario = New-TestScenario + $scenario.result.guidanceReport.knowledge[0].'sample-paths' = @($badSample) + Set-TestJson $scenario.resultPath $scenario.result + Invoke-EvaluatorTest 'unsafe/nonexistent/non-sibling sample rejected' $scenario.scoreArgs $false + } + foreach ($json in @('{', 'null', '[]', '{"caseId":"MODEL_SECRET_SENTINEL","caseId":"duplicate"}', '{"caseId":true,}', '// comment')) { + $scenario = New-TestScenario + [IO.File]::WriteAllText($scenario.resultPath, $json) + Invoke-EvaluatorTest 'malformed model result fails without runtime crash or content leakage' $scenario.scoreArgs $false + } + + $scenario = New-TestScenario + $outside = Join-Path $scenario.directory 'outside' + [IO.Directory]::CreateDirectory($outside) | Out-Null + $link = Join-Path $scenario.workspace 'escape' + $linkKind = if ($IsWindows) { 'Junction' } else { 'SymbolicLink' } + New-Item -ItemType $linkKind -Path $link -Target $outside | Out-Null + try { + Invoke-EvaluatorTest 'target junction/symlink rejected instead of followed' $scenario.scoreArgs $false 'Links, junctions' + Invoke-EvaluatorTest 'baseline capture rejects junction/symlink target children' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-CaptureBaseline', '-WorkspaceMapPath', $scenario.mapPath, '-BaselinePath', (Join-Path $scenario.directory 'linked-baseline.json')) $false 'Links, junctions' + Invoke-EvaluatorTest 'prepared directory cannot escape through junction' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-PrepareDirectory', (Join-Path $link 'prepared'), '-WorkspaceMapPath', $scenario.mapPath) $false 'Links, junctions' + } finally { Remove-Item -LiteralPath $link -Force } + $scenario = New-TestScenario + $external = Join-Path $scenario.directory 'external.txt' + [IO.File]::WriteAllText($external, 'external hard-link target') + $hardLink = Join-Path $scenario.workspace 'hard-link.txt' + New-Item -ItemType HardLink -Path $hardLink -Target $external | Out-Null + try { + Invoke-EvaluatorTest 'hard-link escape rejected' $scenario.scoreArgs $false 'Links, junctions' + } finally { Remove-Item -LiteralPath $hardLink -Force } + $scenario = New-TestScenario + $externalArticle = Join-Path $scenario.directory 'outside.md' + [IO.File]::Copy((Join-Path $root $article.Replace('/', [IO.Path]::DirectorySeparatorChar)), $externalArticle) + $articleLink = Join-Path $root 'custom\knowledge\performance' + [IO.Directory]::CreateDirectory((Split-Path $articleLink -Parent)) | Out-Null + New-Item -ItemType $linkKind -Path $articleLink -Target $scenario.directory | Out-Null + try { + $rejected = $false + try { $null = Resolve-GuidanceReference $root 'custom/knowledge/performance/outside.md' -Knowledge } + catch { $rejected = $_.Exception.Message -match 'Links, junctions' } + if (-not $rejected) { throw 'Linked knowledge reference was followed.' } + $tests.Add('knowledge junction/symlink reference rejected') + $rejected = $false + try { $null = Resolve-GuidanceReference $root 'custom/knowledge/performance/pair-findset-with-next-loop.good.al' -Article 'custom/knowledge/performance/pair-findset-with-next-loop.md' } + catch { $rejected = $_.Exception.Message -match 'Links, junctions' } + if (-not $rejected) { throw 'Linked sample reference was followed.' } + $tests.Add('sample junction/symlink reference rejected') + } finally { Remove-Item -LiteralPath $articleLink -Force } + $normativePath = Join-Path $root 'microsoft\knowledge\performance\single-normative-section.md' + foreach ($heading in @('Best Practice', 'Anti Pattern')) { + [IO.File]::WriteAllText($normativePath, "---`ndomain: performance`n---`n## Description`nSynthetic contract fixture.`n## $heading`nSynthetic normative constraint.`n") + $null = Resolve-GuidanceReference $root 'microsoft/knowledge/performance/single-normative-section.md' -Knowledge + $tests.Add("Knowledge article with only $heading accepted") + } + [IO.File]::Delete($normativePath) + $scenario = New-TestScenario + $linkedRoot = Join-Path $scratch 'linked-knowledge-checkout' + Invoke-TestGit $root @('worktree', 'add', '--quiet', '--detach', $linkedRoot, 'HEAD') + $linkedBaseline = Join-Path $scenario.directory 'linked-root-baseline.json' + Invoke-EvaluatorTest 'linked knowledge checkout baseline capture' @('-Root', $linkedRoot, '-ManifestPath', $scenario.manifestPath, '-CaptureBaseline', '-WorkspaceMapPath', $scenario.mapPath, '-BaselinePath', $linkedBaseline) $true + Invoke-EvaluatorTest 'unchanged linked knowledge checkout scoring' @('-Root', $linkedRoot, '-ManifestPath', $scenario.manifestPath, '-ResultsDirectory', $scenario.results, '-BaselinePath', $linkedBaseline, '-BaselineSha256', (Get-GuidanceHash $linkedBaseline)) $true + $scenario = New-TestScenario + $map = Read-GuidanceJson $scenario.mapPath + $map['synthetic-case'] = $linkedRoot + Set-TestJson $scenario.mapPath $map + Invoke-EvaluatorTest 'linked target checkout rejected as external Git storage' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-CaptureBaseline', '-WorkspaceMapPath', $scenario.mapPath, '-BaselinePath', (Join-Path $scenario.directory 'external-git-baseline.json')) $false 'standalone repositories' + $scenario = New-TestScenario + [IO.File]::AppendAllText((Join-Path $root $sample.Replace('/', [IO.Path]::DirectorySeparatorChar)), "`n// changed knowledge sample") + Invoke-EvaluatorTest 'knowledge checkout changes rejected' $scenario.scoreArgs $false 'Knowledge checkout identity/content changed' + Write-Host "Development guidance evaluator regressions PASSED: $($tests.Count) checks." +} finally { + if (Test-Path -LiteralPath $scratch) { + # Only our own uniquely named directory; links made by tests are removed above. + Remove-Item -LiteralPath $scratch -Recurse -Force + } +} diff --git a/tools/Test-DevelopmentGuidanceFixtures.ps1 b/tools/Test-DevelopmentGuidanceFixtures.ps1 index 0018ffc6..8259f999 100644 --- a/tools/Test-DevelopmentGuidanceFixtures.ps1 +++ b/tools/Test-DevelopmentGuidanceFixtures.ps1 @@ -1,352 +1,215 @@ <# .SYNOPSIS Validates, prepares, and scores read-only AL development-guidance fixtures. +.DESCRIPTION + Capture runner-owned evidence BEFORE invoking an agent: + -CaptureBaseline -WorkspaceMapPath -BaselinePath + The workspace map is {"manifest-case-id":"absolute-standalone-git-root",...}. + Score AFTER the agent finishes: + -BaselinePath -BaselineSha256 -ResultsDirectory + Preparation (-PrepareDirectory) is independent; supply -WorkspaceMapPath to + include runner-selected target paths. Never derive target paths from results. + + Baseline, map, prepared requests, and results must be outside all targets and + the knowledge checkout. Keep the baseline runner-only; retain the printed + SHA256 and pass -BaselineSha256 when scoring to detect baseline tampering. + Capture never overwrites an existing baseline. The runner must protect this + script, the baseline/digest and its invocation from the agent. + + This compares before/after evidence, NOT an OS sandbox or a write monitor. + It cannot detect reverted transient writes, prove that articles were opened, + or validate semantic faithfulness of prose. Files, directories, hashes, + stable metadata, Git HEAD/refs/index and ignored/untracked files are compared. + Links/reparse points, hard links, alternate data streams, external Git + storage in targets, submodules and sparse checkouts are rejected rather than + followed. Run in quiescent repositories. The knowledge checkout may itself + be a linked Git worktree; its Git storage identity is recorded explicitly. #> [CmdletBinding()] param( - [string] $Root = (Resolve-Path (Join-Path $PSScriptRoot '..')), + [string] $Root = (Join-Path $PSScriptRoot '..'), [string] $ManifestPath, [string] $PrepareDirectory, - [string] $ResultsDirectory + [string] $ResultsDirectory, + [switch] $CaptureBaseline, + [string] $BaselinePath, + [string] $BaselineSha256, + [string] $WorkspaceMapPath ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' - -$Root = (Resolve-Path -LiteralPath $Root).Path -if (-not $ManifestPath) { - $ManifestPath = Join-Path $Root 'evaluation/development-guidance-fixtures.json' -} - -$manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json -$problems = [System.Collections.Generic.List[string]]::new() -$validKinds = @('feature', 'bug', 'refactor', 'upgrade', 'maintenance') - -function Get-ModelCaseId { - param([string] $ManifestId) - - $sha = [System.Security.Cryptography.SHA256]::Create() - try { - $bytes = [System.Text.Encoding]::UTF8.GetBytes($ManifestId) - $hash = $sha.ComputeHash($bytes) - $token = ([System.BitConverter]::ToString($hash) -replace '-', '').Substring(0, 8).ToLowerInvariant() - return "case-$token" - } finally { - $sha.Dispose() - } -} - -function Get-PlanRequest { - param([object] $Plan) - - if ($Plan.PSObject.Properties.Name -contains 'request' -and - -not [string]::IsNullOrWhiteSpace([string]$Plan.request)) { - return [string]$Plan.request - } - if ($Plan.PSObject.Properties.Name -contains 'format' -and - [string]$Plan.format -eq 'BCFIX-HANDOFF' -and - $Plan.PSObject.Properties.Name -contains 'nextStep') { - $issue = if ($Plan.PSObject.Properties.Name -contains 'issue') { [string]$Plan.issue } else { 'unknown' } - return "Continue BCFIX issue #${issue}: $($Plan.nextStep)" - } - return '' -} - -if ($manifest.version -ne 1) { - $problems.Add("Unsupported guidance fixture version: $($manifest.version)") | Out-Null -} -foreach ($thresholdName in @('minimumKnowledgeRecall', 'minimumKnowledgePrecision')) { - $threshold = [double]$manifest.$thresholdName - if ($threshold -lt 0 -or $threshold -gt 1) { - $problems.Add("$thresholdName must be between 0 and 1.") | Out-Null - } -} - -$skillPath = [string]$manifest.skill -if (-not (Test-Path -LiteralPath (Join-Path $Root $skillPath) -PathType Leaf)) { - $problems.Add("Guidance skill does not exist: $skillPath") | Out-Null -} - -$seenIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) -foreach ($case in @($manifest.cases)) { - $id = [string]$case.id - if ($id -notmatch '^[a-z0-9]+(?:-[a-z0-9]+)*$') { - $problems.Add("Fixture id must be kebab-case: '$id'.") | Out-Null - } elseif (-not $seenIds.Add($id)) { - $problems.Add("Duplicate fixture id: $id") | Out-Null - } - if ($case.PSObject.Properties.Name -notcontains 'development-plan') { - $problems.Add("${id}: development-plan is required.") | Out-Null - continue - } - $plan = $case.'development-plan' - $expectedKind = if ($case.PSObject.Properties.Name -contains 'expectedKind') { - [string]$case.expectedKind - } else { - '' - } - if ($validKinds -notcontains $expectedKind) { - $problems.Add("${id}: expectedKind is invalid.") | Out-Null - } - $isBcfixHandoff = ( - $plan.PSObject.Properties.Name -contains 'format' -and - [string]$plan.format -eq 'BCFIX-HANDOFF' - ) - if ($isBcfixHandoff) { - $requiredHandoffFields = @( - 'version', 'issue', 'phase', 'status', 'baton', 'rootCause', - 'harnessMap', 'iterationsUsed', 'filesCommitted', 'lastTestResult', - 'deadEnds', 'nextStep' - ) - foreach ($field in $requiredHandoffFields) { - if ($plan.PSObject.Properties.Name -notcontains $field) { - $problems.Add("${id}: BCFIX-HANDOFF is missing '$field'.") | Out-Null +. (Join-Path $PSScriptRoot 'DevelopmentGuidance.Evidence.ps1') + +try { + if ($CaptureBaseline -and ($ResultsDirectory -or $PrepareDirectory)) { + throw 'CaptureBaseline is a separate pre-run operation.' + } + if (($CaptureBaseline -or $ResultsDirectory) -and -not $BaselinePath) { + throw 'Capture and scoring require BaselinePath.' + } + if ($ResultsDirectory -and -not $BaselineSha256) { + throw 'Scoring requires the runner-retained pre-run BaselineSha256.' + } + if ($CaptureBaseline -and -not $WorkspaceMapPath) { + throw 'Capture requires a runner-owned WorkspaceMapPath.' + } + if ($ResultsDirectory -and ($WorkspaceMapPath -or $PrepareDirectory)) { + throw 'Scoring uses only the recorded workspace map; run preparation separately.' + } + $Root = Get-GuidanceSafePath $Root -Directory + if (-not $ManifestPath) { $ManifestPath = Join-Path $Root 'evaluation\development-guidance-fixtures.json' } + $ManifestPath = Get-GuidanceSafePath $ManifestPath -File + $manifest = Read-GuidanceJson $ManifestPath + Assert-GuidanceManifest $manifest $Root + $caseIds = @($manifest.cases | ForEach-Object { $_.id }) + $workspaces = [ordered]@{} + $baseline = $null + if ($WorkspaceMapPath) { + $WorkspaceMapPath = Get-GuidanceSafePath $WorkspaceMapPath -File + $map = Read-GuidanceJson $WorkspaceMapPath + Assert-GuidanceObject $map 'workspace map' + if ($map.Count -ne $caseIds.Count) { throw 'Workspace map must bind exactly every manifest case.' } + foreach ($id in $caseIds) { + Assert-GuidanceString $map[$id] 'workspace map value' + if (-not [IO.Path]::IsPathFullyQualified($map[$id])) { throw 'Workspace roots must be absolute.' } + $workspaces[$id] = Get-GuidanceSafePath $map[$id] -Directory + } + } + if ($ResultsDirectory) { + $BaselinePath = Get-GuidanceSafePath $BaselinePath -File + if ($BaselineSha256 -cnotmatch '^[0-9A-Fa-f]{64}$') { + throw 'BaselineSha256 must be a SHA256 digest.' + } + if ((Get-GuidanceHash $BaselinePath) -ne $BaselineSha256) { + throw 'Runner baseline digest mismatch.' + } + $baseline = Read-GuidanceJson $BaselinePath + Assert-GuidanceObject $baseline 'baseline' @('version', 'kind', 'root', 'manifestPath', 'manifestSha256', 'workspaces', 'rootSnapshot') + if ($baseline.version -ne 1 -or $baseline.kind -cne 'bcquality-guidance-runner-baseline') { + throw 'Unsupported runner baseline.' + } + if ($baseline.root -cne $Root -or $baseline.manifestPath -cne $ManifestPath -or + $baseline.manifestSha256 -ne (Get-GuidanceHash $ManifestPath)) { + throw 'Runner baseline does not match the knowledge root or manifest.' + } + Assert-GuidanceObject $baseline.workspaces 'baseline workspaces' + if ($baseline.workspaces.Count -ne $caseIds.Count) { throw 'Runner baseline case set mismatch.' } + foreach ($id in $caseIds) { + $entry = $baseline.workspaces[$id] + Assert-GuidanceObject $entry 'baseline workspace entry' @('caseId', 'root', 'snapshot') + if ($entry.caseId -cne (Get-GuidanceCaseId $id)) { throw 'Runner baseline case identity mismatch.' } + $workspaces[$id] = Get-GuidanceSafePath $entry.root -Directory + } + } + $protectedRoots = @($Root) + @($workspaces.Values) + for ($i = 0; $i -lt $protectedRoots.Count; $i++) { + for ($j = $i + 1; $j -lt $protectedRoots.Count; $j++) { + if ((Test-GuidanceWithin $protectedRoots[$i] $protectedRoots[$j]) -or + (Test-GuidanceWithin $protectedRoots[$j] $protectedRoots[$i])) { + throw 'Knowledge checkout and case workspaces must be distinct, non-overlapping roots.' } } - $handoffVersion = if ($plan.PSObject.Properties.Name -contains 'version') { [int]$plan.version } else { 0 } - $handoffPhase = if ($plan.PSObject.Properties.Name -contains 'phase') { [string]$plan.phase } else { '' } - $handoffStatus = if ($plan.PSObject.Properties.Name -contains 'status') { [string]$plan.status } else { '' } - if ($handoffVersion -ne 1) { - $problems.Add("${id}: only BCFIX-HANDOFF version 1 is supported.") | Out-Null - } - if ($handoffPhase -notin @('plan', 'baseline', 'implement', 'pr')) { - $problems.Add("${id}: BCFIX-HANDOFF phase is invalid.") | Out-Null - } - if ($handoffStatus -notin @('in-progress', 'paused', 'done')) { - $problems.Add("${id}: BCFIX-HANDOFF status is invalid.") | Out-Null - } - } elseif ( - $plan.PSObject.Properties.Name -notcontains 'kind' -or - $validKinds -notcontains [string]$plan.kind - ) { - $problems.Add("${id}: development-plan.kind is invalid.") | Out-Null } - if ([string]::IsNullOrWhiteSpace((Get-PlanRequest $plan))) { - $problems.Add("${id}: development-plan must provide request or BCFIX nextStep.") | Out-Null - } - - $expectedKnowledge = @($case.requiredKnowledge) + @($case.optionalKnowledge) - if (@($expectedKnowledge | Sort-Object -Unique).Count -ne $expectedKnowledge.Count) { - $problems.Add("${id}: requiredKnowledge and optionalKnowledge contain duplicates.") | Out-Null - } - foreach ($reference in $expectedKnowledge) { - $reference = [string]$reference - if ($reference.Contains('\') -or -not $reference.EndsWith('.md')) { - $problems.Add("${id}: invalid knowledge path: $reference") | Out-Null - } elseif (-not (Test-Path -LiteralPath (Join-Path $Root $reference) -PathType Leaf)) { - $problems.Add("${id}: knowledge article does not exist: $reference") | Out-Null - } - } -} - -if ($problems.Count) { - Write-Host "Development guidance fixture validation FAILED ($($problems.Count) problem(s)):" -ForegroundColor Red - $problems | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } - exit 1 -} - -if ($PrepareDirectory) { - $markerPath = Join-Path $PrepareDirectory '.bcquality-development-guidance-evaluation' - if (Test-Path -LiteralPath $PrepareDirectory) { - $existing = @(Get-ChildItem -LiteralPath $PrepareDirectory -Force) - if ($existing.Count -and -not (Test-Path -LiteralPath $markerPath -PathType Leaf)) { - throw "PrepareDirectory is not empty and is not a BCQuality development-guidance evaluation directory: $PrepareDirectory" - } - if (Test-Path -LiteralPath $markerPath -PathType Leaf) { - Get-ChildItem -LiteralPath $PrepareDirectory -File | - Where-Object { - $_.Name -eq 'knowledge-index.json' -or - $_.Name -like 'request-*.json' -or - $_.Name -like 'result-*.json' - } | - Remove-Item -Force - } - } else { - New-Item -ItemType Directory -Force -Path $PrepareDirectory | Out-Null - } - Set-Content -LiteralPath $markerPath -Value 'BCQuality generated development-guidance evaluation directory' -Encoding UTF8 - - $indexPath = Join-Path $PrepareDirectory 'knowledge-index.json' - & (Join-Path $Root 'tools/Build-KnowledgeIndex.ps1') -BCQualityRoot $Root -IndexPath $indexPath | Out-Null - $skillInstructions = Get-Content -LiteralPath (Join-Path $Root $skillPath) -Raw - - foreach ($case in @($manifest.cases)) { - $modelId = Get-ModelCaseId ([string]$case.id) - [ordered]@{ - protocol = 'Run the supplied AL development-plan skill read-only in a clean fixture repository. Return only guidanceReport using the supplied schema and retain the repository for read-only verification.' - caseId = $modelId - skill = $skillPath - skillInstructions = $skillInstructions - knowledgeIndex = 'knowledge-index.json' - 'task-context' = [ordered]@{ - goal = Get-PlanRequest $case.'development-plan' - 'inputs-available' = @('development-plan', 'repository') - technologies = @($case.context.technologies) - countries = @($case.context.countries) - 'application-area' = @($case.context.'application-area') - } - 'development-plan' = $case.'development-plan' - resultSchema = [ordered]@{ - caseId = $modelId - workspaceRoot = 'absolute path to the retained clean fixture repository' - guidanceReport = [ordered]@{ - skill = [ordered]@{ id = 'al-development-plan'; version = 1 } - outcome = 'completed | not-applicable | no-knowledge | partial | failed' - 'outcome-reason' = 'required for partial or failed' - summary = [ordered]@{ - request = 'planned change' - kind = 'feature | bug | refactor | upgrade | maintenance' - candidates = 0 - selected = 0 - } - context = [ordered]@{ - 'bc-version' = 'resolved target or unknown' - technologies = @('al') - countries = @('w1') - 'application-area' = @('all') - unknown = @() - } - knowledge = @([ordered]@{ - path = 'repo-relative article path' - sha = 'optional commit sha' - 'used-for' = 'plan decision' - constraints = @('faithful normative constraint') - 'sample-paths' = @() - }) - 'validation-considerations' = @([ordered]@{ - id = 'stable id' - reason = 'why evidence is needed' - evidence = 'evidence implementation should obtain' - }) - suppressed = @() - unresolved = @() + foreach ($artifact in @($BaselinePath, $WorkspaceMapPath, $PrepareDirectory, $ResultsDirectory)) { + if ($artifact) { + $safeArtifact = Get-GuidanceSafePath $artifact -AllowMissing + foreach ($protectedRoot in $protectedRoots) { + if (Test-GuidanceWithin $safeArtifact $protectedRoot) { + throw 'Runner artifacts must be outside target workspaces and the knowledge checkout.' } } - } | ConvertTo-Json -Depth 15 | - Set-Content -LiteralPath (Join-Path $PrepareDirectory "request-$modelId.json") -Encoding UTF8 - } -} - -if ($ResultsDirectory) { - $failures = [System.Collections.Generic.List[string]]::new() - foreach ($case in @($manifest.cases)) { - $modelId = Get-ModelCaseId ([string]$case.id) - $resultPath = Join-Path $ResultsDirectory "result-$modelId.json" - if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) { - $failures.Add("$($case.id): missing result file.") | Out-Null - continue - } - try { - $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json - } catch { - $failures.Add("$($case.id): result is not valid JSON: $($_.Exception.Message)") | Out-Null - continue - } - if ($result.PSObject.Properties.Name -notcontains 'caseId' -or [string]$result.caseId -ne $modelId) { - $failures.Add("$($case.id): result caseId mismatch.") | Out-Null - continue } - if ($result.PSObject.Properties.Name -notcontains 'guidanceReport') { - $failures.Add("$($case.id): guidanceReport is missing.") | Out-Null - continue - } - $report = $result.guidanceReport - foreach ($requiredField in @('skill', 'outcome', 'summary', 'context', 'knowledge', 'validation-considerations', 'suppressed', 'unresolved')) { - if ($report.PSObject.Properties.Name -notcontains $requiredField) { - $failures.Add("$($case.id): guidance report is missing '$requiredField'.") | Out-Null + } + if ($CaptureBaseline) { + $BaselinePath = Get-GuidanceSafePath $BaselinePath -AllowMissing + if (Test-Path -LiteralPath $BaselinePath) { throw 'Baseline already exists; capture never overwrites evidence.' } + $snapshots = [ordered]@{} + foreach ($id in $caseIds) { + $snapshots[$id] = [ordered]@{ + caseId = Get-GuidanceCaseId $id + root = $workspaces[$id] + snapshot = Get-GuidanceSnapshot $workspaces[$id] -Target } } - $skillId = if ( - $report.PSObject.Properties.Name -contains 'skill' -and - $report.skill.PSObject.Properties.Name -contains 'id' - ) { [string]$report.skill.id } else { '' } - if ($skillId -ne 'al-development-plan') { - $failures.Add("$($case.id): guidance skill is '$skillId'.") | Out-Null - } - if ($report.PSObject.Properties.Name -notcontains 'outcome' -or [string]$report.outcome -ne 'completed') { - $failures.Add("$($case.id): guidance outcome is not completed.") | Out-Null - } - $kind = if ( - $report.PSObject.Properties.Name -contains 'summary' -and - $report.summary.PSObject.Properties.Name -contains 'kind' - ) { [string]$report.summary.kind } else { '' } - if ($kind -ne [string]$case.expectedKind) { - $failures.Add("$($case.id): summary.kind '$kind' does not match the plan.") | Out-Null - } - - [object[]]$knowledgeEntries = @() - if ($report.PSObject.Properties.Name -contains 'knowledge') { - $knowledgeEntries = @($report.knowledge) - } - $usedKnowledge = @() - foreach ($entry in $knowledgeEntries) { - $path = if ($entry.PSObject.Properties.Name -contains 'path') { [string]$entry.path } else { '' } - $usedFor = if ($entry.PSObject.Properties.Name -contains 'used-for') { [string]$entry.'used-for' } else { '' } - [object[]]$constraints = @() - if ($entry.PSObject.Properties.Name -contains 'constraints') { - $constraints = @($entry.constraints) - } - if ([string]::IsNullOrWhiteSpace($path) -or - -not (Test-Path -LiteralPath (Join-Path $Root $path) -PathType Leaf)) { - $failures.Add("$($case.id): invalid knowledge path '$path'.") | Out-Null - } else { - $usedKnowledge += $path - } - if ([string]::IsNullOrWhiteSpace($usedFor) -or -not $constraints.Count) { - $failures.Add("$($case.id): '$path' lacks used-for or constraints.") | Out-Null + $record = [ordered]@{ + kind = 'bcquality-guidance-runner-baseline' + version = 1 + root = $Root + manifestPath = $ManifestPath + manifestSha256 = Get-GuidanceHash $ManifestPath + rootSnapshot = Get-GuidanceSnapshot $Root + workspaces = $snapshots + } + Write-GuidanceNewJson $BaselinePath $record + Write-Host "Guidance baseline captured. Runner SHA256: $(Get-GuidanceHash $BaselinePath)" + } elseif ($PrepareDirectory) { + $PrepareDirectory = Get-GuidanceSafePath $PrepareDirectory -AllowMissing + if ((Test-Path -LiteralPath $PrepareDirectory) -and + @(Get-ChildItem -LiteralPath $PrepareDirectory -Force).Count) { + throw 'PrepareDirectory must be new or empty; existing requests/evidence are never overwritten.' + } + [IO.Directory]::CreateDirectory($PrepareDirectory) | Out-Null + # The index builder walks recursively; reject linked corpus paths first. + Assert-GuidanceTree $Root + & (Join-Path $Root 'tools\Build-KnowledgeIndex.ps1') -BCQualityRoot $Root ` + -IndexPath (Join-Path $PrepareDirectory 'knowledge-index.json') | Out-Null + $skillInstructions = [IO.File]::ReadAllText((Resolve-GuidanceReference $Root $manifest.skill)) + foreach ($case in $manifest.cases) { + $modelId = Get-GuidanceCaseId $case.id + $request = [ordered]@{ + protocol = 'Run the supplied read-only skill on the runner-assigned repository and existing plan. Return only caseId and guidanceReport. The runner captures evidence before invocation; do not capture or modify it. Do not create artifacts in the target or knowledge checkout.' + caseId = $modelId + skill = $manifest.skill + skillInstructions = $skillInstructions + knowledgeIndex = Join-Path $PrepareDirectory 'knowledge-index.json' + knowledgeRoot = $Root + 'task-context' = [ordered]@{ + goal = Get-GuidancePlanRequest $case.'development-plan' + 'inputs-available' = @('development-plan', 'repository') + 'bc-version' = $case.context.'bc-version' + technologies = $case.context.technologies + countries = $case.context.countries + 'application-area' = $case.context.'application-area' + } + 'development-plan' = $case.'development-plan' + resultSchema = Get-GuidanceResultSchema $modelId } - if ($entry.PSObject.Properties.Name -contains 'sample-paths') { - foreach ($samplePath in @($entry.'sample-paths')) { - $samplePath = [string]$samplePath - if ([string]::IsNullOrWhiteSpace($samplePath) -or - -not (Test-Path -LiteralPath (Join-Path $Root $samplePath) -PathType Leaf)) { - $failures.Add("$($case.id): invalid sample path '$samplePath'.") | Out-Null - } + if ($workspaces.Count) { $request.repository = $workspaces[$case.id] } + Write-GuidanceNewJson (Join-Path $PrepareDirectory "request-$modelId.json") $request + } + Write-Host "Development guidance preparation PASSED: $($caseIds.Count) case(s)." + } elseif ($ResultsDirectory) { + $ResultsDirectory = Get-GuidanceSafePath $ResultsDirectory -Directory + $failures = [Collections.Generic.List[string]]::new() + if (-not (Test-GuidanceSnapshotEqual $baseline.rootSnapshot (Get-GuidanceSnapshot $Root))) { + $failures.Add('Knowledge checkout identity/content changed after baseline capture.') + } + foreach ($case in $manifest.cases) { + $id = $case.id + try { + if (-not (Test-GuidanceSnapshotEqual $baseline.workspaces[$id].snapshot ` + (Get-GuidanceSnapshot $workspaces[$id] -Target))) { + throw 'Target repository identity/content changed after baseline capture.' } + $resultPath = Get-GuidanceSafePath (Join-Path $ResultsDirectory "result-$(Get-GuidanceCaseId $id).json") -File + $result = Read-GuidanceJson $resultPath + Assert-GuidanceResult $result $case $manifest $Root $workspaces[$id] + } catch { + # Only evaluator-authored diagnostics are printed, never model or file contents. + $failures.Add("${id}: $(Get-GuidanceDiagnostic $_)") } } - - $required = @($case.requiredKnowledge | ForEach-Object { [string]$_ }) - $matched = @($required | Where-Object { $usedKnowledge -contains $_ }).Count - $recall = if ($required.Count) { $matched / $required.Count } else { 1.0 } - if ($recall -lt [double]$manifest.minimumKnowledgeRecall) { - $failures.Add("$($case.id): knowledge recall $recall is below $($manifest.minimumKnowledgeRecall).") | Out-Null - } - $accepted = @( - @($case.requiredKnowledge) + @($case.optionalKnowledge) | - ForEach-Object { [string]$_ } | - Sort-Object -Unique - ) - $acceptedUsed = @($usedKnowledge | Where-Object { $accepted -contains $_ }).Count - $precision = if ($usedKnowledge.Count) { $acceptedUsed / $usedKnowledge.Count } else { 0.0 } - if ($precision -lt [double]$manifest.minimumKnowledgePrecision) { - $failures.Add("$($case.id): knowledge precision $precision is below $($manifest.minimumKnowledgePrecision).") | Out-Null + if ($failures.Count) { + Write-Host "Development guidance scoring FAILED ($($failures.Count) problem(s)):" + $failures | ForEach-Object { Write-Host " - $_" } + exit 1 } - - $workspaceRoot = if ($result.PSObject.Properties.Name -contains 'workspaceRoot') { - [string]$result.workspaceRoot - } else { - '' - } - if ([string]::IsNullOrWhiteSpace($workspaceRoot) -or - -not (Test-Path -LiteralPath $workspaceRoot -PathType Container)) { - $failures.Add("$($case.id): workspaceRoot is missing or unavailable.") | Out-Null - } else { - $status = @(& git -C $workspaceRoot status --porcelain) - if ($LASTEXITCODE -ne 0) { - $failures.Add("$($case.id): workspaceRoot is not a readable git worktree.") | Out-Null - } elseif ($status.Count) { - $failures.Add("$($case.id): guidance skill changed the target repository.") | Out-Null - } - } - } - - if ($failures.Count) { - Write-Host "Development guidance scoring FAILED ($($failures.Count) problem(s)):" -ForegroundColor Red - $failures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } - exit 1 + Write-Host "Development guidance scoring PASSED: $($caseIds.Count) case(s)." + } else { + Write-Host "Development guidance fixture validation PASSED: $($caseIds.Count) case(s)." } - Write-Host "Development guidance scoring PASSED: $(@($manifest.cases).Count) case(s)." -} else { - Write-Host "Development guidance fixture validation PASSED: $(@($manifest.cases).Count) case(s)." +} catch { + Write-Host "Development guidance FAILED: $(Get-GuidanceDiagnostic $_)" + exit 1 } diff --git a/tools/Test-LearnCoverage.ps1 b/tools/Test-LearnCoverage.ps1 deleted file mode 100644 index ccd8f82c..00000000 --- a/tools/Test-LearnCoverage.ps1 +++ /dev/null @@ -1,240 +0,0 @@ -<# -.SYNOPSIS - Validates Microsoft Learn ingestion coverage and reports progress. -#> -[CmdletBinding()] -param( - [string] $Root = (Resolve-Path (Join-Path $PSScriptRoot '..')), - [string] $CatalogPath, - [string] $CoveragePath, - [switch] $Json -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$Root = (Resolve-Path -LiteralPath $Root).Path -if (-not $CatalogPath) { - $CatalogPath = Join-Path $Root 'coverage/microsoft-learn-developer-catalog.json' -} -if (-not $CoveragePath) { - $CoveragePath = Join-Path $Root 'coverage/learn-coverage.json' -} - -foreach ($path in @($CatalogPath, $CoveragePath)) { - if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { - throw "Coverage input not found: $path" - } -} - -$catalog = Get-Content -LiteralPath $CatalogPath -Raw | ConvertFrom-Json -$coverage = Get-Content -LiteralPath $CoveragePath -Raw | ConvertFrom-Json -$problems = [System.Collections.Generic.List[string]]::new() - -if ($catalog.version -ne 1) { - $problems.Add("Unsupported catalog version: $($catalog.version)") | Out-Null -} -if ($coverage.version -ne 1) { - $problems.Add("Unsupported coverage version: $($coverage.version)") | Out-Null -} -if ([string]$coverage.catalog -ne 'coverage/microsoft-learn-developer-catalog.json') { - $problems.Add("Coverage catalog path must be coverage/microsoft-learn-developer-catalog.json.") | Out-Null -} - -$catalogUnits = @{} -foreach ($unit in @($catalog.units)) { - $uid = [string]$unit.uid - if ($catalogUnits.ContainsKey($uid)) { - $problems.Add("Duplicate catalog unit uid: $uid") | Out-Null - } else { - $catalogUnits[$uid] = $unit - } - if ([string]::IsNullOrWhiteSpace([string]$unit.title)) { - $problems.Add("${uid}: catalog unit title is empty.") | Out-Null - } - if ([string]::IsNullOrWhiteSpace([string]$unit.url) -or - -not ([string]$unit.url).StartsWith('https://learn.microsoft.com/', [System.StringComparison]::OrdinalIgnoreCase)) { - $problems.Add("${uid}: catalog unit URL is missing or not a Microsoft Learn URL.") | Out-Null - } -} - -$catalogModules = @{} -foreach ($module in @($catalog.modules)) { - $uid = [string]$module.uid - if ($catalogModules.ContainsKey($uid)) { - $problems.Add("Duplicate catalog module uid: $uid") | Out-Null - } else { - $catalogModules[$uid] = $module - } - foreach ($unitUid in @($module.unitUids)) { - if (-not $catalogUnits.ContainsKey([string]$unitUid)) { - $problems.Add("${uid}: references unknown unit '$unitUid'.") | Out-Null - } - } -} - -$catalogPaths = @{} -foreach ($path in @($catalog.learningPaths)) { - $uid = [string]$path.uid - if ($catalogPaths.ContainsKey($uid)) { - $problems.Add("Duplicate catalog learning-path uid: $uid") | Out-Null - } else { - $catalogPaths[$uid] = $path - } - foreach ($moduleUid in @($path.moduleUids)) { - if (-not $catalogModules.ContainsKey([string]$moduleUid)) { - $problems.Add("${uid}: references unknown module '$moduleUid'.") | Out-Null - } - } -} - -if ([int]$catalog.counts.units -ne $catalogUnits.Count) { - $problems.Add("Catalog unit count does not match units array.") | Out-Null -} -if ([int]$catalog.counts.modules -ne $catalogModules.Count) { - $problems.Add("Catalog module count does not match modules array.") | Out-Null -} -if ([int]$catalog.counts.learningPaths -ne $catalogPaths.Count) { - $problems.Add("Catalog learning-path count does not match learningPaths array.") | Out-Null -} - -foreach ($unit in @($catalog.units)) { - foreach ($moduleUid in @($unit.moduleUids)) { - if (-not $catalogModules.ContainsKey([string]$moduleUid)) { - $problems.Add("$($unit.uid): references unknown module '$moduleUid'.") | Out-Null - } elseif (@($catalogModules[[string]$moduleUid].unitUids) -notcontains [string]$unit.uid) { - $problems.Add("$($unit.uid): module '$moduleUid' does not link back to the unit.") | Out-Null - } - } -} - -$validStatuses = @('in-progress', 'complete') -$validDispositions = @('candidate', 'authored', 'covered-existing', 'rejected', 'deferred') -$seenUnits = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) -$seenOutcomes = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) -$articlePaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) -$dispositionCounts = @{} -foreach ($disposition in $validDispositions) { - $dispositionCounts[$disposition] = 0 -} - -foreach ($unitCoverage in @($coverage.units)) { - $uid = [string]$unitCoverage.uid - if (-not $seenUnits.Add($uid)) { - $problems.Add("Duplicate coverage unit uid: $uid") | Out-Null - } - if (-not $catalogUnits.ContainsKey($uid)) { - $problems.Add("Coverage unit is absent from the catalog: $uid") | Out-Null - } - - $reviewStatus = [string]$unitCoverage.reviewStatus - if ($validStatuses -notcontains $reviewStatus) { - $problems.Add("${uid}: invalid reviewStatus '$reviewStatus'.") | Out-Null - } - - [object[]]$outcomes = @() - if ($unitCoverage.PSObject.Properties.Name -contains 'outcomes') { - $outcomes = @($unitCoverage.outcomes) - } - if ($reviewStatus -eq 'in-progress' -and -not $outcomes.Count) { - $problems.Add("${uid}: an in-progress unit must contain at least one outcome.") | Out-Null - } - - foreach ($outcome in $outcomes) { - $id = [string]$outcome.id - if ($id -notmatch '^[a-z0-9]+(?:-[a-z0-9]+)*$') { - $problems.Add("${uid}: outcome id must be kebab-case: '$id'.") | Out-Null - } elseif (-not $seenOutcomes.Add($id)) { - $problems.Add("Duplicate outcome id: $id") | Out-Null - } - - $disposition = [string]$outcome.disposition - if ($validDispositions -notcontains $disposition) { - $problems.Add("${id}: invalid disposition '$disposition'.") | Out-Null - continue - } - $dispositionCounts[$disposition]++ - - if ([string]::IsNullOrWhiteSpace([string]$outcome.title)) { - $problems.Add("${id}: title is required.") | Out-Null - } - - if ($disposition -in @('candidate', 'authored', 'covered-existing')) { - if ([string]::IsNullOrWhiteSpace([string]$outcome.domain)) { - $problems.Add("${id}: domain is required for disposition '$disposition'.") | Out-Null - } - } - - [object[]]$paths = @() - if ($outcome.PSObject.Properties.Name -contains 'articlePaths') { - $paths = @($outcome.articlePaths) - } - if ($disposition -in @('authored', 'covered-existing')) { - if (-not $paths.Count) { - $problems.Add("${id}: articlePaths is required for disposition '$disposition'.") | Out-Null - } - foreach ($relativePath in $paths) { - $relativePath = [string]$relativePath - if ($relativePath.Contains('\') -or -not $relativePath.EndsWith('.md')) { - $problems.Add("${id}: article path must be a forward-slash .md path: $relativePath") | Out-Null - continue - } - if (-not (Test-Path -LiteralPath (Join-Path $Root $relativePath) -PathType Leaf)) { - $problems.Add("${id}: article does not exist: $relativePath") | Out-Null - } - $articlePaths.Add($relativePath) | Out-Null - } - } elseif ($paths.Count) { - $problems.Add("${id}: disposition '$disposition' must not set articlePaths.") | Out-Null - } - - $rationale = if ($outcome.PSObject.Properties.Name -contains 'rationale') { - [string]$outcome.rationale - } else { - '' - } - if ($disposition -in @('rejected', 'deferred') -and - [string]::IsNullOrWhiteSpace($rationale)) { - $problems.Add("${id}: rationale is required for disposition '$disposition'.") | Out-Null - } - } -} - -if ($problems.Count) { - Write-Host "Learn coverage validation FAILED ($($problems.Count) problem(s)):" -ForegroundColor Red - $problems | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } - exit 1 -} - -$complete = @($coverage.units | Where-Object reviewStatus -eq 'complete').Count -$inProgress = @($coverage.units | Where-Object reviewStatus -eq 'in-progress').Count -$summary = [ordered]@{ - catalogUnits = $catalogUnits.Count - completeUnits = $complete - inProgressUnits = $inProgress - unreviewedUnits = $catalogUnits.Count - $complete - $inProgress - candidateOutcomes = $dispositionCounts.candidate - authoredOutcomes = $dispositionCounts.authored - coveredExistingOutcomes = $dispositionCounts.'covered-existing' - rejectedOutcomes = $dispositionCounts.rejected - deferredOutcomes = $dispositionCounts.deferred - referencedArticles = $articlePaths.Count -} - -if ($Json) { - $summary | ConvertTo-Json -} else { - $message = ( - 'Learn coverage: {0} units; {1} complete, {2} in progress, {3} unreviewed; ' + - '{4} candidate outcomes, {5} authored outcomes, {6} existing-coverage outcomes.' - ) -f @( - $summary.catalogUnits, - $summary.completeUnits, - $summary.inProgressUnits, - $summary.unreviewedUnits, - $summary.candidateOutcomes, - $summary.authoredOutcomes, - $summary.coveredExistingOutcomes - ) - Write-Host $message -} diff --git a/tools/Update-LearnCatalog.ps1 b/tools/Update-LearnCatalog.ps1 deleted file mode 100644 index 2368f604..00000000 --- a/tools/Update-LearnCatalog.ps1 +++ /dev/null @@ -1,262 +0,0 @@ -<# -.SYNOPSIS - Updates the committed Microsoft Learn Business Central developer catalog. - -.DESCRIPTION - Filters a Microsoft Learn catalog payload to modules tagged for both - dynamics-business-central and developer. The output is intentionally only - source metadata; editorial dispositions live in coverage/learn-coverage.json. - - The legacy unauthenticated catalog endpoint remains the default while it is - available. Use -CatalogPath with an export from the authenticated Learn - Platform API when the legacy endpoint is retired. -#> -[CmdletBinding(DefaultParameterSetName = 'Remote')] -param( - [string] $Root = (Resolve-Path (Join-Path $PSScriptRoot '..')), - - [Parameter(ParameterSetName = 'Remote')] - [uri] $CatalogUri = 'https://learn.microsoft.com/api/catalog/?locale=en-us', - - [Parameter(Mandatory, ParameterSetName = 'File')] - [string] $CatalogPath, - - [string] $OutputPath -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$Root = (Resolve-Path -LiteralPath $Root).Path -if (-not $OutputPath) { - $OutputPath = Join-Path $Root 'coverage/microsoft-learn-developer-catalog.json' -} - -function Get-CanonicalUrl { - param([string] $Url) - - if ([string]::IsNullOrWhiteSpace($Url)) { - return $null - } - - $parsed = [uri]$Url - return "$($parsed.Scheme)://$($parsed.Host)$($parsed.AbsolutePath)".TrimEnd('/') -} - -function Get-CatalogProperty { - param( - [object] $Object, - [string] $Name, - [object] $Default = $null - ) - - $property = $Object.PSObject.Properties[$Name] - if ($property) { - return $property.Value - } - return $Default -} - -function Get-LatestTimestamp { - param([object[]] $Values) - - $timestamps = @( - $Values | - Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | - ForEach-Object { [datetimeoffset]::Parse([string]$_) } | - Sort-Object - ) - if (-not $timestamps.Count) { - return $null - } - - return $timestamps[-1].ToUniversalTime().ToString( - "yyyy-MM-dd'T'HH:mm:ss'Z'", - [System.Globalization.CultureInfo]::InvariantCulture - ) -} - -function Convert-ToUtcTimestamp { - param([object] $Value) - - return ([datetimeoffset]$Value).ToUniversalTime().ToString( - "yyyy-MM-dd'T'HH:mm:ss'Z'", - [System.Globalization.CultureInfo]::InvariantCulture - ) -} - -$catalog = if ($PSCmdlet.ParameterSetName -eq 'File') { - Get-Content -LiteralPath $CatalogPath -Raw | ConvertFrom-Json -} else { - Invoke-RestMethod -Uri $CatalogUri -} - -$product = 'dynamics-business-central' -$role = 'developer' -$modules = @( - $catalog.modules | - Where-Object { - @(Get-CatalogProperty $_ 'products' @()) -contains $product -and - @(Get-CatalogProperty $_ 'roles' @()) -contains $role - } | - Sort-Object uid -) - -$moduleIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) -foreach ($module in $modules) { - $moduleIds.Add([string]$module.uid) | Out-Null -} - -$learningPaths = @( - $catalog.learningPaths | - Where-Object { - $path = $_ - $pathModules = @(Get-CatalogProperty $path 'modules' @()) - @($pathModules | Where-Object { $moduleIds.Contains([string]$_) }).Count -gt 0 -and - @(Get-CatalogProperty $path 'products' @()) -contains $product -and - @(Get-CatalogProperty $path 'roles' @()) -contains $role - } | - Sort-Object uid -) - -$pathIdsByModule = @{} -foreach ($path in $learningPaths) { - foreach ($moduleId in @(Get-CatalogProperty $path 'modules' @())) { - if (-not $moduleIds.Contains([string]$moduleId)) { - continue - } - if (-not $pathIdsByModule.ContainsKey([string]$moduleId)) { - $pathIdsByModule[[string]$moduleId] = [System.Collections.Generic.List[string]]::new() - } - $pathIdsByModule[[string]$moduleId].Add([string]$path.uid) - } -} - -$unitById = @{} -foreach ($unit in @(Get-CatalogProperty $catalog 'units' @())) { - $unitById[[string]$unit.uid] = $unit -} - -$moduleIdsByUnit = @{} -$moduleById = @{} -$unitIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) -foreach ($module in $modules) { - $moduleById[[string]$module.uid] = $module - foreach ($unitId in @(Get-CatalogProperty $module 'units' @())) { - $unitId = [string]$unitId - $unitIds.Add($unitId) | Out-Null - if (-not $moduleIdsByUnit.ContainsKey($unitId)) { - $moduleIdsByUnit[$unitId] = [System.Collections.Generic.List[string]]::new() - } - $moduleIdsByUnit[$unitId].Add([string]$module.uid) - } -} - -$pathRecords = @( - foreach ($path in $learningPaths) { - [ordered]@{ - uid = [string]$path.uid - title = [string]$path.title - url = Get-CanonicalUrl ([string]$path.url) - lastModified = if (Get-CatalogProperty $path 'last_modified') { - Convert-ToUtcTimestamp (Get-CatalogProperty $path 'last_modified') - } else { - $null - } - durationInMinutes = [int](Get-CatalogProperty $path 'duration_in_minutes' 0) - moduleUids = @( - @(Get-CatalogProperty $path 'modules' @()) | - Where-Object { $moduleIds.Contains([string]$_) } | - ForEach-Object { [string]$_ } | - Sort-Object -Unique - ) - } - } -) - -$moduleRecords = @( - foreach ($module in $modules) { - [ordered]@{ - uid = [string]$module.uid - title = [string]$module.title - summary = [string](Get-CatalogProperty $module 'summary' '') - url = Get-CanonicalUrl ([string]$module.url) - lastModified = Convert-ToUtcTimestamp (Get-CatalogProperty $module 'last_modified') - durationInMinutes = [int](Get-CatalogProperty $module 'duration_in_minutes' 0) - levels = @(@(Get-CatalogProperty $module 'levels' @()) | ForEach-Object { [string]$_ } | Sort-Object -Unique) - subjects = @(@(Get-CatalogProperty $module 'subjects' @()) | ForEach-Object { [string]$_ } | Sort-Object -Unique) - learningPathUids = if ($pathIdsByModule.ContainsKey([string]$module.uid)) { - @($pathIdsByModule[[string]$module.uid] | Sort-Object -Unique) - } else { - @() - } - unitUids = @(@(Get-CatalogProperty $module 'units' @()) | ForEach-Object { [string]$_ }) - } - } -) - -$unitRecords = @( - foreach ($unitId in @($unitIds | Sort-Object)) { - if (-not $unitById.ContainsKey($unitId)) { - throw "Catalog module references missing unit '$unitId'." - } - $unit = $unitById[$unitId] - $unitUrl = [string](Get-CatalogProperty $unit 'url' '') - if ([string]::IsNullOrWhiteSpace($unitUrl)) { - $moduleId = [string]@($moduleIdsByUnit[$unitId])[0] - $moduleUrl = Get-CanonicalUrl ([string]$moduleById[$moduleId].url) - $unitSlug = if ($unitId.StartsWith("$moduleId.", [System.StringComparison]::Ordinal)) { - $unitId.Substring($moduleId.Length + 1) - } else { - @($unitId -split '\.')[-1] - } - $unitUrl = "$moduleUrl/$unitSlug" - } - [ordered]@{ - uid = $unitId - title = [string]$unit.title - url = Get-CanonicalUrl $unitUrl - lastModified = if (Get-CatalogProperty $unit 'last_modified') { - Convert-ToUtcTimestamp (Get-CatalogProperty $unit 'last_modified') - } else { - $null - } - durationInMinutes = [int](Get-CatalogProperty $unit 'duration_in_minutes' 0) - moduleUids = @($moduleIdsByUnit[$unitId] | Sort-Object -Unique) - } - } -) - -$snapshot = [ordered]@{ - version = 1 - source = [ordered]@{ - provider = 'Microsoft Learn' - catalogUri = if ($PSCmdlet.ParameterSetName -eq 'Remote') { [string]$CatalogUri } else { $null } - locale = 'en-us' - filters = [ordered]@{ - product = $product - role = $role - } - catalogLastModified = Get-LatestTimestamp @( - @($moduleRecords | ForEach-Object lastModified) + - @($unitRecords | ForEach-Object lastModified) + - @($pathRecords | ForEach-Object lastModified) - ) - } - counts = [ordered]@{ - learningPaths = $pathRecords.Count - modules = $moduleRecords.Count - units = $unitRecords.Count - } - learningPaths = $pathRecords - modules = $moduleRecords - units = $unitRecords -} - -$outputDirectory = Split-Path -Parent $OutputPath -if (-not (Test-Path -LiteralPath $outputDirectory -PathType Container)) { - New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null -} -$snapshot | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $OutputPath -Encoding UTF8 -Write-Host "Microsoft Learn developer catalog: $($pathRecords.Count) paths, $($moduleRecords.Count) modules, $($unitRecords.Count) units." -Write-Host "Catalog: $OutputPath" From fa7eb750c643438156f1cd42001b5c5885f59f63 Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Fri, 18 Sep 2026 15:46:52 +0200 Subject: [PATCH 4/4] Narrow development guidance to provisional contract Keep plan enrichment internal and read-only pending consumer agreement and runtime pilot evidence. Move knowledge to its independent PR and remove the consumer-owned forensic evaluator. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 638b66d2-9f06-4f60-8781-808709e1485c --- .claude-plugin/marketplace.json | 4 +- .github/scripts/Test-SkillIndex.ps1 | 33 +- .github/workflows/development-guidance.yml | 25 - README.md | 41 +- docs/agent-consumption.md | 23 +- evaluation/README.md | 116 +--- evaluation/development-guidance-fixtures.json | 175 ------ ...ize-document-defaults-in-initrecord.bad.al | 28 - ...ze-document-defaults-in-initrecord.good.al | 45 -- ...tialize-document-defaults-in-initrecord.md | 30 -- ...und-direction-symbols-use-magnitude.bad.al | 8 - ...nd-direction-symbols-use-magnitude.good.al | 10 - .../round-direction-symbols-use-magnitude.md | 30 -- .../guard-interface-casts-with-is.bad.al | 20 - .../guard-interface-casts-with-is.good.al | 24 - .../guard-interface-casts-with-is.md | 30 -- ...oup-respects-lookup-page-visibility.bad.al | 9 - ...up-respects-lookup-page-visibility.good.al | 20 - ...ldgroup-respects-lookup-page-visibility.md | 30 -- ...ropagation-both-refreshes-main-page.bad.al | 26 - ...opagation-both-refreshes-main-page.good.al | 27 - ...atepropagation-both-refreshes-main-page.md | 30 -- ...on-meaning-depends-on-execution-context.md | 26 - ...and-upgrade-codeunits-have-no-order.bad.al | 28 - ...nd-upgrade-codeunits-have-no-order.good.al | 18 - ...all-and-upgrade-codeunits-have-no-order.md | 30 -- .../skills/development/al-development-plan.md | 6 +- .../skills/review/al-data-modeling-review.md | 4 +- .../skills/review/al-interfaces-review.md | 3 +- microsoft/skills/review/al-ui-review.md | 7 +- microsoft/skills/review/al-upgrade-review.md | 4 +- plugin.json | 5 +- .../development-guidance-report.schema.json | 183 +++++++ skills/README.md | 30 +- skills/al-development-plan/SKILL.md | 22 - skills/do.md | 24 +- skills/entry.md | 5 + tools/DevelopmentGuidance.Evidence.ps1 | 499 ------------------ tools/Test-DevelopmentGuidanceEvaluator.ps1 | 452 ---------------- tools/Test-DevelopmentGuidanceFixtures.ps1 | 219 -------- 40 files changed, 295 insertions(+), 2054 deletions(-) delete mode 100644 .github/workflows/development-guidance.yml delete mode 100644 evaluation/development-guidance-fixtures.json delete mode 100644 microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.bad.al delete mode 100644 microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.good.al delete mode 100644 microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md delete mode 100644 microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.bad.al delete mode 100644 microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.good.al delete mode 100644 microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md delete mode 100644 microsoft/knowledge/interfaces/guard-interface-casts-with-is.bad.al delete mode 100644 microsoft/knowledge/interfaces/guard-interface-casts-with-is.good.al delete mode 100644 microsoft/knowledge/interfaces/guard-interface-casts-with-is.md delete mode 100644 microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.bad.al delete mode 100644 microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.good.al delete mode 100644 microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md delete mode 100644 microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.bad.al delete mode 100644 microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.good.al delete mode 100644 microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md delete mode 100644 microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md delete mode 100644 microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.bad.al delete mode 100644 microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.good.al delete mode 100644 microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md create mode 100644 schemas/development-guidance-report.schema.json delete mode 100644 skills/al-development-plan/SKILL.md delete mode 100644 tools/DevelopmentGuidance.Evidence.ps1 delete mode 100644 tools/Test-DevelopmentGuidanceEvaluator.ps1 delete mode 100644 tools/Test-DevelopmentGuidanceFixtures.ps1 diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1fd35cd9..752ae8d2 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,8 +8,8 @@ { "name": "bcquality", "source": "./", - "description": "Business Central AL quality knowledge base and skills, packaged as an installable plugin. Exposes read-only plan enrichment and review adapters through BCQuality's Entry protocol.", - "version": "0.3.0", + "description": "Business Central AL quality knowledge base and review skills, packaged as an installable plugin. Exposes an AL review adapter while preserving BCQuality's internal Entry and action-skill protocols.", + "version": "0.2.0", "skills": [ "./skills/" ] diff --git a/.github/scripts/Test-SkillIndex.ps1 b/.github/scripts/Test-SkillIndex.ps1 index f8ac78fb..0876c79f 100644 --- a/.github/scripts/Test-SkillIndex.ps1 +++ b/.github/scripts/Test-SkillIndex.ps1 @@ -32,7 +32,8 @@ function Assert-ThrowsLike { $generator = Join-Path $Root 'tools/Build-SkillIndex.ps1' $indexSchema = Join-Path $Root 'schemas/skill-index.schema.json' $reportSchema = Join-Path $Root 'schemas/findings-report.schema.json' -foreach ($path in $generator, $indexSchema, $reportSchema) { +$guidanceReportSchema = Join-Path $Root 'schemas/development-guidance-report.schema.json' +foreach ($path in $generator, $indexSchema, $reportSchema, $guidanceReportSchema) { if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Required contract file not found: $path" } @@ -123,6 +124,36 @@ try { throw 'al-development-plan must remain a leaf action skill.' } + $minimalGuidanceReport = @{ + skill = @{ id = 'al-development-plan'; version = 1 } + outcome = 'completed' + summary = @{ + request = 'Enrich the existing plan.' + kind = 'feature' + candidates = 1 + selected = 1 + } + context = @{ + 'bc-version' = '28' + technologies = @('al') + countries = @('w1') + 'application-area' = @('all') + unknown = @() + } + knowledge = @(@{ + path = 'microsoft/knowledge/performance/apply-filters-before-iterating.md' + 'used-for' = 'Constrain filtered iteration.' + constraints = @('Apply filters before iterating.') + 'sample-paths' = @() + }) + 'validation-considerations' = @() + suppressed = @() + unresolved = @() + } | ConvertTo-Json -Depth 10 + if (-not ($minimalGuidanceReport | Test-Json -SchemaFile $guidanceReportSchema -ErrorAction Stop)) { + throw 'Minimal development-guidance report does not satisfy schemas/development-guidance-report.schema.json.' + } + $minimalReport = @{ skill = @{ id = 'al-style-review'; version = 1 } outcome = 'completed' diff --git a/.github/workflows/development-guidance.yml b/.github/workflows/development-guidance.yml deleted file mode 100644 index 51e24b3f..00000000 --- a/.github/workflows/development-guidance.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Validate read-only development guidance - -on: - pull_request: - branches: [main] - push: - branches: [main] - -jobs: - validate-development-guidance: - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Validate and prepare development-guidance fixtures - shell: pwsh - run: ./tools/Test-DevelopmentGuidanceFixtures.ps1 -Root . -PrepareDirectory "$env:RUNNER_TEMP/bcquality-development-guidance-fixtures" - - - name: Run credential-free guidance evaluator regressions - shell: pwsh - run: ./tools/Test-DevelopmentGuidanceEvaluator.ps1 diff --git a/README.md b/README.md index c325aab2..7cc04342 100644 --- a/README.md +++ b/README.md @@ -26,34 +26,24 @@ copilot plugin install microsoft/BCQuality copilot plugin list ``` -The list should include `bcquality`. The plugin exposes -[`al-code-review`](skills/al-code-review/SKILL.md) and the read-only -[`al-development-plan`](skills/al-development-plan/SKILL.md) plan-enrichment -skill. Installation and skill discovery are the general pattern; reviewing an -app is one example of using it. +The list should include `bcquality`. The plugin exposes the +[`al-code-review`](skills/al-code-review/SKILL.md) skill. Installation and skill +discovery are the general pattern; reviewing an app is one example of using it. -Plugin version `0.3.0` adds `al-development-plan`, a read-only adapter for -enriching an **existing** plan. It does not generate a plan or implement code. - -The adapters are intentionally not second implementations: +The adapter is intentionally not a second implementation: ```text standalone host skill: skills/al-code-review/SKILL.md -> routing contract: skills/entry.md -> review coordinator: microsoft/skills/review/al-code-review.md -> domain review leaves - -standalone host skill: skills/al-development-plan/SKILL.md - -> routing contract: skills/entry.md - -> enrichment skill: microsoft/skills/development/al-development-plan.md - -> referenced constraints for the consumer's existing workflow (read-only) ``` Only the files under `skills/*/SKILL.md` follow the host's packaging format. The remaining files are BCQuality's internal protocol and layered action skills. Entry remains the single owner of routing and index preparation. This separation keeps standalone installation available without duplicating policy -in either adapter. +in the adapter. ### Example: Review a complete app folder @@ -73,7 +63,7 @@ Approve access only to a project you trust, then ask: The folder should contain `app.json` and your AL source; it does **not** need to be a Git repository. On macOS or Linux, use your app's local path instead. -Each host adapter and internal action skill intentionally share a name: they +The host adapter and internal action skill intentionally share a name: they expose the same operation in two different skill formats. Their paths make the boundary explicit. @@ -111,8 +101,11 @@ available domains and the difference between a folder review and a comparison. Mechanical issues already enforced by the AL compiler or standard analyzers are intentionally left to those deterministic tools rather than duplicated here. -The read-only `al-development-plan` interface selects relevant constraints -before the consumer implements its own existing plan. +BCQuality defines a provisional internal, read-only `al-development-plan` +action-skill contract that selects relevant constraints before a consumer +implements its own existing plan. It is not registered as a standalone plugin +skill and does not change the plugin version. Consumer-owner agreement and a +runtime pilot are required before treating it as a stable public surface. Repository-specific orchestrators retain planning, implementation, approvals, tests, environment, propagation, and delivery ownership. The intended flow is @@ -131,14 +124,12 @@ Functional areas such as Finance, Supply Chain Management, Manufacturing, Jobs, Warehousing, and Service, and technologies such as PowerShell, pipelines, and Power Platform, remain valid future scope, **not current coverage claims**. -## Evidence and follow-up scope +## Plan-enrichment follow-up scope -The [guidance evaluation](evaluation/README.md#read-only-plan-guidance) separates -credential-free contract/scorer regressions from external agent and runtime -evidence. Prepared requests and fixture counts do not establish compilation, -test execution, better repairs, or a capability percentage. Consumer adoption, -a pinned baseline comparison and runtime pilot, standalone authoring, and -source-ingestion catalog work remain separate follow-ups. +Consumer agreement, consumer-owned persistence and phase injection, a pinned +baseline comparison, and a runtime pilot remain follow-up work. BCQuality does +not claim improved repairs or authoring effectiveness from this provisional +contract alone. ## What's in this repo diff --git a/docs/agent-consumption.md b/docs/agent-consumption.md index b8f2d584..75e6262d 100644 --- a/docs/agent-consumption.md +++ b/docs/agent-consumption.md @@ -78,10 +78,9 @@ Add scheduling, retries, and rendering only when needed, using the - **Layer content** in `/microsoft/`, `/community/`, and `/custom/` — knowledge files and action skills grouped by authority. When BCQuality is installed as a standalone plugin, it additionally exposes -`skills/al-code-review/SKILL.md` and -`skills/al-development-plan/SKILL.md`. These are host-format adapters, not -additional action skills: each creates the task context and enters the same -flow at Entry. +`skills/al-code-review/SKILL.md`. This is a host-format adapter, not an +additional action skill: it creates the task context and enters the same flow +at Entry. ## Repository structure @@ -121,11 +120,10 @@ The orchestrator has a URL setting that points at BCQuality (default: `github.co ### 2. Agent invokes Entry The agent reads `/skills/entry.md` and runs it against the task context. Entry applies its Source → Relevance → Worklist → Action steps over the action skills under `*/skills/**/*.md` and returns a **dispatch record**: the set of action skills to invoke, plus a list of candidates it skipped (with reasons). Routing is a skill, not orchestrator logic. -For a standalone plugin installation, the host activates the matching adapter +For a standalone plugin installation, the host activates the review adapter first. The adapter preserves the caller's actual goal, constructs the task -context, and invokes Entry. It does not select the internal review or -plan-enrichment action skill itself or duplicate Entry's preparation, routing, and -failure semantics. +context, and invokes Entry. It does not select the internal review action skill +itself or duplicate Entry's preparation, routing, and failure semantics. ### 3. Agent consumes the dispatch record The dispatch record names one or more action skills, the subset of inputs each @@ -186,11 +184,12 @@ drive a review/fix loop. Implementation stays in the consuming workflow. ### 7. Orchestrator integrates The orchestrator turns findings into PR comments, build gates, or IDE diagnostics. It can feed read-only guidance into its own implementation phases, preserving all existing approvals and delivery gates. -## Repository-specific development orchestrators +## Provisional repository-specific development integration A repository-specific workflow can consume this read-only foundation before authoring while retaining its independent final review. This is the intended -integration boundary, not a shipped consumer integration: +integration boundary proposed by `al-development-plan`, not a shipped consumer +integration or a stable plugin surface: 1. Investigate and produce the consumer's normal initial plan. Normalize any consumer-specific format outside BCQuality. A full serialized plan document @@ -219,6 +218,10 @@ approvals, TDD and runtime execution, propagation, retries, commits, and PR delivery. BCQuality supplies additional referenced product knowledge, not a replacement orchestrator. +Consumer owners must agree the input/output contract and insertion point before +adoption. Runtime mutation-proofing and effectiveness measurement belong to the +consumer pilot rather than the BCQuality content repository. + ### Outcomes are additive, not a universal coding gate `no-knowledge` with empty `knowledge` means no additional applicable BCQuality diff --git a/evaluation/README.md b/evaluation/README.md index 1af292f9..2125ce39 100644 --- a/evaluation/README.md +++ b/evaluation/README.md @@ -1,4 +1,4 @@ -# AL review and guidance evaluation +# AL review evaluation The evaluation is convention-driven. The harness discovers every `/skills/review/al--review.md` leaf across the enabled `microsoft`, `community`, and `custom` layers. Duplicate domains resolve with `custom > community > microsoft` precedence. For each selected leaf, the harness finds paired knowledge across the same layers, applies the same precedence to duplicate article slugs, selects the first article (by filename) with both `.bad.al` and `.good.al` companions, and derives the expected positive and clean control automatically. Adding a conforming leaf requires no scoring-contract edit. @@ -54,117 +54,3 @@ This credential-free check proves every selected leaf maps to a same-named knowl For a single combined stress-test result, use `-ResultsPath` instead. The committed gate requires full expected recall, the exact convention-derived article ID, and no findings on clean controls. - -## Read-only plan guidance - -`development-guidance-fixtures.json` evaluates the planning interface used by -existing workflows. It supplies an existing plan and expects referenced -constraints without target-repository changes. The initial-plan fixture is -anonymized and synthetic: a full document with metadata and a markdown body -covering root cause, proposed fix, affected files, tests, and acceptance -criteria. It is integration-shaped input, not private consumer content, a -continuation checkpoint, or proof that any production consumer is integrated. - -### Credential-free contract and scorer coverage - -CI validates the manifest, prepares opaque model requests, and runs -deterministic scorer regressions with controlled reports and temporary Git -repositories. These checks cover report shape, outcomes, reference paths, and -the evaluator's pre/post read-only comparison. They do **not** run an agent, -compile AL, run Business Central tests, or establish better code authoring. - -Prepare requests in a runner-owned artifact directory outside every target -workspace: - -```powershell -$run = Join-Path ([IO.Path]::GetTempPath()) 'bcquality-guidance-run' -pwsh ./tools/Test-DevelopmentGuidanceFixtures.ps1 -Root . -PrepareDirectory $run -``` - -Preparation is not a model run. A scorer can validate a citation's path and -required fields, but only external agent traces and expert evaluation can -establish that the article was opened and its normative constraints faithfully -applied. Expected knowledge recall/precision is fixture-specific, not a corpus -coverage or authoring capability percentage. - -`no-knowledge` means no additional applicable BCQuality constraints, not unsafe -work. It requires empty `knowledge`. Partial evaluation, failed retrieval, -unknown context, and materially unresolved applicability must remain visible -and distinct; they cannot be counted as successful enrichment merely because -the JSON is parseable. - -Run the deterministic regression suite without an agent or AL environment: - -```powershell -pwsh ./tools/Test-DevelopmentGuidanceEvaluator.ps1 -``` - -### Runner-owned read-only evidence - -For an external guidance run, first provision a representative, standalone Git -repository for each manifest case. The runner supplies a JSON workspace map -whose keys are the manifest IDs (not the hashed model IDs) and whose values are -absolute workspace roots. It may pass `-WorkspaceMapPath` during preparation -to bind the generated requests to those roots. The model must not select its -own workspace for scoring. - -Capture evidence **before** invoking the agent, with the manifest, workspace -map, and source checkout already finalized: - -```powershell -# Runner-selected paths, all outside the targets and BCQuality checkout. -$map = Join-Path $evidenceDirectory 'workspace-map.json' -$baseline = Join-Path $evidenceDirectory 'baseline.json' -pwsh ./tools/Test-DevelopmentGuidanceFixtures.ps1 -Root . ` - -CaptureBaseline -WorkspaceMapPath $map -BaselinePath $baseline - -# Retain the printed SHA256 in runner-only state BEFORE agent invocation. -# After the external agent writes result-case-.json files: -pwsh ./tools/Test-DevelopmentGuidanceFixtures.ps1 -Root . ` - -ResultsDirectory $resultsDirectory -BaselinePath $baseline ` - -BaselineSha256 $preRunDigest -``` - -`$evidenceDirectory`, `$resultsDirectory`, and `$preRunDigest` are supplied by -the runner; the digest must not be recomputed from potentially modified evidence -after the agent runs. Protect the baseline, digest, evaluator, and invocation -from agent changes. Capture refuses to overwrite an existing baseline. Results -contain only `caseId` and `guidanceReport`; a legacy `workspaceRoot`, if present, -must agree with the independently captured binding and never overrides it. -Missing baselines or digests, malformed reports, and escaped reference paths -fail scoring. - -The comparison checks target identity, Git HEAD, refs and index, filesystem -content and stable metadata, including tracked, untracked, ignored files and -empty directories. Committing edits or making an empty commit does not evade -the check. It also compares the actual knowledge checkout and manifest identity. -Targets must have internal Git storage; linked target worktrees, submodules, -sparse checkouts, links/junctions/reparse points, hard links, and alternate data -streams are unsupported and rejected rather than silently excluded. A linked -**knowledge** checkout is supported with its Git storage identity recorded. -Use quiescent, isolated repositories; concurrent changes also fail the gate. - -This is before/after evidence, not an OS sandbox or a complete write monitor. -It cannot prove that no transient write was reverted, that articles were opened, -or that constraints are semantically faithful. Reports and generated artifacts -must stay outside all target workspaces and the knowledge checkout. The -regression suite creates and removes its own uniquely named fixture directory; -it does not run against or clean a caller's target. - -### External agent/runtime pilot (follow-up) - -Consumer uptake and a real before-authoring pilot are not implemented by these -fixtures. The consumer must normalize its normal initial plan, persist guidance -after state initialization, inject it into existing phases, re-enrich on -material changes, and run an independent final review. See -[the integration boundary](../agent-consumption.md#repository-specific-development-orchestrators). - -Before claiming improved repairs, run an independent pinned baseline without -enrichment and a matched enriched run. Hold starting code, task, model, tools, -runtime, and gates constant; record actual immutable BCQuality checkout and -policy identities rather than trusting a configured ref. Use that same -recorded checkout for enrichment and final review. Retain external logs, -article-read traces, resulting diffs, compile/test outcomes, and independent -review evidence, including failures, no-knowledge, partial, and unresolved -results. No compile/run or authoring-quality claim follows from the -credential-free checks above. diff --git a/evaluation/development-guidance-fixtures.json b/evaluation/development-guidance-fixtures.json deleted file mode 100644 index e34d2ea4..00000000 --- a/evaluation/development-guidance-fixtures.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "version": 1, - "skill": "microsoft/skills/development/al-development-plan.md", - "minimumKnowledgeRecall": 1.0, - "minimumKnowledgePrecision": 0.67, - "cases": [ - { - "id": "synthetic-normal-initial-plan", - "title": "Anonymized normal initial-plan consumer boundary", - "evidenceType": "integration-shaped-synthetic", - "boundary": "A synthetic consumer produces metadata plus a markdown plan body, serializes the full document, and passes that document as the generic existing development-plan. This is not an external real pilot and contains no consumer workflow-state schema.", - "expectedKind": "bug", - "expectedOutcome": "completed", - "expectedUnknown": [], - "requiresUnresolved": false, - "requiresMaterialUnresolved": false, - "development-plan": "{\"metadata\":{\"kind\":\"bug\",\"request\":\"Update every entry in the supplied filtered record set while preserving the caller's selection.\",\"origin\":\"anonymized synthetic initial plan\"},\"body\":\"## Root cause and design\\nThe routine reads and updates only the first record rather than iterating the supplied filtered set. Preserve the supplied filters and update each selected row.\\n\\n## Proposed fix\\nUse an update-safe FindSet/Next loop with an explicit update on each selected entry.\\n\\n## Affected files\\n- src/Batch/UpdateSelectedEntries.Codeunit.al\\n- test/Batch/UpdateSelectedEntriesTests.Codeunit.al\\n\\n## Test strategy\\nUse existing AL test library codeunits to arrange three selected rows and an excluded row. Assert all selected rows are updated and the excluded row is unchanged. This is a proposed test, not a reported result.\\n\\n## Acceptance criteria\\n- Every selected entry is updated exactly once.\\n- The supplied filters remain effective.\\n- No excluded entry changes.\\n- Empty selections cause no changes.\\n\"}", - "context": { - "bc-version": "28", - "technologies": [ - "al" - ], - "countries": [ - "w1" - ], - "application-area": [ - "all" - ], - "unknown": [] - }, - "requiredKnowledge": [ - "microsoft/knowledge/performance/pair-findset-with-next-loop.md", - "microsoft/knowledge/performance/findset-true-applies-updlock-on-read.md", - "microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md" - ], - "optionalKnowledge": [ - "microsoft/knowledge/performance/pass-var-record-to-preserve-partial-load-enumerator.md" - ] - }, - { - "id": "versioned-upgrade-plan-guidance", - "title": "Select guidance for a versioned data upgrade", - "expectedKind": "upgrade", - "expectedOutcome": "completed", - "expectedUnknown": [], - "requiresUnresolved": false, - "requiresMaterialUnresolved": false, - "development-plan": { - "kind": "upgrade", - "request": "Migrate existing customer tier text values to a new enum field in an app upgrade.", - "root-cause": "The new schema needs an explicit, rerunnable migration for existing tenant data.", - "affected-files": [ - "src/Upgrade/CustomerTierUpgrade.Codeunit.al", - "test/Upgrade/CustomerTierUpgradeTests.Codeunit.al" - ], - "proposed-changes": [ - "Add a tagged upgrade step that copies existing values without validation triggers.", - "Add upgrade tests from multiple historical data versions." - ], - "test-strategy": "Run upgrade tests from two prior data versions and verify a second invocation makes no further changes.", - "acceptance-criteria": [ - "Existing values are preserved.", - "The migration is rerunnable.", - "Fresh installation does not execute upgrade migration." - ] - }, - "context": { - "bc-version": "28", - "technologies": [ - "al" - ], - "countries": [ - "w1" - ], - "application-area": [ - "all" - ], - "unknown": [] - }, - "requiredKnowledge": [ - "microsoft/knowledge/upgrade/use-upgrade-tags-not-version-checks.md", - "microsoft/knowledge/upgrade/check-only-triggers-do-not-migrate-data.md", - "microsoft/knowledge/upgrade/install-code-does-not-run-on-version-upgrade.md" - ], - "optionalKnowledge": [ - "microsoft/knowledge/upgrade/datatransfer-skips-triggers-and-subscribers.md", - "microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md" - ] - }, - { - "id": "no-additional-knowledge", - "title": "Honest empty enrichment does not prohibit ordinary work", - "expectedKind": "maintenance", - "expectedOutcome": "no-knowledge", - "expectedUnknown": [], - "requiresUnresolved": false, - "requiresMaterialUnresolved": false, - "development-plan": { - "kind": "maintenance", - "request": "Correct a spelling error in an existing internal explanatory comment in an AL procedure. Do not change the explanation, executable code, UI captions, schema, diagnostic text, configuration or behavior.", - "affected-files": ["src/Batch/EntryProcessor.Codeunit.al"], - "proposed-changes": ["Replace the misspelled word in the existing comment; add no new advice."], - "test-strategy": "Inspect the diff to confirm that only the comment spelling changes.", - "acceptance-criteria": ["Only the intended comment spelling changes; executable AL remains identical."] - }, - "context": { - "bc-version": "28", - "technologies": ["al"], - "countries": ["w1"], - "application-area": ["all"], - "unknown": [] - }, - "requiredKnowledge": [], - "optionalKnowledge": [] - }, - { - "id": "unknown-material-version", - "title": "Materially unresolved version-sensitive guidance stays partial", - "expectedKind": "feature", - "expectedOutcome": "partial", - "expectedUnknown": ["bc-version"], - "requiresUnresolved": true, - "requiresMaterialUnresolved": true, - "development-plan": { - "kind": "feature", - "request": "Add an expensive Sum FlowField as the source of a usually-hidden page control. The proposed design relies on visibility suppressing calculation.", - "affected-files": ["src/Pages/EntryOverview.Page.al"], - "proposed-changes": ["Bind the page control directly to the FlowField and set Visible to a conditional expression."], - "test-strategy": "Verify aggregate queries are not executed while the control is hidden.", - "acceptance-criteria": ["Hidden controls do not cause expensive aggregate queries."], - "unknown": ["The deployment BC version and visible-only calculation feature state cannot be established from this fixture. Do not invent either."] - }, - "context": { - "bc-version": "unknown", - "technologies": ["al"], - "countries": ["w1"], - "application-area": ["all"], - "unknown": ["bc-version"] - }, - "requiredKnowledge": [ - "microsoft/knowledge/performance/hidden-flowfields-still-calculate-before-bc26-opt-in.md" - ], - "optionalKnowledge": [] - }, - { - "id": "partial-plan-decision", - "title": "Known platform context does not resolve an incomplete plan decision", - "expectedKind": "refactor", - "expectedOutcome": "partial", - "expectedUnknown": [], - "requiresUnresolved": true, - "requiresMaterialUnresolved": true, - "development-plan": { - "kind": "refactor", - "request": "Refactor a record read helper currently using FindFirst followed by Next. The caller contract does not establish whether to return one row or enumerate the entire filtered set.", - "affected-files": ["src/Queries/EntryReader.Codeunit.al"], - "proposed-changes": ["Choose the read method consistent with the intended cardinality after that decision is clarified."], - "test-strategy": "Add cardinality assertions after the caller contract is decided.", - "acceptance-criteria": ["The method and enumeration agree with the clarified caller contract."], - "unknown": ["Single-record versus multi-record caller intent remains materially unresolved."] - }, - "context": { - "bc-version": "28", - "technologies": ["al"], - "countries": ["w1"], - "application-area": ["all"], - "unknown": [] - }, - "requiredKnowledge": [ - "microsoft/knowledge/performance/pair-findset-with-next-loop.md" - ], - "optionalKnowledge": [] - } - ] -} diff --git a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.bad.al b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.bad.al deleted file mode 100644 index 0c87bb2a..00000000 --- a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.bad.al +++ /dev/null @@ -1,28 +0,0 @@ -table 50603 "Sample Order Header Bad" -{ - fields - { - field(1; "No."; Code[20]) - { - DataClassification = CustomerContent; - } - field(2; "Document Date"; Date) - { - DataClassification = CustomerContent; - } - } - - trigger OnInsert() - var - SalesSetup: Record "Sales & Receivables Setup"; - NoSeries: Codeunit "No. Series"; - begin - "Document Date" := WorkDate(); - - if "No." = '' then begin - SalesSetup.Get(); - SalesSetup.TestField("Order Nos."); - "No." := NoSeries.GetNextNo(SalesSetup."Order Nos."); - end; - end; -} diff --git a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.good.al b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.good.al deleted file mode 100644 index 355d3d54..00000000 --- a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.good.al +++ /dev/null @@ -1,45 +0,0 @@ -table 50602 "Sample Order Header Good" -{ - fields - { - field(1; "No."; Code[20]) - { - DataClassification = CustomerContent; - } - field(2; "Document Date"; Date) - { - DataClassification = CustomerContent; - } - } - - trigger OnInsert() - var - SalesSetup: Record "Sales & Receivables Setup"; - NoSeries: Codeunit "No. Series"; - begin - if "No." = '' then begin - SalesSetup.Get(); - SalesSetup.TestField("Order Nos."); - "No." := NoSeries.GetNextNo(SalesSetup."Order Nos."); - end; - - InitRecord(); - end; - - procedure InitRecord() - begin - OnBeforeInitRecord(Rec); - "Document Date" := WorkDate(); - OnAfterInitRecord(Rec); - end; - - [IntegrationEvent(false, false)] - local procedure OnBeforeInitRecord(var SampleOrderHeader: Record "Sample Order Header Good") - begin - end; - - [IntegrationEvent(false, false)] - local procedure OnAfterInitRecord(var SampleOrderHeader: Record "Sample Order Header Good") - begin - end; -} diff --git a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md b/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md deleted file mode 100644 index e83a6d34..00000000 --- a/microsoft/knowledge/data-modeling/initialize-document-defaults-in-initrecord.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -bc-version: [all] -domain: data-modeling -keywords: [document-header, initrecord, number-series, default-values, oninsert, initialization] -technologies: [al] -countries: [w1] -application-area: [all] ---- - -# Initialize document defaults in `InitRecord` after assigning the number - -## Description - -Business Central document headers assign their number series first and then call an `InitRecord` procedure that owns the remaining business defaults, such as posting and document dates. Keeping that sequence and extensibility point makes initialization consistent for every creation path and lets extensions subscribe around one documented operation. Defaults scattered across page triggers or unrelated helpers can differ between UI, API, test, and background creation. - -## Best Practice - -In the document table's insert path, assign the document number and then call `InitRecord`. Keep the default assignments in that procedure and expose narrow before/after events when other extensions must participate. - -See sample: [`initialize-document-defaults-in-initrecord.good.al`](initialize-document-defaults-in-initrecord.good.al). - -## Anti Pattern - -Assigning document defaults in a page trigger, or scattering them directly through `OnInsert` with no `InitRecord` boundary. Non-page creation paths can then miss the defaults, and extensions have no stable initialization hook. - -See sample: [`initialize-document-defaults-in-initrecord.bad.al`](initialize-document-defaults-in-initrecord.bad.al). - -## Reference - -[Use the InitRecord function](https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/3-use-initrecord-function) diff --git a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.bad.al b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.bad.al deleted file mode 100644 index f8e11cca..00000000 --- a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.bad.al +++ /dev/null @@ -1,8 +0,0 @@ -codeunit 50601 "Directed Rounding Bad" -{ - procedure FloorAmount(Value: Decimal; Precision: Decimal): Decimal - begin - // For negative values, '<' rounds toward zero rather than toward negative infinity. - exit(Round(Value, Precision, '<')); - end; -} diff --git a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.good.al b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.good.al deleted file mode 100644 index cd8a80fc..00000000 --- a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.good.al +++ /dev/null @@ -1,10 +0,0 @@ -codeunit 50600 "Directed Rounding Good" -{ - procedure RoundAmount(Value: Decimal; Precision: Decimal; IncreaseMagnitude: Boolean): Decimal - begin - if IncreaseMagnitude then - exit(Round(Value, Precision, '>')); - - exit(Round(Value, Precision, '<')); - end; -} diff --git a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md b/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md deleted file mode 100644 index 260905db..00000000 --- a/microsoft/knowledge/data-modeling/round-direction-symbols-use-magnitude.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -bc-version: [all] -domain: data-modeling -keywords: [round, rounding, direction, precision, negative-decimal, amount] -technologies: [al] -countries: [w1] -application-area: [all] ---- - -# `Round` direction symbols follow magnitude, not mathematical ordering - -## Description - -AL's `Round(Number, Precision, Direction)` uses `'>'` to round away from zero and `'<'` to round toward zero. For a negative value this reverses mathematical ordering: `Round(-1234.56789, 0.001, '<')` returns `-1234.567`, while direction `'>'` returns `-1234.568`. Code that treats the symbols as mathematical ceiling and floor produces sign-dependent amount errors, commonly on credit documents and negative adjustments. - -## Best Practice - -Choose the direction from the business meaning: `'>'` increases absolute magnitude and `'<'` decreases absolute magnitude for both positive and negative values. Include positive and negative cases whenever a directed rounding rule is tested. - -See sample: [`round-direction-symbols-use-magnitude.good.al`](round-direction-symbols-use-magnitude.good.al). - -## Anti Pattern - -Using `'<'` as a mathematical floor or `'>'` as a mathematical ceiling. The result looks correct for positive amounts but moves in the opposite mathematical direction for negative amounts. - -See sample: [`round-direction-symbols-use-magnitude.bad.al`](round-direction-symbols-use-magnitude.bad.al). - -## Reference - -[Use the Round function](https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/4a-use-round-function) diff --git a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.bad.al b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.bad.al deleted file mode 100644 index 4fd98be0..00000000 --- a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.bad.al +++ /dev/null @@ -1,20 +0,0 @@ -interface "I Quote Amount Bad" -{ - procedure GetAmount(): Decimal; -} - -interface "I Quote Date Bad" -{ - procedure GetDate(): Date; -} - -codeunit 50611 "Quote Reader Bad" -{ - procedure GetDate(Quote: Interface "I Quote Amount Bad"): Date - var - DatedQuote: Interface "I Quote Date Bad"; - begin - DatedQuote := Quote as "I Quote Date Bad"; - exit(DatedQuote.GetDate()); - end; -} diff --git a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.good.al b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.good.al deleted file mode 100644 index f58a19d8..00000000 --- a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.good.al +++ /dev/null @@ -1,24 +0,0 @@ -interface "I Quote Amount Good" -{ - procedure GetAmount(): Decimal; -} - -interface "I Quote Date Good" -{ - procedure GetDate(): Date; -} - -codeunit 50610 "Quote Reader Good" -{ - procedure TryGetDate(Quote: Interface "I Quote Amount Good"; var QuoteDate: Date): Boolean - var - DatedQuote: Interface "I Quote Date Good"; - begin - if not (Quote is "I Quote Date Good") then - exit(false); - - DatedQuote := Quote as "I Quote Date Good"; - QuoteDate := DatedQuote.GetDate(); - exit(true); - end; -} diff --git a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.md b/microsoft/knowledge/interfaces/guard-interface-casts-with-is.md deleted file mode 100644 index 94444082..00000000 --- a/microsoft/knowledge/interfaces/guard-interface-casts-with-is.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -bc-version: [25..] -domain: interfaces -keywords: [interface, is-operator, as-operator, type-test, cast, variant, runtime-error] -technologies: [al] -countries: [w1] -application-area: [all] ---- - -# Guard optional interface casts with `is` - -## Description - -From runtime 14.0, AL can type-test an interface or `Variant` with `is` and cast it to another interface with `as`. The test is non-throwing, but `as` raises a runtime error when the underlying codeunit does not implement the target interface. This matters when an extended capability is optional or implementations can come from other extensions. - -## Best Practice - -Use `is` to establish that the value supports the target interface before using `as`. Cast directly only where the target implementation is an invariant guaranteed by the surrounding contract. - -See sample: [`guard-interface-casts-with-is.good.al`](guard-interface-casts-with-is.good.al). - -## Anti Pattern - -Using `as` unconditionally for an optional extended interface. An otherwise valid implementation of the base interface then fails at runtime merely because it does not implement the additional contract. - -See sample: [`guard-interface-casts-with-is.bad.al`](guard-interface-casts-with-is.bad.al). - -## Reference - -[Understand type testing and casting operators for interfaces](https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/type-testing) diff --git a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.bad.al b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.bad.al deleted file mode 100644 index 78602b4d..00000000 --- a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.bad.al +++ /dev/null @@ -1,9 +0,0 @@ -tableextension 50622 "Ship-to Dropdown Bad" extends "Ship-to Address" -{ - fieldgroups - { - addlast(DropDown; "Address 2") - { - } - } -} diff --git a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.good.al b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.good.al deleted file mode 100644 index c8a8c28c..00000000 --- a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.good.al +++ /dev/null @@ -1,20 +0,0 @@ -tableextension 50620 "Ship-to Dropdown Good" extends "Ship-to Address" -{ - fieldgroups - { - addlast(DropDown; "Address 2") - { - } - } -} - -pageextension 50621 "Ship-to Lookup Good" extends "Ship-to Address List" -{ - layout - { - modify("Address 2") - { - Visible = true; - } - } -} diff --git a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md b/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md deleted file mode 100644 index afa2c34d..00000000 --- a/microsoft/knowledge/ui/dropdown-fieldgroup-respects-lookup-page-visibility.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -bc-version: [all] -domain: ui -keywords: [fieldgroup, dropdown, addlast, lookup-page, visible, tableextension, pageextension] -technologies: [al] -countries: [w1] -application-area: [all] ---- - -# A `DropDown` field remains hidden when its lookup-page control is hidden - -## Description - -A tableextension can append a field to the `DropDown` field group with `addlast`, but the client still omits that field when its control on the underlying lookup page has `Visible = false`. Changing only the table field group therefore compiles while producing no visible UI change. The field-group name is case-sensitive and must be written as `DropDown`. - -## Best Practice - -When adding a hidden field to a `DropDown` field group, also extend the page used for the lookup and make that field control visible. Verify the actual lookup page rather than assuming the table definition alone controls the drop-down. - -See sample: [`dropdown-fieldgroup-respects-lookup-page-visibility.good.al`](dropdown-fieldgroup-respects-lookup-page-visibility.good.al). - -## Anti Pattern - -Adding the field with `addlast(DropDown; ...)` while leaving its lookup-page control hidden, then expecting the field to appear in the drop-down. - -See sample: [`dropdown-fieldgroup-respects-lookup-page-visibility.bad.al`](dropdown-fieldgroup-respects-lookup-page-visibility.bad.al). - -## Reference - -[Add a new FieldGroup to an existing table](https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/add-field-group) diff --git a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.bad.al b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.bad.al deleted file mode 100644 index f5d2b2be..00000000 --- a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.bad.al +++ /dev/null @@ -1,26 +0,0 @@ -page 50631 "Sample Order Bad" -{ - PageType = Document; - SourceTable = "Sales Header"; - - layout - { - area(Content) - { - group(General) - { - field(Amount; Rec.Amount) - { - ApplicationArea = All; - ToolTip = 'Specifies the total amount of the order.'; - } - } - part(Lines; "Sales Order Subform") - { - ApplicationArea = All; - SubPageLink = "Document Type" = field("Document Type"), - "Document No." = field("No."); - } - } - } -} diff --git a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.good.al b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.good.al deleted file mode 100644 index 34620981..00000000 --- a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.good.al +++ /dev/null @@ -1,27 +0,0 @@ -page 50630 "Sample Order Good" -{ - PageType = Document; - SourceTable = "Sales Header"; - - layout - { - area(Content) - { - group(General) - { - field(Amount; Rec.Amount) - { - ApplicationArea = All; - ToolTip = 'Specifies the total amount of the order.'; - } - } - part(Lines; "Sales Order Subform") - { - ApplicationArea = All; - SubPageLink = "Document Type" = field("Document Type"), - "Document No." = field("No."); - UpdatePropagation = Both; - } - } - } -} diff --git a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md b/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md deleted file mode 100644 index 3ce676b5..00000000 --- a/microsoft/knowledge/ui/updatepropagation-both-refreshes-main-page.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -bc-version: [all] -domain: ui -keywords: [updatepropagation, page-part, subpage, main-page, refresh, flowfield, document-lines] -technologies: [al] -countries: [w1] -application-area: [all] ---- - -# Use `UpdatePropagation = Both` when line edits must refresh the main page - -## Description - -A page part does not automatically refresh its parent page when the subpage changes. `UpdatePropagation = Subpage` updates only the part; `Both` also refreshes the main page. Without `Both`, header totals, FlowFields, and FactBoxes that depend on edited lines can remain stale until another user action refreshes the page. - -## Best Practice - -Set `UpdatePropagation = Both` on a part when edits in that subpage must immediately update values rendered by the main page. Leave propagation at `Subpage` when the parent has no dependent presentation to avoid unnecessary refreshes. - -See sample: [`updatepropagation-both-refreshes-main-page.good.al`](updatepropagation-both-refreshes-main-page.good.al). - -## Anti Pattern - -Displaying a line-dependent total on the main page while the editable lines part updates only itself. The persisted values can be correct while the parent page continues to show an old total. - -See sample: [`updatepropagation-both-refreshes-main-page.bad.al`](updatepropagation-both-refreshes-main-page.bad.al). - -## Reference - -[Set different control properties](https://learn.microsoft.com/en-us/training/modules/work-with-pages/8-controls) diff --git a/microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md b/microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md deleted file mode 100644 index 61e74226..00000000 --- a/microsoft/knowledge/upgrade/appversion-meaning-depends-on-execution-context.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -bc-version: [all] -domain: upgrade -keywords: [appversion, dataversion, moduleinfo, install-codeunit, upgrade-codeunit, version-context] -technologies: [al] -countries: [w1] -application-area: [all] ---- - -# `ModuleInfo.AppVersion` changes meaning with execution context - -## Description - -`ModuleInfo.AppVersion()` is the installed version during normal operation, the version being installed inside install code, and the target version inside upgrade code. It is therefore not the source data version during an upgrade. In upgrade code, `DataVersion()` describes the version of the existing data, whether from the currently installed app or the version most recently uninstalled. - -## Best Practice - -Interpret `AppVersion()` as the code package entering the context and `DataVersion()` as the existing data state. Prefer upgrade tags for controlling individual migration steps; when version information is needed for diagnostics or preconditions, name variables so target app version and source data version cannot be confused. - -## Anti Pattern - -Reading `AppVersion()` from an upgrade codeunit and treating it as the version being upgraded from. The comparison actually observes the target package and can skip or misroute migration logic. - -## Reference - -[Create proper installation and upgrade codeunits](https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/3-installation-upgrade-codeunits) diff --git a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.bad.al b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.bad.al deleted file mode 100644 index 2a92d112..00000000 --- a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.bad.al +++ /dev/null @@ -1,28 +0,0 @@ -codeunit 50641 "Sample Upgrade Part One" -{ - Subtype = Upgrade; - - trigger OnUpgradePerCompany() - begin - CreateUpgradeState(); - end; - - local procedure CreateUpgradeState() - begin - end; -} - -codeunit 50642 "Sample Upgrade Part Two" -{ - Subtype = Upgrade; - - trigger OnUpgradePerCompany() - begin - // This can run before Part One; object IDs do not sequence upgrade codeunits. - MigrateDataThatRequiresUpgradeState(); - end; - - local procedure MigrateDataThatRequiresUpgradeState() - begin - end; -} diff --git a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.good.al b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.good.al deleted file mode 100644 index 42346a88..00000000 --- a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.good.al +++ /dev/null @@ -1,18 +0,0 @@ -codeunit 50640 "Sample Upgrade Good" -{ - Subtype = Upgrade; - - trigger OnUpgradePerCompany() - begin - CreateUpgradeState(); - MigrateDependentData(); - end; - - local procedure CreateUpgradeState() - begin - end; - - local procedure MigrateDependentData() - begin - end; -} diff --git a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md b/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md deleted file mode 100644 index b11a8425..00000000 --- a/microsoft/knowledge/upgrade/install-and-upgrade-codeunits-have-no-order.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -bc-version: [all] -domain: upgrade -keywords: [install-codeunit, upgrade-codeunit, execution-order, subtype-install, subtype-upgrade, sequencing] -technologies: [al] -countries: [w1] -application-area: [all] ---- - -# Separate install or upgrade codeunits have no execution order - -## Description - -An extension can contain multiple `Install` or `Upgrade` codeunits, but Business Central does not guarantee the order in which codeunits of the same subtype execute. Upgrade trigger phases are ordered globally, yet one codeunit's `OnUpgradePerCompany` must not assume another codeunit's same-phase trigger already ran. Object ID and source-file order do not provide sequencing. - -## Best Practice - -Keep separate install or upgrade codeunits independent. When two steps have a real dependency, coordinate them from one owning trigger in the required order; use upgrade tags to make each completed step idempotent. - -See sample: [`install-and-upgrade-codeunits-have-no-order.good.al`](install-and-upgrade-codeunits-have-no-order.good.al). - -## Anti Pattern - -Splitting dependent steps into separate codeunits and relying on names, object IDs, or declaration order. The dependent codeunit can run first and fail or observe partially migrated data. - -See sample: [`install-and-upgrade-codeunits-have-no-order.bad.al`](install-and-upgrade-codeunits-have-no-order.bad.al). - -## Reference - -[Create proper installation and upgrade codeunits](https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/3-installation-upgrade-codeunits) diff --git a/microsoft/skills/development/al-development-plan.md b/microsoft/skills/development/al-development-plan.md index 115e5fe7..d3d65d57 100644 --- a/microsoft/skills/development/al-development-plan.md +++ b/microsoft/skills/development/al-development-plan.md @@ -16,7 +16,7 @@ application-area: [all] Selects the BCQuality knowledge that should constrain an existing AL development plan. It does not implement, edit, stage, commit, or publish anything in the target repository. Repository-specific orchestrators can consume this skill before their own test and implementation phases while retaining ownership of workflow, tooling, and delivery. -Both a readable `repository` and a non-empty `development-plan` are required. The plan may be structured data or text, but it must identify the intended change. Return `not-applicable` without changing files when either input is absent or the repository is not an AL project. +A non-empty `development-plan` is required. A readable `repository` is optional but recommended: use it to confirm affected symbols and applicability context when supplied. When it is absent, preserve repository-dependent facts as unknown and return `partial` only when that missing context materially prevents reliable selection. Return `not-applicable` without changing files when the plan is absent or does not identify the intended change. The caller supplies its existing plan, not a request to generate one. Consumer-specific formats must be normalized by the consumer before invocation. This skill does not interpret issue records, continuation markers, batons, retries, or workflow state. A serialized document containing plan metadata and a markdown body is acceptable when it states the intended change, affected surfaces, proposed approach, test strategy, and acceptance criteria. Missing details remain unknown; do not invent them. @@ -24,7 +24,7 @@ The caller supplies its existing plan, not a request to generate one. Consumer-s Read the BCQuality knowledge index once, using the external path supplied by Entry when present. If no index is available, use READ's path-based discovery across enabled layers; inability to read the corpus is `failed`, not `no-knowledge`. Use entries from every enabled layer and domain. The index supplies candidate paths, applicability dimensions, keywords, titles, and descriptions; it never substitutes for opening selected articles in full. -Inspect the target repository read-only for `app.json`, affected files and symbols named by the plan, relevant tests, permission sets, dependencies, target/runtime versions, countries, application areas, and repository conventions. Do not create scratch or generated files inside the target repository. +When supplied, inspect the target repository read-only for `app.json`, affected files and symbols named by the plan, relevant tests, permission sets, dependencies, target/runtime versions, countries, application areas, and repository conventions. Do not create scratch or generated files inside the target repository. ## Relevance @@ -53,6 +53,8 @@ When a dimension cannot be resolved, retain conditionally applicable candidates Keep the worklist focused. Do not include generic engineering advice, an entire domain, or an article that would not change implementation or validation. +This skill deliberately does not dispatch the review leaves. Review leaves inspect existing source and emit defects through domain-specific code signals; plan enrichment runs before that source exists and must collect constraints that cross several domains. Both paths consume the same indexed article metadata and full normative article bodies, so new knowledge is automatically eligible for plan retrieval. Review-leaf token maps remain code-detection precision rules, not a second registry that plan retrieval must duplicate. + ## Action For each worklist article: diff --git a/microsoft/skills/review/al-data-modeling-review.md b/microsoft/skills/review/al-data-modeling-review.md index 3fca8ecb..05dcd0ba 100644 --- a/microsoft/skills/review/al-data-modeling-review.md +++ b/microsoft/skills/review/al-data-modeling-review.md @@ -39,7 +39,7 @@ Narrow the relevant files to the subset that applies to the changes under review - The changed AL object names and types — especially `* Setup` singleton tables and Card pages, custom master tables, tableextensions that add master-data fields, and document or journal lines that reference a master. - The changed fields, keys, triggers, and procedures, weighted toward `Primary Key`, `No.`, `No. Series`, `Blocked`, `Last Date Modified`, `OnInsert`, `OnModify`, `OnRename`, reference-field `OnValidate`, and posting validation. -- Tokens extracted from the diff that relate to data modeling (`setup`, `master`, `Primary Key`, `Code[10]`, `Code[20]`, `AutoIncrement`, `SystemId`, `No.`, `No. Series`, `NoSeriesManagement`, `Codeunit "No. Series"`, `GetNextNo`, `IsManual`, `TestManual`, `Blocked`, `TestField`, `Last Date Modified`, `Today`, `WorkDate`, `InsertAllowed`, `DeleteAllowed`, `PageType = Card`, `OnOpenPage`, `GetRecordOnce`, `OnInsert`, `OnModify`, `OnRename`, `InitRecord`, `Round`, `Precision`, `Direction`, `TableRelation`, `tableextension`, `enumextension`, `Media`, `MediaSet`, `Item`, `Count`). +- Tokens extracted from the diff that relate to data modeling (`setup`, `master`, `Primary Key`, `Code[10]`, `Code[20]`, `AutoIncrement`, `SystemId`, `No.`, `No. Series`, `NoSeriesManagement`, `Codeunit "No. Series"`, `GetNextNo`, `IsManual`, `TestManual`, `Blocked`, `TestField`, `Last Date Modified`, `Today`, `WorkDate`, `InsertAllowed`, `DeleteAllowed`, `PageType = Card`, `OnOpenPage`, `GetRecordOnce`, `OnInsert`, `OnModify`, `OnRename`, `TableRelation`, `tableextension`, `enumextension`, `Media`, `MediaSet`, `Item`, `Count`). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. When the diff contains no data-modeling changes by any of the above signals, return `outcome: "not-applicable"` without evaluating files. @@ -52,8 +52,6 @@ The following targeted checks cover every current `data-modeling` article. Treat - A master table adds or changes `Last Date Modified`, `OnModify`, or `OnRename`, but the non-editable field is not assigned `Today()` in both triggers — `set-last-date-modified-in-onmodify-and-onrename`. - A `tableextension` appends a conditional `TableRelation` as if it overrides an earlier unconditional relation, or relation branches are otherwise designed without accounting for additive top-down evaluation — `table-relation-extensions-are-additive-and-top-down`. - A `Media` or `MediaSet` field is assigned directly between different table types or different field IDs instead of registering each shared item with `MediaSet.Insert` — `share-mediaset-items-with-insert-not-field-assignment`. -- A custom document header assigns defaults outside an `InitRecord` boundary, calls `InitRecord` before assigning its number, or places UI-independent defaults only in a page trigger — `initialize-document-defaults-in-initrecord`. -- Directed `Round` calls use `'<'` as mathematical floor or `'>'` as mathematical ceiling, especially where negative amounts are possible — `round-direction-symbols-use-magnitude`. Once the candidate worklist is known, resolve layer-precedence conflicts per READ. Drop lower-precedence files whose normative guidance (`## Best Practice` or `## Anti Pattern`) directly contradicts a higher-precedence candidate, and record each dropped file in `suppressed` with `reason: "layer-precedence"`. Files that would have been candidates but are hidden because their layer is disabled in consumer configuration are recorded with `reason: "configuration"`. Files that never became candidates are NOT recorded in `suppressed`. diff --git a/microsoft/skills/review/al-interfaces-review.md b/microsoft/skills/review/al-interfaces-review.md index 1c38be76..f5d65cd0 100644 --- a/microsoft/skills/review/al-interfaces-review.md +++ b/microsoft/skills/review/al-interfaces-review.md @@ -39,7 +39,7 @@ Narrow the relevant files to the subset that applies to the changes under review - The changed AL object names and types — especially `interface` objects, codeunits and enums declared with the `implements` keyword, and consumers that declare or assign an `Interface` variable. - The changed procedures and triggers, weighted toward factory or dispatch routines that resolve a variant to behaviour, setter-injection procedures that take an `Interface` parameter, and `case`-over-enum blocks that select between strategies. -- Tokens extracted from the diff that relate to interfaces and enum-backed implementation (`interface`, `extends`, `implements`, `Implementation`, `DefaultImplementation`, `UnknownValueImplementation`, `enum`, `Extensible`, `Interface`, `Variant`, `is`, `as`, `case`, and the `case of` anti-pattern signal — a `case` over an enum value whose branches choose between variant computations). +- Tokens extracted from the diff that relate to interfaces and enum-backed implementation (`interface`, `extends`, `implements`, `Implementation`, `DefaultImplementation`, `UnknownValueImplementation`, `enum`, `Extensible`, `Interface`, `case`, and the `case of` anti-pattern signal — a `case` over an enum value whose branches choose between variant computations). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. @@ -54,7 +54,6 @@ The following targeted checks map diff signals to specific `interfaces` articles - `DefaultImplementation` used as the only fallback where a persisted ordinal may no longer match any declared enum value, or a persisted enum lacks `UnknownValueImplementation` on BC18 or later — `handle-unknown-enum-ordinals-with-unknownvalueimplementation`. - A method added directly to an interface that exists in the baseline, instead of adding a BC25+ interface that `extends` it or a versioned sibling for older targets — `extend-published-interfaces-dont-edit-them`. - A declared enum value with no `Implementation` and no enum-level `DefaultImplementation` — `set-defaultimplementation-on-enum`. -- An `Interface` or `Variant` is cast with `as` to an optional extended interface without first establishing support with `is` — `guard-interface-casts-with-is`. For `set-defaultimplementation-on-enum`, inspect the complete containing enum before emitting. An enum-level `DefaultImplementation = = ;` conclusively covers every declared value that omits its own `Implementation`; do not flag such a value and do not replace the intentional fallback with a per-value mapping. diff --git a/microsoft/skills/review/al-ui-review.md b/microsoft/skills/review/al-ui-review.md index 8c35f498..8ffd7319 100644 --- a/microsoft/skills/review/al-ui-review.md +++ b/microsoft/skills/review/al-ui-review.md @@ -41,15 +41,10 @@ Narrow the relevant files to the subset that applies to the changes under review - **UI-file filter.** UI review applies to files declaring `page`, `pageextension`, or `pagecustomization`, and to JavaScript/CSS/HTML that implements a control add-in's rendering or Business Central communication. When the diff contains no such files, return `outcome: "not-applicable"` without evaluating knowledge files. - For each relevant knowledge file, compute overlap against changed page declarations and control add-in files, weighted toward `Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `OptionCaption`, `ShowCaption`, `InstructionalText`, `GridLayout`, `Style`, `StyleExpr`, promoted action definitions, field importance, page background tasks, DOM creation, ARIA attributes, keyboard/focus handlers, packaged-resource AJAX, and calls from JavaScript into AL. -- Tokens extracted from the diff (`Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `PageType`, `ShowCaption`, `InstructionalText`, `grid`, `fixed`, `GridLayout`, `Style`, `StyleExpr`, `Importance`, `Promoted`, `Additional`, `area(Promoted)`, `actionref`, `PromotedCategory`, `PromotedOnly`, `PromotedIsBig`, `ShowAs`, `SplitButton`, `fieldgroups`, `DropDown`, `UpdatePropagation`, `EnqueueBackgroundTask`, `OnAfterGetCurrRecord`, `OnAfterGetRecord`, `OnPageBackgroundTaskCompleted`, `OnPageBackgroundTaskError`, `RunPageBackgroundTask`, `Favorable`, `Unfavorable`, `Ambiguous`, `cuegroup`, `controladdin`, `control-add-in`, `usercontrol`, `aria-`, `tabindex`, `keydown`, `focus`, `innerHTML`, `createElement`, `packaged-resource`, `ajax`, `$.get`, `$.ajax`, `XMLHttpRequest`, `xhrFields`, `withCredentials`, `withcredentials`, `InvokeExtensibilityMethod`, `invokeextensibilitymethod`, `skipIfBusy`, `successCallback`, `success-callback`, `errorCallback`, `setInterval`, `JSON.stringify`, `payload`, `throttling`, `reduced-functionality`, `ClientServicesMaxUploadSize`, `&`, `Specifies`, `Message(`, `Confirm(`, `Error(` in a page context, `Disabled`, `Invalid`, `Whitelist`, `Blacklist`, trailing punctuation patterns on captions). +- Tokens extracted from the diff (`Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `PageType`, `ShowCaption`, `InstructionalText`, `grid`, `fixed`, `GridLayout`, `Style`, `StyleExpr`, `Importance`, `Promoted`, `Additional`, `area(Promoted)`, `actionref`, `PromotedCategory`, `PromotedOnly`, `PromotedIsBig`, `ShowAs`, `SplitButton`, `EnqueueBackgroundTask`, `OnAfterGetCurrRecord`, `OnAfterGetRecord`, `OnPageBackgroundTaskCompleted`, `OnPageBackgroundTaskError`, `RunPageBackgroundTask`, `Favorable`, `Unfavorable`, `Ambiguous`, `cuegroup`, `controladdin`, `control-add-in`, `usercontrol`, `aria-`, `tabindex`, `keydown`, `focus`, `innerHTML`, `createElement`, `packaged-resource`, `ajax`, `$.get`, `$.ajax`, `XMLHttpRequest`, `xhrFields`, `withCredentials`, `withcredentials`, `InvokeExtensibilityMethod`, `invokeextensibilitymethod`, `skipIfBusy`, `successCallback`, `success-callback`, `errorCallback`, `setInterval`, `JSON.stringify`, `payload`, `throttling`, `reduced-functionality`, `ClientServicesMaxUploadSize`, `&`, `Specifies`, `Message(`, `Confirm(`, `Error(` in a page context, `Disabled`, `Invalid`, `Whitelist`, `Blacklist`, trailing punctuation patterns on captions). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed page element. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. -Apply these high-signal mappings before fuzzy topic ranking: - -- A tableextension adds a field to `DropDown` while the corresponding lookup-page control remains `Visible = false` — `dropdown-fieldgroup-respects-lookup-page-visibility`. -- An editable page part affects a total, FlowField, or FactBox on the parent but does not set `UpdatePropagation = Both` — `updatepropagation-both-refreshes-main-page`. - Once the candidate worklist is known, resolve layer-precedence conflicts per READ and record suppressions. When the post-conflict worklist is empty because no applicable UI knowledge exists, or because configuration suppressed every candidate, emit `outcome: "no-knowledge"`. When the worklist is empty because no applicable UI knowledge matched the page changes, emit `outcome: "completed"` with an empty `findings` array. diff --git a/microsoft/skills/review/al-upgrade-review.md b/microsoft/skills/review/al-upgrade-review.md index 9ae61f8f..94851a36 100644 --- a/microsoft/skills/review/al-upgrade-review.md +++ b/microsoft/skills/review/al-upgrade-review.md @@ -39,12 +39,10 @@ Narrow the relevant files to the subset that applies to the changes under review - The changed AL object names and types — especially codeunits with `Subtype = Upgrade` or `Subtype = Install`, tables and tableextensions adding or changing fields, enums and enumextensions, and objects under `Hybrid*`/`Migration`/`Upgrade` namespaces. - The changed triggers and procedures, weighted toward `OnCheckPreconditionsPerCompany`/`PerDatabase`, `OnUpgradePerCompany`/`PerDatabase`, `OnValidateUpgradePerCompany`/`PerDatabase`, `OnInstallAppPerCompany`/`PerDatabase`, the `OnGetPerCompanyUpgradeTags`/`OnGetPerDatabaseUpgradeTags` subscribers, and helper procedures transitively reachable from those entry points. -- Tokens extracted from the diff that relate to upgrade concerns (`Subtype = Upgrade`, `Subtype = Install`, `Upgrade Tag`, `HasUpgradeTag`, `SetUpgradeTag`, `OnCheckPreconditions`, `OnUpgrade`, `OnValidateUpgrade`, `OnInstallApp`, `DataTransfer`, `CopyFields`, `Insert`, `Modify`, `Delete`, `Rename`, `InitValue`, `ObsoleteState`, `ObsoleteReason`, `ObsoleteTag`, `ModuleInfo`, `AppVersion`, `DataVersion`, `NavApp.GetCurrentModuleInfo`, `ExecutionContext`, `PrimaryKey`, `key(`, `field(`, `value(`, `enum`, `enumextension`, `HybridSL`, `HybridGP`, `HybridBC`, `HybridBaseDeployment`). +- Tokens extracted from the diff that relate to upgrade concerns (`Subtype = Upgrade`, `Subtype = Install`, `Upgrade Tag`, `HasUpgradeTag`, `SetUpgradeTag`, `OnCheckPreconditions`, `OnUpgrade`, `OnValidateUpgrade`, `OnInstallApp`, `DataTransfer`, `CopyFields`, `Insert`, `Modify`, `Delete`, `Rename`, `InitValue`, `ObsoleteState`, `ObsoleteReason`, `ObsoleteTag`, `DataVersion`, `ExecutionContext`, `PrimaryKey`, `key(`, `field(`, `value(`, `enum`, `enumextension`, `HybridSL`, `HybridGP`, `HybridBC`, `HybridBaseDeployment`). - For each `OnCheckPreconditions...` and `OnValidateUpgrade...` trigger, build the best available call graph from surrounding unchanged source as well as changed hunks, tracing resolved calls through reachable local or internal helpers. Worklist the check-only rule when a database write occurs either directly in the trigger or in any helper procedure reachable from it. Writes include `Insert`, `Modify`, `ModifyAll`, `Delete`, `DeleteAll`, `Rename`, and `DataTransfer`. Also perform the reverse check when a PR changes a writing helper body: worklist the rule when that helper is invoked directly or transitively by an unchanged check or validation trigger. - Treat a direct write or a fully resolved call chain as high-confidence evidence. When cross-object dispatch, unavailable declarations, or an incomplete call graph prevents proving the complete chain, cap confidence at `medium`, name the unresolved edge in the finding, and do not claim a violation without a resolved path from a check or validation trigger to a write. - Worklist the install-versus-upgrade rule when migration helpers are reachable only from an install codeunit. -- Worklist `install-and-upgrade-codeunits-have-no-order.md` when a change adds multiple install or upgrade codeunits whose same-phase triggers share state or depend on one another. -- Worklist `appversion-meaning-depends-on-execution-context.md` when install or upgrade code branches on `ModuleInfo.AppVersion()` or confuses it with `DataVersion()`. A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. When the diff contains no upgrade-related changes by any of the above signals, return `outcome: "not-applicable"` without evaluating files. diff --git a/plugin.json b/plugin.json index edb78c9d..c99785b0 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "name": "bcquality", - "description": "Quality skills and knowledge for Business Central. Exposes read-only AL plan enrichment and code-review adapters backed by BCQuality's Entry protocol.", - "version": "0.3.0", + "description": "Quality skills and knowledge for Business Central development. Exposes a standalone AL review adapter backed by BCQuality's Entry protocol.", + "version": "0.2.0", "author": { "name": "microsoft/BCQuality", "url": "https://github.com/microsoft/BCQuality" @@ -13,7 +13,6 @@ "al", "business-central", "code-review", - "plan-guidance", "quality" ], "skills": [ diff --git a/schemas/development-guidance-report.schema.json b/schemas/development-guidance-report.schema.json new file mode 100644 index 00000000..9ace0f77 --- /dev/null +++ b/schemas/development-guidance-report.schema.json @@ -0,0 +1,183 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/microsoft/BCQuality/schemas/development-guidance-report.schema.json", + "title": "BCQuality development-guidance report", + "type": "object", + "additionalProperties": false, + "required": [ + "skill", + "outcome", + "summary", + "context", + "knowledge", + "validation-considerations", + "suppressed", + "unresolved" + ], + "properties": { + "skill": { + "type": "object", + "additionalProperties": false, + "required": ["id", "version"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, + "version": { "type": "integer", "minimum": 1 } + } + }, + "outcome": { + "enum": ["completed", "not-applicable", "no-knowledge", "partial", "failed"] + }, + "outcome-reason": { "type": "string", "minLength": 1 }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": ["request", "kind", "candidates", "selected"], + "properties": { + "request": { "type": "string", "minLength": 1 }, + "kind": { "enum": ["feature", "bug", "refactor", "upgrade", "maintenance"] }, + "candidates": { "type": "integer", "minimum": 0 }, + "selected": { "type": "integer", "minimum": 0 } + } + }, + "context": { + "type": "object", + "additionalProperties": false, + "required": [ + "bc-version", + "technologies", + "countries", + "application-area", + "unknown" + ], + "properties": { + "bc-version": { "type": "string", "minLength": 1 }, + "technologies": { "$ref": "#/definitions/stringArray" }, + "countries": { "$ref": "#/definitions/stringArray" }, + "application-area": { "$ref": "#/definitions/stringArray" }, + "unknown": { + "type": "array", + "uniqueItems": true, + "items": { + "enum": ["bc-version", "technologies", "countries", "application-area"] + } + } + } + }, + "knowledge": { + "type": "array", + "uniqueItems": true, + "items": { "$ref": "#/definitions/knowledge" } + }, + "validation-considerations": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "reason", "evidence"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, + "reason": { "type": "string", "minLength": 1 }, + "evidence": { "type": "string", "minLength": 1 } + } + } + }, + "suppressed": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["reference", "reason"], + "properties": { + "reference": { "$ref": "#/definitions/reference" }, + "reason": { "enum": ["layer-precedence", "configuration"] } + } + } + }, + "unresolved": { "$ref": "#/definitions/stringArray" } + }, + "allOf": [ + { + "if": { + "properties": { + "outcome": { "enum": ["partial", "failed"] } + }, + "required": ["outcome"] + }, + "then": { "required": ["outcome-reason"] } + }, + { + "if": { + "properties": { + "outcome": { "const": "completed" } + }, + "required": ["outcome"] + }, + "then": { + "properties": { + "knowledge": { "minItems": 1 } + } + } + }, + { + "if": { + "properties": { + "outcome": { "enum": ["not-applicable", "no-knowledge"] } + }, + "required": ["outcome"] + }, + "then": { + "properties": { + "knowledge": { "maxItems": 0 } + } + } + } + ], + "definitions": { + "stringArray": { + "type": "array", + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + }, + "reference": { + "type": "object", + "additionalProperties": false, + "required": ["path"], + "properties": { + "path": { + "type": "string", + "pattern": "^(microsoft|community|custom)/knowledge/[a-z0-9-]+/(?:[a-z0-9-]+/)*[a-z0-9-]+\\.md$" + }, + "sha": { "type": "string", "pattern": "^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$" } + } + }, + "knowledge": { + "type": "object", + "additionalProperties": false, + "required": ["path", "used-for", "constraints", "sample-paths"], + "properties": { + "path": { + "type": "string", + "pattern": "^(microsoft|community|custom)/knowledge/[a-z0-9-]+/(?:[a-z0-9-]+/)*[a-z0-9-]+\\.md$" + }, + "sha": { "type": "string", "pattern": "^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$" }, + "used-for": { "type": "string", "minLength": 1 }, + "constraints": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + }, + "sample-paths": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^(microsoft|community|custom)/knowledge/[a-z0-9-]+/(?:[a-z0-9-]+/)*[a-z0-9-]+\\.(good|bad)\\.[a-zA-Z0-9]+$" + } + } + } + } + } +} diff --git a/skills/README.md b/skills/README.md index b1d6391a..766131f5 100644 --- a/skills/README.md +++ b/skills/README.md @@ -1,7 +1,7 @@ # BCQuality global skills This folder contains BCQuality's layer-independent protocol files and the -host-native adapters used by standalone plugin installations. +host-native adapter used by standalone plugin installations. The protocol files have two kinds: @@ -31,9 +31,8 @@ READ and DO are read on demand — typically by the first action skill the agent | Path | Role | |---|---| | [`al-code-review/SKILL.md`](al-code-review/SKILL.md) | Exposes BCQuality through the standard `SKILL.md` format when this repository is installed as a plugin. | -| [`al-development-plan/SKILL.md`](al-development-plan/SKILL.md) | Enriches an existing AL plan read-only through the standard `SKILL.md` format; does not generate a plan or implement code. | -Each adapter is deliberately thin. It translates the caller's request into an +The adapter is deliberately thin. It translates the caller's request into an Entry task context, then follows Entry's dispatch without owning routing, review, index, or output policy. It is not an action skill, is not considered by Entry, and should not accumulate behavior already defined by `entry.md`, @@ -41,23 +40,18 @@ by Entry, and should not accumulate behavior already defined by `entry.md`, This gives the two skill formats distinct roles: -- `skills/al-code-review/SKILL.md` and - `skills/al-development-plan/SKILL.md` are the public host integration - surfaces for a standalone plugin installation. +- `skills/al-code-review/SKILL.md` is the public host integration surface for a + standalone plugin installation. - `microsoft/skills/review/al-code-review.md` is BCQuality's internal Microsoft-layer super-skill for coordinating a broad AL review. -- `microsoft/skills/development/al-development-plan.md` is the read-only - knowledge-enrichment interface for existing plans. Consumers own format - normalization, planning, implementation, and delivery. - -Each host adapter deliberately shares its name with the internal action skill -for the same operation. Their locations distinguish the host integration from -the layered policy. `al-code-review` remains distinct from BC-ALAgents' -separately installed `al-review` skill, avoiding a collision in hosts that use -one shared skill inventory. References from adapters to Entry, and from a -dispatched super-skill to its leaves, are intentional progressive disclosure. -This avoids registering every internal protocol file as an ambient host skill -while allowing each review domain to run in an isolated context. + +The host adapter and internal coordinator deliberately share the +`al-code-review` name because they represent the same user-facing operation in +their respective formats. Their locations distinguish their roles. The +reference from the adapter to Entry, and from a dispatched super-skill to its +leaf skills, is intentional progressive disclosure. It avoids registering +every internal BCQuality protocol file as an ambient host skill while allowing +each review domain to run in an isolated context. These contracts are stable. Changes require a PR approved by both maintainers. diff --git a/skills/al-development-plan/SKILL.md b/skills/al-development-plan/SKILL.md deleted file mode 100644 index 04044913..00000000 --- a/skills/al-development-plan/SKILL.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: al-development-plan -description: Enrich an existing Business Central AL development plan with read-only BCQuality knowledge constraints. Does not generate a plan or implement code. ---- - -# AL development plan guidance - -This host-native adapter translates an existing plan and repository into Entry's task context. It does not plan new work, edit the target repository, run an implementation or review/fix loop, stage, commit, or publish changes. - -## Execute - -1. Resolve `PLUGIN_ROOT` to the directory containing this plugin's root `plugin.json`, two levels above this file. -2. Preserve the caller's existing `development-plan` verbatim. Consumer-specific workflow payloads must be normalized by the consumer; do not interpret workflow state or manufacture a plan from a coding request. -3. Build the task context for `PLUGIN_ROOT/skills/entry.md`: - - Set `goal` to read-only BCQuality knowledge enrichment of the supplied plan, preserving the caller's intended change. - - List only actually supplied inputs from `[development-plan, repository]` in `inputs-available`. - - Set `technologies: [al]` only when established, and pass other applicability dimensions only when supplied or reliably determined. - - Apply `BCQUALITY_ENABLED_LAYERS` and `BCQUALITY_DISABLED_SKILLS` as described in the `al-code-review` adapter. -4. Read and execute Entry, including Preparation. Resolve its paths against `PLUGIN_ROOT`, not the target repository. Keep all generated index and scratch artifacts outside the target repository. Use READ's path-based fallback if index generation is unavailable; an unreadable corpus is a failure, not empty knowledge. -5. Follow Entry's dispatch, checking its output metadata against the referenced skill before invocation. This operation accepts only `development-guidance-report`; return `failed` rather than execute another output kind. Pass the supplied existing plan and readable repository, and return the report unchanged. Return Entry's `no-match` or `failed` record unchanged when nothing is dispatched. - -Missing inputs remain missing; the dispatched action skill returns `not-applicable` when it cannot proceed. A `no-knowledge` report is additive: it means no additional BCQuality constraints, not a refusal to let the consumer implement under its own gates. The consumer retains all implementation and delivery ownership. diff --git a/skills/do.md b/skills/do.md index cc4ff362..3d534ecf 100644 --- a/skills/do.md +++ b/skills/do.md @@ -73,6 +73,11 @@ proceed with the supplied subset MUST return `outcome: "not-applicable"`. - `findings-report` — evaluates an input and reports defects or observations. - `development-guidance-report` — selects and summarizes applicable BCQuality knowledge for an existing development plan without changing the target repository. +`development-plan` and `development-guidance-report` are provisional contract +extensions. They are intentionally not exposed by the standalone plugin in this +release. Consumer-owner agreement and pilot evidence are required before they +are treated as frozen public integration surfaces. + `file-path` is one file. `folder-path` is a directory whose recursively contained files form the complete current-state input, such as a Business Central app folder containing `app.json` and AL source. The input value is the @@ -106,6 +111,14 @@ Every action skill MUST contain these five sections, in order: **Action.** Execute the skill's work against the worklist. Evaluate each item in the worklist against the task input and emit findings. The action step is where skill behavior differs; the preceding three steps are uniform. +Review and plan enrichment use different Worklist signals by design. Review +leaves inspect existing source and use domain-specific code tokens to decide +which rules can produce findings. Plan enrichment precedes implementation and +selects cross-domain constraints from plan vocabulary and confirmed repository +symbols. Both use the same knowledge index, applicability semantics, layer +precedence, and full normative article bodies; review-leaf cue lists are not a +second knowledge registry. + ## Findings-report contract @@ -345,6 +358,11 @@ Severity taxonomy: An action skill with `outputs: [development-guidance-report]` emits one JSON document: +The provisional machine-readable structural schema is +[`schemas/development-guidance-report.schema.json`](../schemas/development-guidance-report.schema.json). +The semantic rules below remain authoritative for count arithmetic, exact +reference existence, applicability, and normative constraint fidelity. + ```json { "skill": { "id": "string", "version": 1 }, @@ -389,12 +407,12 @@ An action skill with `outputs: [development-guidance-report]` emits one JSON doc } ``` -The skill is read-only with respect to the target repository: no edits, generated files, staging, commits, or publication. Keep index, report, and scratch artifacts outside that repository. The report is strict JSON with no surrounding commentary. The caller supplies an existing plan and repository; consumer-specific input normalization and workflow state are outside this contract. +The skill is read-only with respect to the target repository: no edits, generated files, staging, commits, or publication. Keep index, report, and scratch artifacts outside that repository. The report is strict JSON with no surrounding commentary. The caller supplies an existing plan and may supply a readable repository; consumer-specific input normalization and workflow state are outside this contract. ### Guidance outcome semantics - `completed` — evaluation finished, at least one article was selected, every selected article was opened and faithfully converted into constraints, and no materially unresolved conditional guidance remains. -- `not-applicable` — the required existing plan or readable repository is absent, or the task is outside the skill's applicability. No constraints are claimed. +- `not-applicable` — the required existing plan is absent, does not identify the intended change, or is outside the skill's applicability. No constraints are claimed. - `no-knowledge` — evaluation finished and there are **no additional applicable BCQuality constraints** for this plan. `knowledge` is empty. This is not a statement that the work is unsafe or unimplementable; the consuming workflow can proceed under its ordinary gates. Do not add generic or filler articles to avoid this outcome. - `partial` — evaluation is incomplete or conditional guidance remains materially unresolved. Name each gap in `outcome-reason` and `unresolved`; do not silently treat an unknown dimension as a match. - `failed` — retrieval, reference integrity, or another error prevents a reliable report. Set `outcome-reason`; consumers must not treat the result as reliable constraints or as `no-knowledge`. @@ -409,7 +427,7 @@ The skill is read-only with respect to the target repository: no edits, generate `validation-considerations` states evidence the implementation workflow should obtain; it does not claim that a command or test has run. `suppressed` has the same shape and semantics as in a findings-report. `unresolved` records missing repository context or plan decisions that prevent a reliable constraint. Unknown applicability dimensions must appear in both `context.unknown` and a relevant unresolved entry, explaining whether they materially affect a candidate. An unknown dimension is not itself a failure or proof that relevant knowledge exists. -Reference SHAs, when present, identify the files read; they do not prove runtime pinning on their own. The consumer records and verifies the actual immutable BCQuality checkout used for both enrichment and final review, plus its filtering policy and run provenance outside the target repository. See [agent-consumption.md](../agent-consumption.md). +Reference SHAs, when present, identify the files read; they do not prove runtime pinning on their own. The consumer records and verifies the actual immutable BCQuality checkout used for both enrichment and final review, plus its filtering policy and run provenance outside the target repository. See [agent-consumption.md](../docs/agent-consumption.md). ## Composition (super-skills) diff --git a/skills/entry.md b/skills/entry.md index a1f13a98..e7c76131 100644 --- a/skills/entry.md +++ b/skills/entry.md @@ -36,6 +36,11 @@ task-context: `goal` and `inputs-available` are required. Filter dimensions (`technologies`, `bc-version`, `countries`, `application-area`) are optional; omitting a dimension is equivalent to "unconstrained" — see Relevance for the exact matching rule. `enabled-layers` defaults to all three. `disabled-skills` defaults to empty. +`development-plan` and the corresponding `development-guidance-report` output +kind are provisional. They are available to explicit integrations for review +and pilot use, but are not registered as standalone plugin capabilities in +this release. + ## Preparation — knowledge index Before routing, ensure the knowledge index is current for the **live** clone. The dispatched skills read `knowledge-index.json` (by default at the clone root) at their Source step instead of opening every knowledge file — see READ's [Retrieval workflow](read.md). When a consumer prunes its clone to policy *before* the agent runs, the index MUST be built over the clone as it exists now, so it lists exactly the articles that survived pruning and never an article the consumer denied: diff --git a/tools/DevelopmentGuidance.Evidence.ps1 b/tools/DevelopmentGuidance.Evidence.ps1 deleted file mode 100644 index 03c2aa95..00000000 --- a/tools/DevelopmentGuidance.Evidence.ps1 +++ /dev/null @@ -1,499 +0,0 @@ -# Helpers for Test-DevelopmentGuidanceFixtures.ps1 and its deterministic regressions. -function Get-GuidanceDiagnostic { - param($ErrorRecord) - if ($ErrorRecord.Exception -is [Management.Automation.RuntimeException] -and - $ErrorRecord.FullyQualifiedErrorId -eq $ErrorRecord.Exception.Message) { - return $ErrorRecord.Exception.Message - } - return 'Evidence or report could not be read safely (missing, malformed, inaccessible or unsupported state).' -} - -function Assert-GuidanceObject { - param($Value, [string] $Label, [string[]] $Fields = @()) - if ($Value -isnot [Collections.IDictionary]) { throw "$Label must be a JSON object." } - foreach ($field in $Fields) { - if (-not $Value.Contains($field)) { throw "$Label is missing required field '$field'." } - } -} - -function Assert-GuidanceString { - param($Value, [string] $Label) - if ($Value -isnot [string] -or [string]::IsNullOrWhiteSpace($Value)) { throw "$Label must be a non-empty string." } -} - -function Assert-GuidanceArray { - param($Value, [string] $Label, [switch] $Strings, [switch] $NonEmpty) - if ($Value -isnot [array]) { throw "$Label must be a JSON array." } - if ($NonEmpty -and $Value.Count -eq 0) { throw "$Label must not be empty." } - if ($Strings) { foreach ($item in $Value) { Assert-GuidanceString $item "$Label entry" } } -} - -function Assert-GuidanceInteger { - param($Value, [string] $Label) - if (($Value -isnot [long] -and $Value -isnot [int] -and $Value -isnot [bigint]) -or $Value -lt 0) { - throw "$Label must be a non-negative integer." - } -} - -function Read-GuidanceJson { - param([string] $Path) - function Assert-JsonMembers($Element) { - if ($Element.ValueKind -eq [Text.Json.JsonValueKind]::Object) { - $names = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) - foreach ($property in $Element.EnumerateObject()) { - if (-not $names.Add($property.Name)) { throw 'JSON contains duplicate or case-ambiguous members.' } - Assert-JsonMembers $property.Value - } - } elseif ($Element.ValueKind -eq [Text.Json.JsonValueKind]::Array) { - foreach ($item in $Element.EnumerateArray()) { Assert-JsonMembers $item } - } - } - try { - if ((Get-Item -LiteralPath $Path -Force).Length -gt 32MB) { throw 'Oversized JSON.' } - $text = [IO.File]::ReadAllText($Path) - $document = [Text.Json.JsonDocument]::Parse($text) - try { Assert-JsonMembers $document.RootElement } finally { $document.Dispose() } - return ConvertFrom-Json -InputObject $text -AsHashtable -Depth 64 -NoEnumerate - } catch { throw 'Input is not readable, strict JSON with unique members.' } -} - -function Get-GuidanceHash { - param([string] $Path) - return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash -} - -function Get-GuidanceCaseId { - param([string] $Id) - $bytes = [Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($Id)) - return "case-$([Convert]::ToHexString($bytes).Substring(0, 8).ToLowerInvariant())" -} - -function Test-GuidanceWithin { - param([string] $Path, [string] $Parent) - $comparison = if ($IsWindows) { [StringComparison]::OrdinalIgnoreCase } else { [StringComparison]::Ordinal } - $prefix = $Parent.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar - return $Path.Equals($Parent, $comparison) -or $Path.StartsWith($prefix, $comparison) -} - -function Assert-GuidanceItem { - param($Item) - if (($Item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $Item.LinkType -or $Item.LinkTarget) { - throw 'Links, junctions, hard links and reparse points are not supported.' - } - if (-not $Item.PSIsContainer -and $IsWindows) { - $unsupportedStreams = @( - Get-Item -LiteralPath $Item.FullName -Stream '*' -Force -ErrorAction Stop | - Where-Object Stream -notin @(':$DATA', 'sec.endpointdlp') - ) - if ($unsupportedStreams.Count) { - throw 'Alternate data streams are not supported.' - } - } -} - -function Get-GuidanceSafePath { - param([string] $Path, [switch] $AllowMissing, [switch] $Directory, [switch] $File) - Assert-GuidanceString $Path 'Filesystem path' - $full = [IO.Path]::GetFullPath($Path) - $driveRoot = [IO.Path]::GetPathRoot($full) - if ($IsWindows -and ($driveRoot.StartsWith('\\') -or $full.Substring($driveRoot.Length).Contains(':'))) { - throw 'Network paths and alternate stream paths are not supported.' - } - $cursor = $driveRoot - $components = $full.Substring($driveRoot.Length).Split([IO.Path]::DirectorySeparatorChar, [StringSplitOptions]::RemoveEmptyEntries) - foreach ($component in $components) { - if ($component -match '[\. ]$') { throw 'Ambiguous filesystem path components are not supported.' } - $cursor = Join-Path $cursor $component - # Get-Item sees dangling links that Test-Path may treat as missing. - $item = Get-Item -LiteralPath $cursor -Force -ErrorAction SilentlyContinue - if ($null -ne $item) { Assert-GuidanceItem $item } - elseif (-not $AllowMissing) { throw 'Required filesystem path is missing.' } - } - if ($Directory -and -not (Test-Path -LiteralPath $full -PathType Container)) { throw 'Required directory is missing.' } - if ($File -and -not (Test-Path -LiteralPath $full -PathType Leaf)) { throw 'Required file is missing.' } - return $full.TrimEnd([IO.Path]::DirectorySeparatorChar) -} - -function Assert-GuidanceTree { - param([string] $Root) - $pending = [Collections.Generic.Stack[string]]::new() - $pending.Push($Root) - while ($pending.Count) { - foreach ($item in Get-ChildItem -LiteralPath $pending.Pop() -Force) { - Assert-GuidanceItem $item - if ($item.PSIsContainer) { $pending.Push($item.FullName) } - } - } -} - -function Invoke-GuidanceGit { - param([string] $Root, [string[]] $Arguments, [switch] $RawOutput) - $start = [Diagnostics.ProcessStartInfo]::new('git') - $start.UseShellExecute = $false - $start.RedirectStandardOutput = $true - $start.RedirectStandardError = $true - foreach ($variable in @('GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE', - 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CONFIG', - 'GIT_CONFIG_COUNT', 'GIT_CONFIG_PARAMETERS', 'GIT_NAMESPACE')) { - $null = $start.Environment.Remove($variable) - } - $start.Environment['GIT_CONFIG_NOSYSTEM'] = '1' - $start.Environment['GIT_CONFIG_GLOBAL'] = '' - $start.Environment['GIT_TERMINAL_PROMPT'] = '0' - # In particular, do not execute a repository-supplied fsmonitor hook on reads. - foreach ($argument in (@('--no-optional-locks', '-c', 'core.fsmonitor=false', '-C', $Root) + $Arguments)) { - $start.ArgumentList.Add($argument) - } - $process = [Diagnostics.Process]::Start($start) - try { - $output = $process.StandardOutput.ReadToEndAsync() - $errors = $process.StandardError.ReadToEndAsync() - $process.WaitForExit() - $null = $errors.GetAwaiter().GetResult() - if ($process.ExitCode -ne 0) { throw 'Git evidence could not be read.' } - $text = $output.GetAwaiter().GetResult() - if ($RawOutput) { return $text } - return $text.TrimEnd("`r", "`n") - } finally { $process.Dispose() } -} - -function Get-GuidanceSnapshot { - param([string] $Root, [switch] $Target) - $Root = Get-GuidanceSafePath $Root -Directory - $dotGit = Get-GuidanceSafePath (Join-Path $Root '.git') - if (Test-Path -LiteralPath $dotGit -PathType Container) { - $gitDir = $dotGit - } else { - if ($Target) { throw 'Target workspaces must be standalone repositories with internal Git storage.' } - $pointer = [IO.File]::ReadAllText($dotGit) - if ($pointer -notmatch '\Agitdir: ([^\r\n]+)\r?\n?\z') { throw 'Unsupported Git worktree pointer.' } - $gitPath = $Matches[1] - if (-not [IO.Path]::IsPathFullyQualified($gitPath)) { $gitPath = Join-Path $Root $gitPath } - $gitDir = Get-GuidanceSafePath $gitPath -Directory - } - $commonDir = $gitDir - $commonPointer = Get-GuidanceSafePath (Join-Path $gitDir 'commondir') -AllowMissing - if (Test-Path -LiteralPath $commonPointer) { - $commonPath = [IO.File]::ReadAllText($commonPointer).Trim() - if (-not [IO.Path]::IsPathFullyQualified($commonPath)) { $commonPath = Join-Path $gitDir $commonPath } - $commonDir = Get-GuidanceSafePath $commonPath -Directory - } - if ($Target -and ($gitDir -cne (Join-Path $Root '.git') -or $commonDir -cne $gitDir)) { - throw 'Target workspaces must be standalone repositories with internal Git storage.' - } - $files = [Collections.Generic.List[object]]::new() - $pending = [Collections.Generic.Stack[string]]::new() - $pending.Push($Root) - while ($pending.Count) { - foreach ($item in @(Get-ChildItem -LiteralPath $pending.Pop() -Force | Sort-Object Name -CaseSensitive)) { - Assert-GuidanceItem $item - $relative = [IO.Path]::GetRelativePath($Root, $item.FullName).Replace('\', '/') - $entry = [ordered]@{ - path = $relative - kind = if ($item.PSIsContainer) { 'directory' } else { 'file' } - attributes = [int]$item.Attributes - unixMode = [int]$item.UnixFileMode - creationUtcTicks = $item.CreationTimeUtc.Ticks - } - if ($item.PSIsContainer) { - $pending.Push($item.FullName) - } else { - $entry.length = $item.Length - $entry.lastWriteUtcTicks = $item.LastWriteTimeUtc.Ticks - $entry.sha256 = Get-GuidanceHash $item.FullName - } - $files.Add($entry) - } - } - # Linked knowledge worktrees have explicitly identified Git metadata outside - # the content root. Record its meaningful configuration as well as HEAD/refs. - $gitMetadata = [ordered]@{} - foreach ($storage in @($gitDir, $commonDir) | Sort-Object -Unique) { - foreach ($name in @('HEAD', 'commondir', 'config', 'config.worktree', 'packed-refs', 'refs', 'objects', 'index', 'info\exclude', 'shallow')) { - $path = Get-GuidanceSafePath (Join-Path $storage $name) -AllowMissing - if ((Test-Path -LiteralPath $path -PathType Container) -and -not (Test-GuidanceWithin $path $Root)) { - Assert-GuidanceTree $path - } - if (Test-Path -LiteralPath $path -PathType Leaf) { - $gitMetadata[$path] = Get-GuidanceHash $path - } - if ($name -in @('config', 'config.worktree') -and (Test-Path -LiteralPath $path) -and - [IO.File]::ReadAllText($path) -match '(?im)^\s*\[include(?:If)?\b') { - throw 'External Git configuration includes are not supported.' - } - } - foreach ($name in @('objects\info\alternates', 'objects\info\http-alternates')) { - if (Test-Path -LiteralPath (Join-Path $storage $name)) { throw 'External Git object stores are not supported.' } - } - } - $top = Get-GuidanceSafePath (Invoke-GuidanceGit $Root @('rev-parse', '--show-toplevel')) -Directory - if ($top -cne $Root) { throw 'Evidence requires the exact Git worktree root, not a subdirectory.' } - $indexPath = Get-GuidanceSafePath (Join-Path $gitDir 'index') -AllowMissing - $tracked = Invoke-GuidanceGit $Root @('ls-files', '--stage') - if ($tracked -match '(?m)^160000 ') { throw 'Submodule workspaces are not supported.' } - if ((Invoke-GuidanceGit $Root @('ls-files', '-t')) -match '(?m)^S ') { throw 'Sparse workspaces are not supported.' } - $head = Invoke-GuidanceGit $Root @('rev-parse', '--verify', 'HEAD') - $headFile = Get-GuidanceSafePath (Join-Path $gitDir 'HEAD') -File - # Access times, Git status refreshes, and index-builder generatedAt are not evidence. - return [ordered]@{ - version = 1 - root = $Root - rootCreationUtcTicks = (Get-Item -LiteralPath $Root -Force).CreationTimeUtc.Ticks - gitDir = $gitDir - commonDir = $commonDir - gitMetadata = $gitMetadata - head = $head - headFileSha256 = Get-GuidanceHash $headFile - references = Invoke-GuidanceGit $Root @('for-each-ref', '--format=%(refname) %(objectname) %(symref)') - indexPath = $indexPath - indexSha256 = if (Test-Path -LiteralPath $indexPath) { Get-GuidanceHash $indexPath } else { $null } - files = @($files | Sort-Object { $_.path } -CaseSensitive) - } -} - -function Test-GuidanceSnapshotEqual { - param($Before, $After) - return ($Before | ConvertTo-Json -Depth 64 -Compress) -ceq ($After | ConvertTo-Json -Depth 64 -Compress) -} - -function Write-GuidanceNewJson { - param([string] $Path, $Value) - $parent = Split-Path -Parent $Path - [IO.Directory]::CreateDirectory($parent) | Out-Null - $bytes = [Text.Encoding]::UTF8.GetBytes(($Value | ConvertTo-Json -Depth 64)) - $stream = [IO.File]::Open($Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) - try { $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Dispose() } -} - -function Resolve-GuidanceReference { - param([string] $Root, $Reference, [switch] $Knowledge, [string] $Article) - Assert-GuidanceString $Reference 'Reference path' - if ($Reference -cnotmatch '^[a-zA-Z0-9_-]+(?:/[a-zA-Z0-9_.-]+)+$' -or - @($Reference.Split('/') | Where-Object { $_ -in @('.', '..') -or $_ -match '[\. ]$' }).Count) { - throw 'Reference must be an unambiguous forward-slash repository-relative path without traversal.' - } - if ($Knowledge -and $Reference -cnotmatch '^(microsoft|community|custom)/knowledge/[a-z0-9-]+/(?:[a-z0-9-]+/)*[a-z0-9-]+\.md$') { - throw 'Knowledge references must identify actual layered knowledge articles.' - } - if ($Article) { - $stem = $Article.Substring(0, $Article.Length - 3) - if ($Reference -cnotmatch ('^' + [regex]::Escape($stem) + '\.(good|bad)\.[a-zA-Z0-9]+$')) { - throw 'Sample references must be good/bad siblings of their knowledge article.' - } - } - $cursor = $Root - foreach ($component in $Reference.Split('/')) { - $cursor = Join-Path $cursor $component - $cursor = Get-GuidanceSafePath $cursor - if ((Get-Item -LiteralPath $cursor -Force).Name -cne $component) { throw 'Reference path casing must match the actual file.' } - } - if (-not (Test-GuidanceWithin $cursor $Root) -or -not (Test-Path -LiteralPath $cursor -PathType Leaf)) { - throw 'Reference must resolve to an existing file inside the knowledge checkout.' - } - if ($Knowledge) { - $text = [IO.File]::ReadAllText($cursor) - if ($text -notmatch '(?s)^---\r?\n.*?\r?\n---' -or - $text -notmatch '(?m)^domain:\s*\S+' -or $text -notmatch '(?m)^## (Best Practice|Anti Pattern)\s*$') { - throw 'Knowledge reference does not contain a normative knowledge article.' - } - } - return $cursor -} - -function Get-GuidancePlanRequest { - param($Plan) - if ($Plan -is [string]) { Assert-GuidanceString $Plan 'development-plan'; return $Plan } - Assert-GuidanceObject $Plan 'development-plan' @('request') - Assert-GuidanceString $Plan.request 'development-plan.request' - return $Plan.request -} - -function Assert-GuidanceContext { - param($Context) - Assert-GuidanceObject $Context 'context' @('bc-version', 'technologies', 'countries', 'application-area', 'unknown') - Assert-GuidanceString $Context.'bc-version' 'context.bc-version' - foreach ($key in @('technologies', 'countries', 'application-area', 'unknown')) { - Assert-GuidanceArray $Context[$key] "context.$key" -Strings - if (@($Context[$key] | Sort-Object -Unique).Count -ne $Context[$key].Count) { throw "context.$key contains duplicates." } - } - foreach ($key in $Context.unknown) { - if ($key -cnotin @('bc-version', 'technologies', 'countries', 'application-area')) { throw 'context.unknown contains an invalid dimension.' } - } - if ($Context.'bc-version' -eq 'unknown' -and 'bc-version' -cnotin $Context.unknown) { - throw 'Unknown BC version must be recorded in context.unknown.' - } - foreach ($key in @('technologies', 'countries', 'application-area')) { - if ((-not $Context[$key].Count -or 'unknown' -in $Context[$key]) -and $key -cnotin $Context.unknown) { - throw 'Unavailable applicability dimensions must be recorded in context.unknown.' - } - } -} - -function Assert-GuidanceManifest { - param($Manifest, [string] $Root) - Assert-GuidanceObject $Manifest 'manifest' @('version', 'minimumKnowledgeRecall', 'minimumKnowledgePrecision', 'skill', 'cases') - if ($Manifest.version -ne 1) { throw 'Unsupported guidance fixture manifest version.' } - foreach ($name in @('minimumKnowledgeRecall', 'minimumKnowledgePrecision')) { - $value = $Manifest[$name] - if (($value -isnot [double] -and $value -isnot [long] -and $value -isnot [int] -and $value -isnot [decimal]) -or - $value -lt 0 -or $value -gt 1) { throw 'Manifest thresholds must be numbers between zero and one.' } - } - $null = Resolve-GuidanceReference $Root $Manifest.skill - Assert-GuidanceArray $Manifest.cases 'manifest.cases' -NonEmpty - $ids = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) - $modelIds = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) - foreach ($case in $Manifest.cases) { - Assert-GuidanceObject $case 'manifest case' @('id', 'expectedKind', 'expectedOutcome', 'development-plan', 'context', 'requiredKnowledge', 'optionalKnowledge', 'expectedUnknown', 'requiresUnresolved', 'requiresMaterialUnresolved') - if ($case.id -isnot [string] -or $case.id -cnotmatch '^[a-z0-9]+(?:-[a-z0-9]+)*$') { throw 'Fixture id must be kebab-case.' } - if (-not $ids.Add($case.id) -or -not $modelIds.Add((Get-GuidanceCaseId $case.id))) { throw 'Duplicate fixture or model case identity.' } - if ($case.expectedKind -cnotin @('feature', 'bug', 'refactor', 'upgrade', 'maintenance')) { throw 'Fixture expectedKind is invalid.' } - if ($case.expectedOutcome -cnotin @('completed', 'not-applicable', 'no-knowledge', 'partial', 'failed')) { throw 'Fixture expectedOutcome is invalid.' } - Assert-GuidanceString $case.expectedKind 'fixture.expectedKind' - Assert-GuidanceString $case.expectedOutcome 'fixture.expectedOutcome' - $null = Get-GuidancePlanRequest $case.'development-plan' - Assert-GuidanceContext $case.context - foreach ($name in @('requiredKnowledge', 'optionalKnowledge', 'expectedUnknown')) { - Assert-GuidanceArray $case[$name] "fixture.$name" -Strings - } - if ($case.requiresUnresolved -isnot [bool]) { throw 'Fixture requiresUnresolved must be boolean.' } - if ($case.requiresMaterialUnresolved -isnot [bool]) { throw 'Fixture requiresMaterialUnresolved must be boolean.' } - if ($case.requiresMaterialUnresolved -and ($case.expectedOutcome -ne 'partial' -or -not $case.requiresUnresolved)) { - throw 'Materially unresolved fixtures must expect partial and unresolved evidence.' - } - foreach ($key in $case.expectedUnknown) { - if ($key -cnotin @('bc-version', 'technologies', 'countries', 'application-area')) { throw 'Fixture expectedUnknown is invalid.' } - } - $references = @($case.requiredKnowledge) + @($case.optionalKnowledge) - if (@($references | Sort-Object -Unique).Count -ne $references.Count) { throw 'Fixture knowledge references contain duplicates.' } - foreach ($reference in $references) { $null = Resolve-GuidanceReference $Root $reference -Knowledge } - if ($case.expectedOutcome -in @('no-knowledge', 'not-applicable') -and $references.Count) { - throw 'Empty-knowledge outcomes cannot require or accept knowledge.' - } - } -} - -function Assert-GuidanceResult { - param($Result, $Case, $Manifest, [string] $Root, [string] $Workspace) - Assert-GuidanceObject $Result 'result' @('caseId', 'guidanceReport') - Assert-GuidanceString $Result.caseId 'result.caseId' - if ($Result.caseId -cne (Get-GuidanceCaseId $Case.id)) { throw 'Result caseId mismatch.' } - if ($Result.Contains('workspaceRoot')) { - Assert-GuidanceString $Result.workspaceRoot 'result.workspaceRoot' - if (-not [IO.Path]::IsPathFullyQualified($Result.workspaceRoot) -or - (Get-GuidanceSafePath $Result.workspaceRoot -Directory) -cne $Workspace) { - throw 'Result workspaceRoot disagrees with the runner binding.' - } - } - $report = $Result.guidanceReport - Assert-GuidanceObject $report 'guidanceReport' @('skill', 'outcome', 'summary', 'context', 'knowledge', 'validation-considerations', 'suppressed', 'unresolved') - Assert-GuidanceObject $report.skill 'skill' @('id', 'version') - Assert-GuidanceString $report.skill.id 'skill.id' - if ($report.skill.id -cne 'al-development-plan' -or $report.skill.version -isnot [long] -or $report.skill.version -ne 1) { - throw 'Report skill identity/version is invalid.' - } - Assert-GuidanceString $report.outcome 'outcome' - if ($report.outcome -cnotin @('completed', 'not-applicable', 'no-knowledge', 'partial', 'failed')) { throw 'Report outcome enum is invalid.' } - if ($report.outcome -cne $Case.expectedOutcome) { throw 'Report outcome does not match fixture expectedOutcome.' } - if ($report.outcome -in @('partial', 'failed') -or $report.Contains('outcome-reason')) { - Assert-GuidanceString $report['outcome-reason'] 'outcome-reason' - } - Assert-GuidanceObject $report.summary 'summary' @('request', 'kind', 'candidates', 'selected') - Assert-GuidanceString $report.summary.request 'summary.request' - Assert-GuidanceString $report.summary.kind 'summary.kind' - if ($report.summary.kind -cne $Case.expectedKind) { throw 'summary.kind does not match the intended change.' } - Assert-GuidanceInteger $report.summary.candidates 'summary.candidates' - Assert-GuidanceInteger $report.summary.selected 'summary.selected' - Assert-GuidanceContext $report.context - foreach ($name in @('knowledge', 'validation-considerations', 'suppressed', 'unresolved')) { - Assert-GuidanceArray $report[$name] $name - } - Assert-GuidanceArray $report.unresolved 'unresolved' -Strings - if ($report.summary.selected -ne $report.knowledge.Count -or $report.summary.selected -gt $report.summary.candidates) { - throw 'Summary counts disagree with selected knowledge/candidates.' - } - if ($report.outcome -in @('no-knowledge', 'not-applicable') -and $report.knowledge.Count) { throw 'This outcome requires empty knowledge.' } - if ($report.outcome -eq 'completed' -and -not $report.knowledge.Count) { throw 'Completed requires selected knowledge; empty evaluation is no-knowledge.' } - if (($report.outcome -eq 'partial' -or $Case.requiresUnresolved) -and -not $report.unresolved.Count) { - throw 'Partial/incomplete evaluation must explain unresolved gaps.' - } - foreach ($dimension in $Case.expectedUnknown) { - if ($dimension -cnotin $report.context.unknown) { throw 'Expected unknown context was silently resolved.' } - } - foreach ($dimension in $report.context.unknown) { - if (-not @($report.unresolved | Where-Object { $_ -match [regex]::Escape($dimension) }).Count) { - throw 'Unknown dimensions require a corresponding unresolved explanation.' - } - } - # Free-form unresolved text has no machine-readable materiality field in DO. - # Known material fixture conditions are runner expectations, not model claims. - if ($Case.requiresMaterialUnresolved -and $report.outcome -ne 'partial') { throw 'Material unknown guidance must remain partial.' } - $used = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) - foreach ($entry in $report.knowledge) { - Assert-GuidanceObject $entry 'knowledge entry' @('path', 'used-for', 'constraints', 'sample-paths') - $null = Resolve-GuidanceReference $Root $entry.path -Knowledge - if (-not $used.Add($entry.path)) { throw 'Duplicate knowledge reference.' } - Assert-GuidanceString $entry.'used-for' 'knowledge.used-for' - Assert-GuidanceArray $entry.constraints 'knowledge.constraints' -Strings -NonEmpty - Assert-GuidanceArray $entry.'sample-paths' 'knowledge.sample-paths' -Strings - if (@($entry.'sample-paths' | Sort-Object -Unique).Count -ne $entry.'sample-paths'.Count) { throw 'Duplicate sample reference.' } - foreach ($sample in $entry.'sample-paths') { $null = Resolve-GuidanceReference $Root $sample -Article $entry.path } - Assert-GuidanceReferenceSha $entry $Root - } - $validationIds = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) - foreach ($entry in $report.'validation-considerations') { - Assert-GuidanceObject $entry 'validation consideration' @('id', 'reason', 'evidence') - foreach ($key in @('id', 'reason', 'evidence')) { Assert-GuidanceString $entry[$key] "validation-considerations.$key" } - if (-not $validationIds.Add($entry.id)) { throw 'Duplicate validation consideration id.' } - } - $suppressed = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) - foreach ($entry in $report.suppressed) { - Assert-GuidanceObject $entry 'suppressed entry' @('reference', 'reason') - Assert-GuidanceObject $entry.reference 'suppressed.reference' @('path') - $null = Resolve-GuidanceReference $Root $entry.reference.path -Knowledge - Assert-GuidanceReferenceSha $entry.reference $Root - Assert-GuidanceString $entry.reason 'suppression reason' - if ($entry.reason -cnotin @('layer-precedence', 'configuration')) { throw 'Suppression reason is invalid.' } - if (-not $suppressed.Add($entry.reference.path) -or $used.Contains($entry.reference.path)) { throw 'Duplicate or selected suppressed reference.' } - } - $matched = @($Case.requiredKnowledge | Where-Object { $used.Contains($_) }).Count - $recall = if ($Case.requiredKnowledge.Count) { $matched / $Case.requiredKnowledge.Count } else { 1.0 } - $accepted = @($Case.requiredKnowledge) + @($Case.optionalKnowledge) - $acceptedCount = @($used | Where-Object { $_ -cin $accepted }).Count - $precision = if ($used.Count) { $acceptedCount / $used.Count } elseif (-not $Case.requiredKnowledge.Count) { 1.0 } else { 0.0 } - if ($recall -lt $Manifest.minimumKnowledgeRecall) { throw 'Knowledge recall is below the manifest threshold.' } - if ($precision -lt $Manifest.minimumKnowledgePrecision) { throw 'Knowledge precision is below the manifest threshold.' } -} - -function Assert-GuidanceReferenceSha { - param($Entry, [string] $Root) - if ($Entry.Contains('sha')) { - if ($Entry.sha -isnot [string] -or $Entry.sha -cnotmatch '^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$') { - throw 'Reference SHA must be a full commit object id.' - } - $head = Invoke-GuidanceGit $Root @('rev-parse', '--verify', 'HEAD') - if ($Entry.sha -ne $head) { throw 'Reference SHA does not identify the recorded live checkout.' } - $committed = Invoke-GuidanceGit $Root @('cat-file', 'blob', "$head`:$($Entry.path)") -RawOutput - $live = [IO.File]::ReadAllText((Resolve-GuidanceReference $Root $Entry.path -Knowledge)) - if ($committed.Replace("`r`n", "`n") -cne $live.Replace("`r`n", "`n")) { - throw 'Reference SHA content differs from the live knowledge article.' - } - } -} - -function Get-GuidanceResultSchema { - param([string] $CaseId) - return [ordered]@{ - caseId = $CaseId - guidanceReport = [ordered]@{ - skill = [ordered]@{ id = 'al-development-plan'; version = 1 } - outcome = 'completed | not-applicable | no-knowledge | partial | failed' - 'outcome-reason' = 'required for partial or failed' - summary = [ordered]@{ request = 'planned intent'; kind = 'feature | bug | refactor | upgrade | maintenance'; candidates = 0; selected = 0 } - context = [ordered]@{ 'bc-version' = 'resolved target or unknown'; technologies = @('al'); countries = @('w1'); 'application-area' = @('all'); unknown = @() } - knowledge = @([ordered]@{ path = 'repo-relative knowledge article'; sha = 'optional full checkout commit id'; 'used-for' = 'plan decision'; constraints = @('faithful normative constraint'); 'sample-paths' = @() }) - 'validation-considerations' = @([ordered]@{ id = 'stable id'; reason = 'why needed'; evidence = 'evidence implementation should obtain' }) - suppressed = @([ordered]@{ reference = [ordered]@{ path = 'suppressed knowledge path' }; reason = 'layer-precedence | configuration' }) - unresolved = @('Missing context/decision, affected candidate, and materiality; name unknown dimensions exactly.') - } - } -} diff --git a/tools/Test-DevelopmentGuidanceEvaluator.ps1 b/tools/Test-DevelopmentGuidanceEvaluator.ps1 deleted file mode 100644 index 2fb1769f..00000000 --- a/tools/Test-DevelopmentGuidanceEvaluator.ps1 +++ /dev/null @@ -1,452 +0,0 @@ -<# -.SYNOPSIS - Deterministic, offline regressions for the guidance evaluator (no model/AL run). -.DESCRIPTION - Creates only a uniquely named .guidance-evaluator-regression-* directory below - the current checkout, with standalone Git repositories and sibling runner - artifacts. Removes that exact directory in finally; never uses the OS temp - directory or cleans a caller-provided repository. -#> -[CmdletBinding()] -param() - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' -. (Join-Path $PSScriptRoot 'DevelopmentGuidance.Evidence.ps1') -$evaluator = Join-Path $PSScriptRoot 'Test-DevelopmentGuidanceFixtures.ps1' -$sourceRoot = (Get-Item -LiteralPath (Join-Path $PSScriptRoot '..')).FullName -$scratch = Join-Path $sourceRoot ".guidance-evaluator-regression-$([guid]::NewGuid().ToString('N'))" -$root = Join-Path $scratch 'knowledge-checkout' -$article = 'microsoft/knowledge/performance/pair-findset-with-next-loop.md' -$otherArticle = 'microsoft/knowledge/performance/findset-true-applies-updlock-on-read.md' -$sample = 'microsoft/knowledge/performance/pair-findset-with-next-loop.good.al' -$tests = [Collections.Generic.List[string]]::new() -$script:scenarioNumber = 0 -$script:reportScenario = $null - -function Set-TestJson($Path, $Value) { - [IO.File]::WriteAllText($Path, ($Value | ConvertTo-Json -Depth 64)) -} - -function Invoke-TestGit([string] $Directory, [string[]] $Arguments) { - $output = @(& git --no-optional-locks -C $Directory @Arguments 2>&1) - if ($LASTEXITCODE -ne 0) { throw 'Regression Git setup failed.' } -} - -function Initialize-TestRepository([string] $Directory) { - [IO.Directory]::CreateDirectory($Directory) | Out-Null - Invoke-TestGit $Directory @('init', '--quiet') - Invoke-TestGit $Directory @('config', 'user.email', 'guidance-fixture@example.invalid') - Invoke-TestGit $Directory @('config', 'user.name', 'Guidance fixture') - Invoke-TestGit $Directory @('config', 'commit.gpgSign', 'false') - Invoke-TestGit $Directory @('config', 'core.autocrlf', 'false') - [IO.File]::WriteAllText((Join-Path $Directory 'app.json'), '{"name":"Synthetic AL fixture","application":"28.0.0.0"}') - [IO.File]::WriteAllText((Join-Path $Directory 'tracked.al'), 'codeunit 50100 Example {}') - [IO.File]::WriteAllText((Join-Path $Directory '.gitignore'), "ignored.txt`nignored-directory/`n") - Invoke-TestGit $Directory @('add', '.') - Invoke-TestGit $Directory @('commit', '--quiet', '-m', 'Synthetic fixture baseline') -} - -function Invoke-EvaluatorTest([string] $Name, [string[]] $Arguments, [bool] $ShouldPass, [string] $Diagnostic = '') { - $output = @(& pwsh -NoProfile -File $evaluator @Arguments 2>&1) -join "`n" - $code = $LASTEXITCODE - if (($code -eq 0) -ne $ShouldPass -or $output -notmatch $(if ($ShouldPass) { 'PASSED|captured' } else { 'FAILED' })) { - throw "Regression '$Name' unexpected exit $code. $output" - } - if ($Diagnostic -and $output -notmatch [regex]::Escape($Diagnostic)) { - throw "Regression '$Name' missing diagnostic '$Diagnostic'. $output" - } - if ($output -match 'MODEL_SECRET_SENTINEL') { throw "Regression '$Name' leaked model content." } - $tests.Add($Name) -} - -function New-TestScenario([string] $Outcome = 'completed', [switch] $Unknown, [switch] $SecondCase) { - $script:scenarioNumber++ - $directory = Join-Path $scratch "scenario-$script:scenarioNumber" - [IO.Directory]::CreateDirectory($directory) | Out-Null - $workspace = Join-Path $directory 'target' - Initialize-TestRepository $workspace - [IO.File]::WriteAllText((Join-Path $workspace 'untracked.txt'), 'existing untracked content') - [IO.File]::WriteAllText((Join-Path $workspace 'ignored.txt'), 'existing ignored content') - [IO.Directory]::CreateDirectory((Join-Path $workspace 'ignored-directory')) | Out-Null - [IO.File]::WriteAllText((Join-Path $workspace 'ignored-directory\child.txt'), 'ignored child') - $results = Join-Path $directory 'results' - [IO.Directory]::CreateDirectory($results) | Out-Null - $hasKnowledge = $Outcome -in @('completed', 'partial') - $case = [ordered]@{ - id = 'synthetic-case' - expectedKind = 'bug' - expectedOutcome = $Outcome - expectedUnknown = @($(if ($Unknown) { 'bc-version' })) - requiresUnresolved = ($Outcome -eq 'partial' -or $Unknown.IsPresent) - requiresMaterialUnresolved = ($Outcome -eq 'partial') - 'development-plan' = [ordered]@{ kind = 'bug'; request = 'Iterate the supplied filtered record set.' } - context = [ordered]@{ - 'bc-version' = $(if ($Unknown) { 'unknown' } else { '28' }) - technologies = @('al') - countries = @('w1') - 'application-area' = @('all') - unknown = @($(if ($Unknown) { 'bc-version' })) - } - requiredKnowledge = @($(if ($hasKnowledge) { $article })) - optionalKnowledge = @() - } - $manifest = [ordered]@{ - version = 1 - skill = 'microsoft/skills/development/al-development-plan.md' - minimumKnowledgeRecall = 1.0 - minimumKnowledgePrecision = 1.0 - cases = @($case) - } - $result = [ordered]@{ - caseId = Get-GuidanceCaseId $case.id - guidanceReport = [ordered]@{ - skill = [ordered]@{ id = 'al-development-plan'; version = 1 } - outcome = $Outcome - summary = [ordered]@{ request = 'Iterate all selected records.'; kind = 'bug'; candidates = [int]$hasKnowledge; selected = [int]$hasKnowledge } - context = $case.context - knowledge = @($(if ($hasKnowledge) { - [ordered]@{ path = $article; 'used-for' = 'Choose the multi-record reader.'; constraints = @('Use FindSet when iterating with Next.'); 'sample-paths' = @($sample) } - })) - 'validation-considerations' = @([ordered]@{ id = 'all-selected'; reason = 'Preserve selection.'; evidence = 'Test all selected records and an excluded record.' }) - suppressed = @() - unresolved = @($(if ($Outcome -eq 'partial') { - if ($Unknown) { 'bc-version is unknown and materially affects the candidate; clarify before completing guidance.' } - else { 'The caller cardinality decision remains materially unresolved.' } - } elseif ($Unknown) { 'bc-version is unknown but immaterial: selected loop guidance applies to all versions.' })) - } - } - if ($Outcome -in @('partial', 'failed')) { $result.guidanceReport.'outcome-reason' = 'Fixture intentionally leaves evaluation incomplete.' } - $manifestPath = Join-Path $directory 'manifest.json' - $mapPath = Join-Path $directory 'workspace-map.json' - $map = [ordered]@{ 'synthetic-case' = $workspace } - if ($SecondCase) { - $second = $case | ConvertTo-Json -Depth 64 | ConvertFrom-Json -AsHashtable - $second.id = 'second-case' - $manifest.cases += $second - $secondWorkspace = Join-Path $directory 'second-target' - Initialize-TestRepository $secondWorkspace - $map[$second.id] = $secondWorkspace - $secondResult = $result | ConvertTo-Json -Depth 64 | ConvertFrom-Json -AsHashtable - $secondResult.caseId = Get-GuidanceCaseId $second.id - Set-TestJson (Join-Path $results "result-$($secondResult.caseId).json") $secondResult - } - Set-TestJson $manifestPath $manifest - Set-TestJson $mapPath $map - $resultPath = Join-Path $results "result-$($result.caseId).json" - Set-TestJson $resultPath $result - $baselinePath = Join-Path $directory 'baseline.json' - $captureArgs = @('-Root', $root, '-ManifestPath', $manifestPath, '-CaptureBaseline', '-WorkspaceMapPath', $mapPath, '-BaselinePath', $baselinePath) - $output = @(& pwsh -NoProfile -File $evaluator @captureArgs 2>&1) -join "`n" - if ($LASTEXITCODE -ne 0) { throw "Baseline setup failed. $output" } - return [ordered]@{ - directory = $directory; workspace = $workspace; resultPath = $resultPath; result = $result - results = $results; manifest = $manifest; manifestPath = $manifestPath; mapPath = $mapPath - baselinePath = $baselinePath; captureArgs = $captureArgs - scoreArgs = @('-Root', $root, '-ManifestPath', $manifestPath, '-ResultsDirectory', $results, '-BaselinePath', $baselinePath, '-BaselineSha256', (Get-GuidanceHash $baselinePath)) - } -} - -function Test-ReportMutation([string] $Name, [scriptblock] $Change, [string] $Diagnostic) { - if ($null -eq $script:reportScenario) { $script:reportScenario = New-TestScenario } - $scenario = $script:reportScenario - $result = $scenario.result | ConvertTo-Json -Depth 64 | ConvertFrom-Json -AsHashtable - & $Change $result - Set-TestJson $scenario.resultPath $result - Invoke-EvaluatorTest $Name $scenario.scoreArgs $false $Diagnostic -} - -try { - [IO.Directory]::CreateDirectory($root) | Out-Null - foreach ($reference in @($article, $otherArticle, $sample, $otherArticle.Replace('.md', '.good.al'), - 'microsoft/skills/development/al-development-plan.md', 'tools/Build-KnowledgeIndex.ps1', - 'tools/Knowledge-Retrieval.ps1')) { - $destination = Join-Path $root $reference.Replace('/', [IO.Path]::DirectorySeparatorChar) - [IO.Directory]::CreateDirectory((Split-Path $destination -Parent)) | Out-Null - [IO.File]::Copy((Join-Path $sourceRoot $reference.Replace('/', [IO.Path]::DirectorySeparatorChar)), $destination) - } - Initialize-TestRepository $root - - $publicManifestPath = Join-Path $sourceRoot 'evaluation\development-guidance-fixtures.json' - $publicManifest = Read-GuidanceJson $publicManifestPath - $publicRoot = Join-Path $scratch 'public-fixture-checkout' - $publicKnowledgeReferences = @( - $publicManifest.cases | - ForEach-Object { $_.requiredKnowledge; $_.optionalKnowledge } | - Sort-Object -Unique - ) - $publicSampleReferences = @( - foreach ($articleReference in $publicKnowledgeReferences) { - $articlePath = Join-Path $sourceRoot $articleReference.Replace('/', [IO.Path]::DirectorySeparatorChar) - $articleDirectory = Split-Path $articleReference -Parent - $articleText = [IO.File]::ReadAllText($articlePath) - foreach ($match in [regex]::Matches( - $articleText, - '\[[^\]]+\]\((?[a-z0-9-]+\.(?:good|bad)\.[a-zA-Z0-9]+)\)' - )) { - "$($articleDirectory.Replace('\', '/'))/$($match.Groups['sample'].Value)" - } - } - ) - $publicReferences = @( - @( - $publicManifest.skill, - 'tools/Build-KnowledgeIndex.ps1', - 'tools/Knowledge-Retrieval.ps1', - 'evaluation/development-guidance-fixtures.json' - ) + - $publicKnowledgeReferences + - $publicSampleReferences | - Sort-Object -Unique - ) - foreach ($reference in @($publicReferences | Sort-Object -Unique)) { - $destination = Join-Path $publicRoot $reference.Replace('/', [IO.Path]::DirectorySeparatorChar) - [IO.Directory]::CreateDirectory((Split-Path $destination -Parent)) | Out-Null - [IO.File]::Copy((Join-Path $sourceRoot $reference.Replace('/', [IO.Path]::DirectorySeparatorChar)), $destination) - } - $publicPrepared = Join-Path $scratch 'public-prepared' - Invoke-EvaluatorTest 'public five-case manifest validation' @('-Root', $publicRoot) $true - Invoke-EvaluatorTest 'public five-case manifest preparation' @('-Root', $publicRoot, '-PrepareDirectory', $publicPrepared) $true - $initialCase = $publicManifest.cases | Where-Object id -eq 'synthetic-normal-initial-plan' - $initialRequest = Read-GuidanceJson (Join-Path $publicPrepared "request-$(Get-GuidanceCaseId $initialCase.id).json") - if ($initialRequest.'development-plan' -cne $initialCase.'development-plan') { throw 'Preparation truncated the serialized initial plan.' } - $document = $initialRequest.'development-plan' | ConvertFrom-Json -AsHashtable - if ($document.metadata.kind -ne 'bug' -or - @('Root cause and design', 'Proposed fix', 'Affected files', 'Test strategy', 'Acceptance criteria' | - Where-Object { $document.body -notmatch [regex]::Escape($_) }).Count) { - throw 'Synthetic consumer boundary lost metadata or markdown plan sections.' - } - $tests.Add('serialized synthetic initial-plan boundary preserves full metadata and markdown body') - - $scenario = New-TestScenario - Invoke-EvaluatorTest 'unchanged target with ignored and untracked files passes' $scenario.scoreArgs $true - Invoke-EvaluatorTest 'scoring itself leaves index and content unchanged' $scenario.scoreArgs $true - Invoke-EvaluatorTest 'existing baseline cannot silently recapture' $scenario.captureArgs $false 'Baseline already exists' - Invoke-EvaluatorTest 'scoring without baseline fails' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-ResultsDirectory', $scenario.results) $false 'require BaselinePath' - Invoke-EvaluatorTest 'scoring requires independently retained baseline digest' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-ResultsDirectory', $scenario.results, '-BaselinePath', $scenario.baselinePath) $false 'runner-retained pre-run BaselineSha256' - Invoke-EvaluatorTest 'tampered baseline digest fails' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-ResultsDirectory', $scenario.results, '-BaselinePath', $scenario.baselinePath, '-BaselineSha256', ('0' * 64)) $false 'digest mismatch' - - $prepare = Join-Path $scenario.directory 'prepared' - Invoke-EvaluatorTest 'prepare outside roots with runner workspace binding' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-PrepareDirectory', $prepare, '-WorkspaceMapPath', $scenario.mapPath) $true - $prepared = Read-GuidanceJson (Join-Path $prepare "request-$($scenario.result.caseId).json") - if ($prepared.repository -cne $scenario.workspace -or $prepared.Contains('expectedOutcome') -or $prepared.Contains('requiredKnowledge')) { - throw 'Prepared request lost runner binding or exposed answers.' - } - Invoke-EvaluatorTest 'preparation never overwrites requests' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-PrepareDirectory', $prepare) $false 'new or empty' - foreach ($option in @('PrepareDirectory', 'BaselinePath', 'ResultsDirectory')) { - $inside = Join-Path $scenario.workspace 'unsafe-artifact' - $arguments = @('-Root', $root, '-ManifestPath', $scenario.manifestPath) - if ($option -eq 'PrepareDirectory') { $arguments += @('-PrepareDirectory', $inside, '-WorkspaceMapPath', $scenario.mapPath) } - elseif ($option -eq 'BaselinePath') { $arguments += @('-CaptureBaseline', '-BaselinePath', $inside, '-WorkspaceMapPath', $scenario.mapPath) } - else { $arguments += @('-BaselinePath', $scenario.baselinePath, '-BaselineSha256', (Get-GuidanceHash $scenario.baselinePath), '-ResultsDirectory', $inside) } - Invoke-EvaluatorTest "$option inside target rejected" $arguments $false 'outside target workspaces' - } - - foreach ($mutation in @('uncommitted', 'committed', 'empty-commit', 'staged', 'index-only', 'untracked', 'ignored', 'ignored-child', 'added', 'deleted', 'directory', 'ref', 'metadata')) { - $scenario = New-TestScenario - switch ($mutation) { - 'uncommitted' { [IO.File]::AppendAllText((Join-Path $scenario.workspace 'tracked.al'), "`n// changed") } - 'committed' { - [IO.File]::AppendAllText((Join-Path $scenario.workspace 'tracked.al'), "`n// changed") - Invoke-TestGit $scenario.workspace @('add', 'tracked.al') - Invoke-TestGit $scenario.workspace @('commit', '--quiet', '-m', 'Committed forbidden edit') - } - 'staged' { - [IO.File]::AppendAllText((Join-Path $scenario.workspace 'tracked.al'), "`n// changed") - Invoke-TestGit $scenario.workspace @('add', 'tracked.al') - } - 'empty-commit' { Invoke-TestGit $scenario.workspace @('commit', '--quiet', '--allow-empty', '-m', 'Forbidden empty commit') } - 'index-only' { Invoke-TestGit $scenario.workspace @('update-index', '--assume-unchanged', 'tracked.al') } - 'untracked' { [IO.File]::AppendAllText((Join-Path $scenario.workspace 'untracked.txt'), 'changed') } - 'ignored' { [IO.File]::AppendAllText((Join-Path $scenario.workspace 'ignored.txt'), 'changed') } - 'ignored-child' { [IO.File]::AppendAllText((Join-Path $scenario.workspace 'ignored-directory\child.txt'), 'changed') } - 'added' { [IO.File]::WriteAllText((Join-Path $scenario.workspace 'new.txt'), 'new ignored/untracked payload') } - 'deleted' { [IO.File]::Delete((Join-Path $scenario.workspace 'untracked.txt')) } - 'directory' { [IO.Directory]::CreateDirectory((Join-Path $scenario.workspace 'new-empty-directory')) | Out-Null } - 'ref' { Invoke-TestGit $scenario.workspace @('branch', 'new-reference') } - 'metadata' { - $path = Join-Path $scenario.workspace 'tracked.al' - [IO.File]::SetLastWriteTimeUtc($path, [IO.File]::GetLastWriteTimeUtc($path).AddSeconds(5)) - } - } - Invoke-EvaluatorTest "$mutation mutation fails" $scenario.scoreArgs $false 'identity/content changed' - } - - $scenario = New-TestScenario -SecondCase - Invoke-EvaluatorTest 'two independently bound workspaces pass' $scenario.scoreArgs $true - $map = Read-GuidanceJson $scenario.mapPath - [IO.File]::AppendAllText((Join-Path $scenario.workspace 'tracked.al'), "`n// changed") - $scenario.result.workspaceRoot = $map['second-case'] - Set-TestJson $scenario.resultPath $scenario.result - Invoke-EvaluatorTest 'self-reported clean workspace cannot hide changed runner target' $scenario.scoreArgs $false 'identity/content changed' - $scenario = New-TestScenario -SecondCase - $scenario.result.workspaceRoot = (Read-GuidanceJson $scenario.mapPath)['second-case'] - Set-TestJson $scenario.resultPath $scenario.result - Invoke-EvaluatorTest 'result workspace swapping rejected even when both are clean' $scenario.scoreArgs $false 'runner binding' - $scenario = New-TestScenario -SecondCase - $scenario.result.caseId = Get-GuidanceCaseId 'second-case' - Set-TestJson $scenario.resultPath $scenario.result - Invoke-EvaluatorTest 'result case swapping rejected' $scenario.scoreArgs $false 'caseId mismatch' - $scenario = New-TestScenario - $scenario.manifest.cases[0].expectedKind = 'feature' - Set-TestJson $scenario.manifestPath $scenario.manifest - Invoke-EvaluatorTest 'changed manifest rejected' $scenario.scoreArgs $false 'root or manifest' - $scenario = New-TestScenario - $baseline = Read-GuidanceJson $scenario.baselinePath - $baseline.workspaces['synthetic-case'].caseId = 'case-00000000' - Set-TestJson $scenario.baselinePath $baseline - Invoke-EvaluatorTest 'baseline case binding checked even with matching digest' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-ResultsDirectory', $scenario.results, '-BaselinePath', $scenario.baselinePath, '-BaselineSha256', (Get-GuidanceHash $scenario.baselinePath)) $false 'case identity mismatch' - - foreach ($outcome in @('no-knowledge', 'partial', 'failed', 'not-applicable')) { - $scenario = New-TestScenario $outcome - Invoke-EvaluatorTest "honest $outcome is distinguishable and passes" $scenario.scoreArgs $true - } - $scenario = New-TestScenario 'partial' -Unknown - Invoke-EvaluatorTest 'material unknown stays partial' $scenario.scoreArgs $true - $scenario.result.guidanceReport.outcome = 'completed' - Set-TestJson $scenario.resultPath $scenario.result - Invoke-EvaluatorTest 'material unknown cannot silently complete' $scenario.scoreArgs $false 'expectedOutcome' - $scenario = New-TestScenario 'completed' -Unknown - Invoke-EvaluatorTest 'nonmaterial unknown can complete with explanation' $scenario.scoreArgs $true - $scenario.result.guidanceReport.unresolved = @() - Set-TestJson $scenario.resultPath $scenario.result - Invoke-EvaluatorTest 'unknown cannot disappear from unresolved evidence' $scenario.scoreArgs $false 'unresolved gaps' - $scenario = New-TestScenario 'no-knowledge' -Unknown - Invoke-EvaluatorTest 'immaterial unknown and no-knowledge are not failures' $scenario.scoreArgs $true - foreach ($outcome in @('partial', 'failed')) { - $scenario = New-TestScenario $outcome - $scenario.result.guidanceReport.Remove('outcome-reason') - Set-TestJson $scenario.resultPath $scenario.result - Invoke-EvaluatorTest "$outcome requires outcome-reason" $scenario.scoreArgs $false 'outcome-reason' - } - $scenario = New-TestScenario 'no-knowledge' - $scenario.result.guidanceReport.outcome = 'completed' - Set-TestJson $scenario.resultPath $scenario.result - Invoke-EvaluatorTest 'no-knowledge is not completed-empty' $scenario.scoreArgs $false 'expectedOutcome' - $scenario = New-TestScenario 'no-knowledge' - $scenario.result.guidanceReport.knowledge = @(@{ path = $article; 'used-for' = 'filler'; constraints = @('filler'); 'sample-paths' = @() }) - $scenario.result.guidanceReport.summary.candidates = 1 - $scenario.result.guidanceReport.summary.selected = 1 - Set-TestJson $scenario.resultPath $scenario.result - Invoke-EvaluatorTest 'no-knowledge cannot contain filler knowledge' $scenario.scoreArgs $false 'requires empty knowledge' - - Test-ReportMutation 'invalid outcome enum' { param($r) $r.guidanceReport.outcome = 'success' } 'outcome enum' - Test-ReportMutation 'missing report fields' { param($r) $r.guidanceReport.Remove('knowledge') } 'missing required field' - Test-ReportMutation 'null report object' { param($r) $r.guidanceReport = $null } 'JSON object' - Test-ReportMutation 'wrong skill version type' { param($r) $r.guidanceReport.skill.version = '1' } 'identity/version' - Test-ReportMutation 'wrong skill id type' { param($r) $r.guidanceReport.skill.id = @('al-development-plan') } 'non-empty string' - Test-ReportMutation 'wrong kind type' { param($r) $r.guidanceReport.summary.kind = @('bug') } 'non-empty string' - Test-ReportMutation 'wrong summary type' { param($r) $r.guidanceReport.summary = @() } 'JSON object' - Test-ReportMutation 'fractional count' { param($r) $r.guidanceReport.summary.candidates = 1.5 } 'non-negative integer' - Test-ReportMutation 'negative count' { param($r) $r.guidanceReport.summary.selected = -1 } 'non-negative integer' - Test-ReportMutation 'inconsistent selected count' { param($r) $r.guidanceReport.summary.selected = 0 } 'Summary counts' - Test-ReportMutation 'candidate count below selected' { param($r) $r.guidanceReport.summary.candidates = 0 } 'Summary counts' - Test-ReportMutation 'wrong context list type' { param($r) $r.guidanceReport.context.technologies = 'al' } 'JSON array' - Test-ReportMutation 'missing constraints' { param($r) $r.guidanceReport.knowledge[0].constraints = @() } 'must not be empty' - Test-ReportMutation 'non-string constraints' { param($r) $r.guidanceReport.knowledge[0].constraints = @(@{ body = 'MODEL_SECRET_SENTINEL' }) } 'non-empty string' - Test-ReportMutation 'missing sample array' { param($r) $r.guidanceReport.knowledge[0].Remove('sample-paths') } 'missing required field' - Test-ReportMutation 'duplicate knowledge' { - param($r) - $r.guidanceReport.knowledge += $r.guidanceReport.knowledge[0] - $r.guidanceReport.summary.selected = 2 - $r.guidanceReport.summary.candidates = 2 - } 'Duplicate knowledge' - Test-ReportMutation 'bad validation entry' { param($r) $r.guidanceReport.'validation-considerations'[0].evidence = $false } 'non-empty string' - Test-ReportMutation 'invalid suppression shape' { param($r) $r.guidanceReport.suppressed = @(@{ path = $article; reason = 'configuration' }) } 'missing required field' - Test-ReportMutation 'invalid unresolved shape' { param($r) $r.guidanceReport.unresolved = @(@{ candidate = $article }) } 'non-empty string' - Test-ReportMutation 'invalid SHA provenance' { param($r) $r.guidanceReport.knowledge[0].sha = '0' * 40 } 'recorded live checkout' - $scenario = New-TestScenario - $scenario.result.guidanceReport.knowledge[0].sha = Invoke-GuidanceGit $root @('rev-parse', 'HEAD') - Set-TestJson $scenario.resultPath $scenario.result - Invoke-EvaluatorTest 'actual pinned knowledge SHA accepted' $scenario.scoreArgs $true - - foreach ($badPath in @('../outside.md', '/absolute.md', 'C:/external.md', - 'microsoft\knowledge\performance\pair-findset-with-next-loop.md', - 'microsoft/knowledge/performance/../performance/pair-findset-with-next-loop.md', - 'https://example.invalid/article.md', 'microsoft/knowledge/performance/missing.md', - 'microsoft/skills/development/al-development-plan.md')) { - $scenario = New-TestScenario - $scenario.result.guidanceReport.knowledge[0].path = $badPath - Set-TestJson $scenario.resultPath $scenario.result - Invoke-EvaluatorTest 'unsafe/nonexistent/non-knowledge citation rejected' $scenario.scoreArgs $false - } - foreach ($badSample in @('../outside.al', $article, $otherArticle.Replace('.md', '.good.al'), 'microsoft/knowledge/performance/other.good.al', 'microsoft\knowledge\performance\pair-findset-with-next-loop.good.al')) { - $scenario = New-TestScenario - $scenario.result.guidanceReport.knowledge[0].'sample-paths' = @($badSample) - Set-TestJson $scenario.resultPath $scenario.result - Invoke-EvaluatorTest 'unsafe/nonexistent/non-sibling sample rejected' $scenario.scoreArgs $false - } - foreach ($json in @('{', 'null', '[]', '{"caseId":"MODEL_SECRET_SENTINEL","caseId":"duplicate"}', '{"caseId":true,}', '// comment')) { - $scenario = New-TestScenario - [IO.File]::WriteAllText($scenario.resultPath, $json) - Invoke-EvaluatorTest 'malformed model result fails without runtime crash or content leakage' $scenario.scoreArgs $false - } - - $scenario = New-TestScenario - $outside = Join-Path $scenario.directory 'outside' - [IO.Directory]::CreateDirectory($outside) | Out-Null - $link = Join-Path $scenario.workspace 'escape' - $linkKind = if ($IsWindows) { 'Junction' } else { 'SymbolicLink' } - New-Item -ItemType $linkKind -Path $link -Target $outside | Out-Null - try { - Invoke-EvaluatorTest 'target junction/symlink rejected instead of followed' $scenario.scoreArgs $false 'Links, junctions' - Invoke-EvaluatorTest 'baseline capture rejects junction/symlink target children' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-CaptureBaseline', '-WorkspaceMapPath', $scenario.mapPath, '-BaselinePath', (Join-Path $scenario.directory 'linked-baseline.json')) $false 'Links, junctions' - Invoke-EvaluatorTest 'prepared directory cannot escape through junction' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-PrepareDirectory', (Join-Path $link 'prepared'), '-WorkspaceMapPath', $scenario.mapPath) $false 'Links, junctions' - } finally { Remove-Item -LiteralPath $link -Force } - $scenario = New-TestScenario - $external = Join-Path $scenario.directory 'external.txt' - [IO.File]::WriteAllText($external, 'external hard-link target') - $hardLink = Join-Path $scenario.workspace 'hard-link.txt' - New-Item -ItemType HardLink -Path $hardLink -Target $external | Out-Null - try { - Invoke-EvaluatorTest 'hard-link escape rejected' $scenario.scoreArgs $false 'Links, junctions' - } finally { Remove-Item -LiteralPath $hardLink -Force } - $scenario = New-TestScenario - $externalArticle = Join-Path $scenario.directory 'outside.md' - [IO.File]::Copy((Join-Path $root $article.Replace('/', [IO.Path]::DirectorySeparatorChar)), $externalArticle) - $articleLink = Join-Path $root 'custom\knowledge\performance' - [IO.Directory]::CreateDirectory((Split-Path $articleLink -Parent)) | Out-Null - New-Item -ItemType $linkKind -Path $articleLink -Target $scenario.directory | Out-Null - try { - $rejected = $false - try { $null = Resolve-GuidanceReference $root 'custom/knowledge/performance/outside.md' -Knowledge } - catch { $rejected = $_.Exception.Message -match 'Links, junctions' } - if (-not $rejected) { throw 'Linked knowledge reference was followed.' } - $tests.Add('knowledge junction/symlink reference rejected') - $rejected = $false - try { $null = Resolve-GuidanceReference $root 'custom/knowledge/performance/pair-findset-with-next-loop.good.al' -Article 'custom/knowledge/performance/pair-findset-with-next-loop.md' } - catch { $rejected = $_.Exception.Message -match 'Links, junctions' } - if (-not $rejected) { throw 'Linked sample reference was followed.' } - $tests.Add('sample junction/symlink reference rejected') - } finally { Remove-Item -LiteralPath $articleLink -Force } - $normativePath = Join-Path $root 'microsoft\knowledge\performance\single-normative-section.md' - foreach ($heading in @('Best Practice', 'Anti Pattern')) { - [IO.File]::WriteAllText($normativePath, "---`ndomain: performance`n---`n## Description`nSynthetic contract fixture.`n## $heading`nSynthetic normative constraint.`n") - $null = Resolve-GuidanceReference $root 'microsoft/knowledge/performance/single-normative-section.md' -Knowledge - $tests.Add("Knowledge article with only $heading accepted") - } - [IO.File]::Delete($normativePath) - $scenario = New-TestScenario - $linkedRoot = Join-Path $scratch 'linked-knowledge-checkout' - Invoke-TestGit $root @('worktree', 'add', '--quiet', '--detach', $linkedRoot, 'HEAD') - $linkedBaseline = Join-Path $scenario.directory 'linked-root-baseline.json' - Invoke-EvaluatorTest 'linked knowledge checkout baseline capture' @('-Root', $linkedRoot, '-ManifestPath', $scenario.manifestPath, '-CaptureBaseline', '-WorkspaceMapPath', $scenario.mapPath, '-BaselinePath', $linkedBaseline) $true - Invoke-EvaluatorTest 'unchanged linked knowledge checkout scoring' @('-Root', $linkedRoot, '-ManifestPath', $scenario.manifestPath, '-ResultsDirectory', $scenario.results, '-BaselinePath', $linkedBaseline, '-BaselineSha256', (Get-GuidanceHash $linkedBaseline)) $true - $scenario = New-TestScenario - $map = Read-GuidanceJson $scenario.mapPath - $map['synthetic-case'] = $linkedRoot - Set-TestJson $scenario.mapPath $map - Invoke-EvaluatorTest 'linked target checkout rejected as external Git storage' @('-Root', $root, '-ManifestPath', $scenario.manifestPath, '-CaptureBaseline', '-WorkspaceMapPath', $scenario.mapPath, '-BaselinePath', (Join-Path $scenario.directory 'external-git-baseline.json')) $false 'standalone repositories' - $scenario = New-TestScenario - [IO.File]::AppendAllText((Join-Path $root $sample.Replace('/', [IO.Path]::DirectorySeparatorChar)), "`n// changed knowledge sample") - Invoke-EvaluatorTest 'knowledge checkout changes rejected' $scenario.scoreArgs $false 'Knowledge checkout identity/content changed' - Write-Host "Development guidance evaluator regressions PASSED: $($tests.Count) checks." -} finally { - if (Test-Path -LiteralPath $scratch) { - # Only our own uniquely named directory; links made by tests are removed above. - Remove-Item -LiteralPath $scratch -Recurse -Force - } -} - -# Intentional negative native-command probes leave LASTEXITCODE nonzero. -exit 0 diff --git a/tools/Test-DevelopmentGuidanceFixtures.ps1 b/tools/Test-DevelopmentGuidanceFixtures.ps1 deleted file mode 100644 index 0233ddfb..00000000 --- a/tools/Test-DevelopmentGuidanceFixtures.ps1 +++ /dev/null @@ -1,219 +0,0 @@ -<# -.SYNOPSIS - Validates, prepares, and scores read-only AL development-guidance fixtures. -.DESCRIPTION - Capture runner-owned evidence BEFORE invoking an agent: - -CaptureBaseline -WorkspaceMapPath -BaselinePath - The workspace map is {"manifest-case-id":"absolute-standalone-git-root",...}. - Score AFTER the agent finishes: - -BaselinePath -BaselineSha256 -ResultsDirectory - Preparation (-PrepareDirectory) is independent; supply -WorkspaceMapPath to - include runner-selected target paths. Never derive target paths from results. - - Baseline, map, prepared requests, and results must be outside all targets and - the knowledge checkout. Keep the baseline runner-only; retain the printed - SHA256 and pass -BaselineSha256 when scoring to detect baseline tampering. - Capture never overwrites an existing baseline. The runner must protect this - script, the baseline/digest and its invocation from the agent. - - This compares before/after evidence, NOT an OS sandbox or a write monitor. - It cannot detect reverted transient writes, prove that articles were opened, - or validate semantic faithfulness of prose. Files, directories, hashes, - stable metadata, Git HEAD/refs/index and ignored/untracked files are compared. - Links/reparse points, hard links, external Git storage in targets, - submodules and sparse checkouts are rejected rather than followed. Windows - alternate data streams are rejected except for the known endpoint-DLP - metadata stream `sec.endpointdlp`, which is ignored because endpoint - protection may add or refresh it asynchronously without changing file - content. Direct stream paths remain rejected. Run in quiescent repositories. - The knowledge checkout may itself be a linked Git worktree; its Git storage - identity is recorded explicitly. -#> -[CmdletBinding()] -param( - [string] $Root = (Join-Path $PSScriptRoot '..'), - [string] $ManifestPath, - [string] $PrepareDirectory, - [string] $ResultsDirectory, - [switch] $CaptureBaseline, - [string] $BaselinePath, - [string] $BaselineSha256, - [string] $WorkspaceMapPath -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' -. (Join-Path $PSScriptRoot 'DevelopmentGuidance.Evidence.ps1') - -try { - if ($CaptureBaseline -and ($ResultsDirectory -or $PrepareDirectory)) { - throw 'CaptureBaseline is a separate pre-run operation.' - } - if (($CaptureBaseline -or $ResultsDirectory) -and -not $BaselinePath) { - throw 'Capture and scoring require BaselinePath.' - } - if ($ResultsDirectory -and -not $BaselineSha256) { - throw 'Scoring requires the runner-retained pre-run BaselineSha256.' - } - if ($CaptureBaseline -and -not $WorkspaceMapPath) { - throw 'Capture requires a runner-owned WorkspaceMapPath.' - } - if ($ResultsDirectory -and ($WorkspaceMapPath -or $PrepareDirectory)) { - throw 'Scoring uses only the recorded workspace map; run preparation separately.' - } - $Root = Get-GuidanceSafePath $Root -Directory - if (-not $ManifestPath) { $ManifestPath = Join-Path $Root 'evaluation\development-guidance-fixtures.json' } - $ManifestPath = Get-GuidanceSafePath $ManifestPath -File - $manifest = Read-GuidanceJson $ManifestPath - Assert-GuidanceManifest $manifest $Root - $caseIds = @($manifest.cases | ForEach-Object { $_.id }) - $workspaces = [ordered]@{} - $baseline = $null - if ($WorkspaceMapPath) { - $WorkspaceMapPath = Get-GuidanceSafePath $WorkspaceMapPath -File - $map = Read-GuidanceJson $WorkspaceMapPath - Assert-GuidanceObject $map 'workspace map' - if ($map.Count -ne $caseIds.Count) { throw 'Workspace map must bind exactly every manifest case.' } - foreach ($id in $caseIds) { - Assert-GuidanceString $map[$id] 'workspace map value' - if (-not [IO.Path]::IsPathFullyQualified($map[$id])) { throw 'Workspace roots must be absolute.' } - $workspaces[$id] = Get-GuidanceSafePath $map[$id] -Directory - } - } - if ($ResultsDirectory) { - $BaselinePath = Get-GuidanceSafePath $BaselinePath -File - if ($BaselineSha256 -cnotmatch '^[0-9A-Fa-f]{64}$') { - throw 'BaselineSha256 must be a SHA256 digest.' - } - if ((Get-GuidanceHash $BaselinePath) -ne $BaselineSha256) { - throw 'Runner baseline digest mismatch.' - } - $baseline = Read-GuidanceJson $BaselinePath - Assert-GuidanceObject $baseline 'baseline' @('version', 'kind', 'root', 'manifestPath', 'manifestSha256', 'workspaces', 'rootSnapshot') - if ($baseline.version -ne 1 -or $baseline.kind -cne 'bcquality-guidance-runner-baseline') { - throw 'Unsupported runner baseline.' - } - if ($baseline.root -cne $Root -or $baseline.manifestPath -cne $ManifestPath -or - $baseline.manifestSha256 -ne (Get-GuidanceHash $ManifestPath)) { - throw 'Runner baseline does not match the knowledge root or manifest.' - } - Assert-GuidanceObject $baseline.workspaces 'baseline workspaces' - if ($baseline.workspaces.Count -ne $caseIds.Count) { throw 'Runner baseline case set mismatch.' } - foreach ($id in $caseIds) { - $entry = $baseline.workspaces[$id] - Assert-GuidanceObject $entry 'baseline workspace entry' @('caseId', 'root', 'snapshot') - if ($entry.caseId -cne (Get-GuidanceCaseId $id)) { throw 'Runner baseline case identity mismatch.' } - $workspaces[$id] = Get-GuidanceSafePath $entry.root -Directory - } - } - $protectedRoots = @($Root) + @($workspaces.Values) - for ($i = 0; $i -lt $protectedRoots.Count; $i++) { - for ($j = $i + 1; $j -lt $protectedRoots.Count; $j++) { - if ((Test-GuidanceWithin $protectedRoots[$i] $protectedRoots[$j]) -or - (Test-GuidanceWithin $protectedRoots[$j] $protectedRoots[$i])) { - throw 'Knowledge checkout and case workspaces must be distinct, non-overlapping roots.' - } - } - } - foreach ($artifact in @($BaselinePath, $WorkspaceMapPath, $PrepareDirectory, $ResultsDirectory)) { - if ($artifact) { - $safeArtifact = Get-GuidanceSafePath $artifact -AllowMissing - foreach ($protectedRoot in $protectedRoots) { - if (Test-GuidanceWithin $safeArtifact $protectedRoot) { - throw 'Runner artifacts must be outside target workspaces and the knowledge checkout.' - } - } - } - } - if ($CaptureBaseline) { - $BaselinePath = Get-GuidanceSafePath $BaselinePath -AllowMissing - if (Test-Path -LiteralPath $BaselinePath) { throw 'Baseline already exists; capture never overwrites evidence.' } - $snapshots = [ordered]@{} - foreach ($id in $caseIds) { - $snapshots[$id] = [ordered]@{ - caseId = Get-GuidanceCaseId $id - root = $workspaces[$id] - snapshot = Get-GuidanceSnapshot $workspaces[$id] -Target - } - } - $record = [ordered]@{ - kind = 'bcquality-guidance-runner-baseline' - version = 1 - root = $Root - manifestPath = $ManifestPath - manifestSha256 = Get-GuidanceHash $ManifestPath - rootSnapshot = Get-GuidanceSnapshot $Root - workspaces = $snapshots - } - Write-GuidanceNewJson $BaselinePath $record - Write-Host "Guidance baseline captured. Runner SHA256: $(Get-GuidanceHash $BaselinePath)" - } elseif ($PrepareDirectory) { - $PrepareDirectory = Get-GuidanceSafePath $PrepareDirectory -AllowMissing - if ((Test-Path -LiteralPath $PrepareDirectory) -and - @(Get-ChildItem -LiteralPath $PrepareDirectory -Force).Count) { - throw 'PrepareDirectory must be new or empty; existing requests/evidence are never overwritten.' - } - [IO.Directory]::CreateDirectory($PrepareDirectory) | Out-Null - # The index builder walks recursively; reject linked corpus paths first. - Assert-GuidanceTree $Root - & (Join-Path $Root 'tools\Build-KnowledgeIndex.ps1') -BCQualityRoot $Root ` - -IndexPath (Join-Path $PrepareDirectory 'knowledge-index.json') | Out-Null - $skillInstructions = [IO.File]::ReadAllText((Resolve-GuidanceReference $Root $manifest.skill)) - foreach ($case in $manifest.cases) { - $modelId = Get-GuidanceCaseId $case.id - $request = [ordered]@{ - protocol = 'Run the supplied read-only skill on the runner-assigned repository and existing plan. Return only caseId and guidanceReport. The runner captures evidence before invocation; do not capture or modify it. Do not create artifacts in the target or knowledge checkout.' - caseId = $modelId - skill = $manifest.skill - skillInstructions = $skillInstructions - knowledgeIndex = Join-Path $PrepareDirectory 'knowledge-index.json' - knowledgeRoot = $Root - 'task-context' = [ordered]@{ - goal = Get-GuidancePlanRequest $case.'development-plan' - 'inputs-available' = @('development-plan', 'repository') - 'bc-version' = $case.context.'bc-version' - technologies = $case.context.technologies - countries = $case.context.countries - 'application-area' = $case.context.'application-area' - } - 'development-plan' = $case.'development-plan' - resultSchema = Get-GuidanceResultSchema $modelId - } - if ($workspaces.Count) { $request.repository = $workspaces[$case.id] } - Write-GuidanceNewJson (Join-Path $PrepareDirectory "request-$modelId.json") $request - } - Write-Host "Development guidance preparation PASSED: $($caseIds.Count) case(s)." - } elseif ($ResultsDirectory) { - $ResultsDirectory = Get-GuidanceSafePath $ResultsDirectory -Directory - $failures = [Collections.Generic.List[string]]::new() - if (-not (Test-GuidanceSnapshotEqual $baseline.rootSnapshot (Get-GuidanceSnapshot $Root))) { - $failures.Add('Knowledge checkout identity/content changed after baseline capture.') - } - foreach ($case in $manifest.cases) { - $id = $case.id - try { - if (-not (Test-GuidanceSnapshotEqual $baseline.workspaces[$id].snapshot ` - (Get-GuidanceSnapshot $workspaces[$id] -Target))) { - throw 'Target repository identity/content changed after baseline capture.' - } - $resultPath = Get-GuidanceSafePath (Join-Path $ResultsDirectory "result-$(Get-GuidanceCaseId $id).json") -File - $result = Read-GuidanceJson $resultPath - Assert-GuidanceResult $result $case $manifest $Root $workspaces[$id] - } catch { - # Only evaluator-authored diagnostics are printed, never model or file contents. - $failures.Add("${id}: $(Get-GuidanceDiagnostic $_)") - } - } - if ($failures.Count) { - Write-Host "Development guidance scoring FAILED ($($failures.Count) problem(s)):" - $failures | ForEach-Object { Write-Host " - $_" } - exit 1 - } - Write-Host "Development guidance scoring PASSED: $($caseIds.Count) case(s)." - } else { - Write-Host "Development guidance fixture validation PASSED: $($caseIds.Count) case(s)." - } -} catch { - Write-Host "Development guidance FAILED: $(Get-GuidanceDiagnostic $_)" - exit 1 -}