mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
Merge pull request #46 from Curabis/fix/custom-layer-validation
Custom-laget bestaar nu begge CI-checks: 72 validator-fejl -> 0
This commit is contained in:
commit
4989e3ec4b
39 changed files with 729 additions and 814 deletions
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [build, output, alpackages, duplicate, language-server, app-package, project-root, AL0197]
|
||||
keywords: [build, output, alpackages, duplicate, language-server, app-package, project-root, al0197]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
|
|
@ -20,16 +20,12 @@ AL build output (`.app` files) **must not** accumulate in the project root folde
|
|||
Configure the build output path to a dedicated subfolder that is excluded from language server scanning.
|
||||
|
||||
In `.vscode/settings.json`:
|
||||
```json
|
||||
{
|
||||
"al.outputPath": ".output"
|
||||
}
|
||||
```
|
||||
{
|
||||
"al.outputPath": ".output"
|
||||
}
|
||||
|
||||
When using the MCP `al_build` tool, pass `outputPath` explicitly:
|
||||
```
|
||||
al_build projectPath="..." outputPath=".output/AppName.app"
|
||||
```
|
||||
al_build projectPath="..." outputPath=".output/AppName.app"
|
||||
|
||||
Add `.output/` to `.gitignore` if not already excluded.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [identifiers, naming, english, captions, translation]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# AL Naming Convention: English Identifiers Only
|
||||
|
||||
## Core Rule
|
||||
## Description
|
||||
|
||||
All AL identifiers must be written in English, regardless of the developer's native language. "Translations are handled separately via XLIFF files — never by writing Danish, German or other language identifiers in AL source code."
|
||||
|
||||
|
|
|
|||
|
|
@ -39,15 +39,13 @@ Ask before coding if any of the following is true:
|
|||
|
||||
State what you understand the task to be, then list the specific questions:
|
||||
|
||||
```
|
||||
I understand the task as: [one sentence summary]
|
||||
I understand the task as: [one sentence summary]
|
||||
|
||||
Before I proceed, I need clarification on:
|
||||
1. [specific question]
|
||||
2. [specific question]
|
||||
Before I proceed, I need clarification on:
|
||||
1. [specific question]
|
||||
2. [specific question]
|
||||
|
||||
I will not write any code until these are answered.
|
||||
```
|
||||
I will not write any code until these are answered.
|
||||
|
||||
Do not write partial code while waiting. Do not write "placeholder" code.
|
||||
Do not write code with TODO comments where a business decision is needed.
|
||||
|
|
@ -66,29 +64,25 @@ files first and resolve the ambiguity yourself before asking the developer.
|
|||
|
||||
## Anti Pattern
|
||||
|
||||
```
|
||||
// WRONG: Assuming and building
|
||||
// Task: "write a test for the vendor flow"
|
||||
// Agent assumes: Vendor, Incoming, Handle+Invoice, no contact
|
||||
// Writes 80 lines of code
|
||||
// Developer says: "I meant outgoing flow with a customer"
|
||||
// Result: wasted work, possible production code changes to revert
|
||||
```
|
||||
// WRONG: Assuming and building
|
||||
// Task: "write a test for the vendor flow"
|
||||
// Agent assumes: Vendor, Incoming, Handle+Invoice, no contact
|
||||
// Writes 80 lines of code
|
||||
// Developer says: "I meant outgoing flow with a customer"
|
||||
// Result: wasted work, possible production code changes to revert
|
||||
|
||||
## Best Practice
|
||||
|
||||
```
|
||||
// CORRECT: Clarify first
|
||||
// Task: "write a test for the vendor flow"
|
||||
// CORRECT: Clarify first
|
||||
// Task: "write a test for the vendor flow"
|
||||
|
||||
I understand the task as: writing a test codeunit for a Settlement Voucher
|
||||
flow where the counterparty is a Vendor.
|
||||
I understand the task as: writing a test codeunit for a Settlement Voucher
|
||||
flow where the counterparty is a Vendor.
|
||||
|
||||
Before I proceed, I need clarification on:
|
||||
1. Incoming (vendor delivers goods) or Outgoing (vendor picks up goods)?
|
||||
2. Handle-only, Invoice-only, or combined Handle+Invoice in one run?
|
||||
3. Should the test use an existing vendor from the database or create one
|
||||
via LibraryPurchase.CreateVendor?
|
||||
Before I proceed, I need clarification on:
|
||||
1. Incoming (vendor delivers goods) or Outgoing (vendor picks up goods)?
|
||||
2. Handle-only, Invoice-only, or combined Handle+Invoice in one run?
|
||||
3. Should the test use an existing vendor from the database or create one
|
||||
via LibraryPurchase.CreateVendor?
|
||||
|
||||
I will not write any code until these are answered.
|
||||
```
|
||||
I will not write any code until these are answered.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [claude-md, agents, routing, visibility, setup]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [claude-md, agents, visibility, setup, mode-b, curabis-standard]
|
||||
|
|
@ -35,9 +43,7 @@ with a proposed addition before the session continues.
|
|||
|
||||
After running Mode B (or any agent install), compare:
|
||||
|
||||
```
|
||||
Get-ChildItem .github/.agents/*.agent.md | Select-Object -ExpandProperty BaseName
|
||||
```
|
||||
Get-ChildItem .github/.agents/*.agent.md | Select-Object -ExpandProperty BaseName
|
||||
|
||||
against the agent references in CLAUDE.md. Any filename present in the directory
|
||||
but absent from CLAUDE.md is a gap that must be surfaced.
|
||||
|
|
@ -46,13 +52,11 @@ but absent from CLAUDE.md is a gap that must be surfaced.
|
|||
|
||||
When a gap is found, output exactly this before continuing:
|
||||
|
||||
```
|
||||
⚠️ Ny agent installeret men ikke refereret i CLAUDE.md:
|
||||
⚠️ Ny agent installeret men ikke refereret i CLAUDE.md:
|
||||
|
||||
- <agent-navn>.agent.md
|
||||
- <agent-navn>.agent.md
|
||||
|
||||
Claude kan ikke kalde denne agent medmindre den tilføjes til CLAUDE.md.
|
||||
Vil du have mig til at tilføje den nu?
|
||||
```
|
||||
Claude kan ikke kalde denne agent medmindre den tilføjes til CLAUDE.md.
|
||||
Vil du have mig til at tilføje den nu?
|
||||
|
||||
Do not continue with other activity until the developer has responded.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [commit-message, bc-task, task-id, traceability, git]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
---
|
||||
name: commit-message-must-include-bc-task-id
|
||||
description: >
|
||||
|
|
@ -21,19 +29,15 @@ used across Curabis teams.
|
|||
|
||||
## Anti Pattern
|
||||
|
||||
```
|
||||
Add Price Lookup feature — FindPrice page, tier prices, currency conversion
|
||||
```
|
||||
Add Price Lookup feature — FindPrice page, tier prices, currency conversion
|
||||
|
||||
No traceability. Impossible to find the BC task from git history.
|
||||
|
||||
## Best Practice
|
||||
|
||||
```
|
||||
[#8738] Add Price Lookup feature — FindPrice page, tier prices, currency conversion
|
||||
[#8738] Add 22 UI tests for PRICING LOOKUP feature
|
||||
[#8738] Add translations, shared project memory and cspell config
|
||||
```
|
||||
[#8738] Add Price Lookup feature — FindPrice page, tier prices, currency conversion
|
||||
[#8738] Add 22 UI tests for PRICING LOOKUP feature
|
||||
[#8738] Add translations, shared project memory and cspell config
|
||||
|
||||
## The two task numbers — use taskId, not taskNo
|
||||
|
||||
|
|
@ -65,4 +69,4 @@ create-task workflow) or ask the project manager to register the work.
|
|||
## Scope
|
||||
|
||||
All commits that reach the main branch — feature, fix, test, chore, docs.
|
||||
Merge commits and auto-generated commits (renovate, al-go) are exempt.
|
||||
Merge commits and auto-generated commits (renovate, al-go) are exempt.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [dependency, source, add_repo, github, curabis, closed-source, test, symbol, black-box]
|
||||
keywords: [dependency, source, add-repo, github, curabis, closed-source, test, symbol, black-box]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
|
|
@ -49,26 +49,22 @@ the current project.
|
|||
|
||||
## Anti Pattern
|
||||
|
||||
```
|
||||
// WRONG: reverse-engineering the compiled symbol package instead of reading source
|
||||
// Agent parses SymbolReference.json from .alpackages/*.app to learn
|
||||
// Contract Management table fields and public procedure signatures.
|
||||
// Result: incomplete picture, missed validation logic, excluded feature from tests.
|
||||
```
|
||||
// WRONG: reverse-engineering the compiled symbol package instead of reading source
|
||||
// Agent parses SymbolReference.json from .alpackages/*.app to learn
|
||||
// Contract Management table fields and public procedure signatures.
|
||||
// Result: incomplete picture, missed validation logic, excluded feature from tests.
|
||||
|
||||
## Best Practice
|
||||
|
||||
```
|
||||
// CORRECT: add the source repo and read it directly
|
||||
add_repo Curabis/ContractMgmt365app
|
||||
// CORRECT: add the source repo and read it directly
|
||||
add_repo Curabis/ContractMgmt365app
|
||||
|
||||
// Then read the actual table definitions, codeunits, and any Test Library
|
||||
// codeunits that may already exist in the repo's own test app.
|
||||
// Then read the actual table definitions, codeunits, and any Test Library
|
||||
// codeunits that may already exist in the repo's own test app.
|
||||
|
||||
// If no Test Library exists in the dependency's test app:
|
||||
// build GIVEN helpers in the consuming project's own Test Library codeunit
|
||||
// based on the REAL table field definitions and trigger logic you can now read.
|
||||
```
|
||||
// If no Test Library exists in the dependency's test app:
|
||||
// build GIVEN helpers in the consuming project's own Test Library codeunit
|
||||
// based on the REAL table field definitions and trigger logic you can now read.
|
||||
|
||||
## When to apply this rule
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [permission-set, api-page, web-service, exposure, security]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# Exposed objects must be in at least one permission set
|
||||
|
||||
## Description
|
||||
|
||||
**Rule (CURABIS-ARCH-011):** Every *exposed* object in a CURABIS app must be a member of
|
||||
at least one permission set shipped by that app. "Exposed" means any object reachable from
|
||||
outside the app's own UI:
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
---
|
||||
name: feature-branch-must-merge-to-track-branch
|
||||
title: Feature branches must merge into the project's declared track branch
|
||||
category: architecture
|
||||
severity: required
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [git, feature-branch, track-branch, merge, workflow]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Feature branches must merge into the project's declared track branch
|
||||
|
||||
## Rule
|
||||
## Description
|
||||
|
||||
When a project declares a track branch in `CLAUDE.md`, all feature branches
|
||||
MUST merge into that track branch — not into `main` directly. `main` is
|
||||
|
|
@ -17,10 +19,8 @@ reserved for releases and hotfixes.
|
|||
|
||||
The track branch is declared once in `CLAUDE.md`:
|
||||
|
||||
```yaml
|
||||
# Declares the integration target for this development sprint/module
|
||||
trackBranch: purchase
|
||||
```
|
||||
# Declares the integration target for this development sprint/module
|
||||
trackBranch: purchase
|
||||
|
||||
If no `trackBranch` is declared, `main` is the default and feature branches
|
||||
merge there directly.
|
||||
|
|
@ -46,33 +46,27 @@ The rule protects the invariant: **`main` is deployable at any moment.**
|
|||
|
||||
## The branching model
|
||||
|
||||
```
|
||||
main
|
||||
└── <track-branch> (e.g. "purchase" — lives for one sprint/module)
|
||||
└── feature/<name> ← development happens here
|
||||
└── feature/<name>
|
||||
└── bugfix/<name>
|
||||
└── hotfix/<name> ← branches from main, merges back to main
|
||||
```
|
||||
main
|
||||
└── <track-branch> (e.g. "purchase" — lives for one sprint/module)
|
||||
└── feature/<name> ← development happens here
|
||||
└── feature/<name>
|
||||
└── bugfix/<name>
|
||||
└── hotfix/<name> ← branches from main, merges back to main
|
||||
|
||||
At release: track-branch → main (via PR, after full QA).
|
||||
|
||||
## Non-compliant
|
||||
|
||||
```bash
|
||||
# Merging a feature directly to main when a track branch is declared in CLAUDE.md
|
||||
git checkout main
|
||||
git merge feature/my-feature # violates rule
|
||||
```
|
||||
# Merging a feature directly to main when a track branch is declared in CLAUDE.md
|
||||
git checkout main
|
||||
git merge feature/my-feature # violates rule
|
||||
|
||||
## Compliant
|
||||
|
||||
```bash
|
||||
# Read track branch from CLAUDE.md → merge there
|
||||
git checkout purchase
|
||||
git merge --no-ff feature/my-feature
|
||||
# Then sync BC: gitHubDevStatus = "Done"
|
||||
```
|
||||
# Read track branch from CLAUDE.md → merge there
|
||||
git checkout purchase
|
||||
git merge --no-ff feature/my-feature
|
||||
# Then sync BC: gitHubDevStatus = "Done"
|
||||
|
||||
## Scope
|
||||
|
||||
|
|
|
|||
|
|
@ -48,9 +48,7 @@ whether Mode B executes for a given repository.
|
|||
|
||||
At session start, compare:
|
||||
|
||||
```
|
||||
Local per-repo marker: .github/.agents/.bcquality-version (if present)
|
||||
```
|
||||
Local per-repo marker: .github/.agents/.bcquality-version (if present)
|
||||
|
||||
against the current BCQuality main SHA. If they differ (or the local marker is
|
||||
missing), run Mode B reconciliation for this repository regardless of what the
|
||||
|
|
@ -60,10 +58,8 @@ global `~/.claude/.bcquality-version` file says.
|
|||
|
||||
When a per-repo reconciliation gap is found, output exactly this before continuing:
|
||||
|
||||
```
|
||||
⚠️ Dette repository er ikke reconciled mod seneste BCQuality-SHA, selvom den
|
||||
globale versions-fil allerede er opdateret (formentlig af et andet projekt).
|
||||
Kører Mode B-reconciliation for dette repo nu.
|
||||
```
|
||||
⚠️ Dette repository er ikke reconciled mod seneste BCQuality-SHA, selvom den
|
||||
globale versions-fil allerede er opdateret (formentlig af et andet projekt).
|
||||
Kører Mode B-reconciliation for dette repo nu.
|
||||
|
||||
Do not silently skip Mode B just because the global marker looks current.
|
||||
|
|
|
|||
|
|
@ -42,9 +42,7 @@ before the update is considered complete.
|
|||
After any Mode B run, compare the list of files in `curabis-standard.agent.md`'s
|
||||
Mode B template table against:
|
||||
|
||||
```
|
||||
Get-ChildItem .github/.agents/*.agent.md | Select-Object -ExpandProperty BaseName
|
||||
```
|
||||
Get-ChildItem .github/.agents/*.agent.md | Select-Object -ExpandProperty BaseName
|
||||
|
||||
Any template file present in the table but absent from the directory is a gap —
|
||||
install it, then surface it per `claude-md-must-reference-all-agents.md` if it
|
||||
|
|
@ -54,12 +52,10 @@ also needs a CLAUDE.md reference.
|
|||
|
||||
When a reconciliation gap is found, output exactly this before continuing:
|
||||
|
||||
```
|
||||
⚠️ Mode B kørte, men følgende template-fil(er) blev ikke installeret:
|
||||
⚠️ Mode B kørte, men følgende template-fil(er) blev ikke installeret:
|
||||
|
||||
- <agent-navn>.agent.md
|
||||
- <agent-navn>.agent.md
|
||||
|
||||
Vil du have mig til at installere den/dem nu?
|
||||
```
|
||||
Vil du have mig til at installere den/dem nu?
|
||||
|
||||
Do not continue with other activity until the developer has responded.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [namespace, verification, bcapps, source-of-truth]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# AL Language Namespace Verification Rule
|
||||
|
||||
## Core Requirement
|
||||
## Description
|
||||
|
||||
When adding variables or references to Business Central objects, agents must **verify namespaces by reading the actual source file**—not by inference or training data assumptions.
|
||||
|
||||
|
|
|
|||
|
|
@ -70,12 +70,10 @@ stale symbol cache issue — not a missing implementation.
|
|||
|
||||
When this situation occurs, output exactly this message before stopping:
|
||||
|
||||
```
|
||||
WARNING: VS Code needs a refresh before I can check for real compilation errors.
|
||||
WARNING: VS Code needs a refresh before I can check for real compilation errors.
|
||||
|
||||
Please run: Ctrl+Shift+P -> AL: Reload Extension
|
||||
Please run: Ctrl+Shift+P -> AL: Reload Extension
|
||||
|
||||
Let me know when the refresh is done and I will re-check diagnostics.
|
||||
```
|
||||
Let me know when the refresh is done and I will re-check diagnostics.
|
||||
|
||||
Do not continue with any other activity until the developer confirms the refresh.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [pages, business-logic, codeunit, separation-of-concerns]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# CURABIS Architecture: Page Presentation vs. Business Logic
|
||||
|
||||
## Core Rule
|
||||
## Description
|
||||
|
||||
In CURABIS codebases, pages serve exclusively as presentation layers. All business logic—including calculations, validations, and record modifications—must reside in codeunits, not in page triggers or actions. This standard is more rigorous than general Business Central guidance and applies uniformly across all CURABIS PTE applications.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [permission-set, least-privilege, tiers, security]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# CURABIS Architecture: Permission Sets Must Follow Least-Privilege Hierarchy
|
||||
|
||||
## Core Rule
|
||||
## Description
|
||||
|
||||
Permission sets in CURABIS apps must be structured in access tiers following the least-privilege principle. Tiers must be **additive** — each tier includes the one below it via `IncludedPermissionSets`. No single permission set should bundle user-level and administrative access in a flat structure.
|
||||
|
||||
|
|
@ -19,87 +27,69 @@ Permission sets in CURABIS apps must be structured in access tiers following the
|
|||
|
||||
## Implementation Pattern
|
||||
|
||||
```al
|
||||
permissionset 50100 "PM365 - View"
|
||||
{
|
||||
Access = Public;
|
||||
Assignable = true;
|
||||
Caption = 'Project Mgmt 365 - View';
|
||||
Permissions =
|
||||
tabledata "PM Project" = R,
|
||||
tabledata "PM Project Task" = R,
|
||||
page "PM Project List" = X,
|
||||
page "PM Project Card" = X;
|
||||
}
|
||||
permissionset 50100 "PM365 - View"
|
||||
{
|
||||
Access = Public;
|
||||
Assignable = true;
|
||||
Caption = 'Project Mgmt 365 - View';
|
||||
Permissions =
|
||||
tabledata "PM Project" = R,
|
||||
page "PM Project List" = X;
|
||||
}
|
||||
|
||||
permissionset 50101 "PM365 - Edit"
|
||||
{
|
||||
Access = Public;
|
||||
Assignable = true;
|
||||
Caption = 'Project Mgmt 365 - Edit';
|
||||
IncludedPermissionSets = "PM365 - View";
|
||||
Permissions =
|
||||
tabledata "PM Project" = RIMD,
|
||||
tabledata "PM Project Task" = RIMD,
|
||||
codeunit "PM Project Management" = X;
|
||||
}
|
||||
permissionset 50101 "PM365 - Edit"
|
||||
{
|
||||
Access = Public;
|
||||
Assignable = true;
|
||||
Caption = 'Project Mgmt 365 - Edit';
|
||||
IncludedPermissionSets = "PM365 - View";
|
||||
Permissions =
|
||||
tabledata "PM Project" = RIMD,
|
||||
tabledata "PM Project Task" = RIMD,
|
||||
codeunit "PM Project Management" = X;
|
||||
}
|
||||
|
||||
permissionset 50102 "PM365 - Admin"
|
||||
{
|
||||
Access = Public;
|
||||
Assignable = false;
|
||||
Caption = 'Project Mgmt 365 - Admin';
|
||||
IncludedPermissionSets = "PM365 - Edit";
|
||||
Permissions =
|
||||
tabledata "PM Setup" = RIMD,
|
||||
page "PM Setup" = X;
|
||||
}
|
||||
```
|
||||
permissionset 50102 "PM365 - Admin"
|
||||
{
|
||||
Access = Public;
|
||||
Assignable = false;
|
||||
Caption = 'Project Mgmt 365 - Admin';
|
||||
IncludedPermissionSets = "PM365 - Edit";
|
||||
Permissions =
|
||||
tabledata "PM Setup" = RIMD,
|
||||
page "PM Setup" = X;
|
||||
}
|
||||
|
||||
## Relationship to CURABIS-ARCH-011
|
||||
|
||||
This rule is a **companion to CURABIS-ARCH-011** (`exposed-objects-must-be-in-a-permission-set`):
|
||||
|
||||
- **CURABIS-ARCH-011**: Every exposed object *must exist* in at least one permission set
|
||||
- **This rule**: Permission sets *themselves* must follow the hierarchical least-privilege structure
|
||||
|
||||
Both must be satisfied simultaneously: it is not enough that objects appear in a permission set if that set grants excessive access.
|
||||
Companion to **CURABIS-ARCH-011** (`exposed-objects-must-be-in-a-permission-set`):
|
||||
ARCH-011 requires every exposed object to *exist* in a permission set; this rule
|
||||
requires the sets *themselves* to follow the tiered least-privilege structure.
|
||||
Both must hold — objects in a set that grants excessive access is not enough.
|
||||
|
||||
## Anti-Pattern
|
||||
|
||||
```al
|
||||
// Violation: flat "full access" set bundles user and admin access
|
||||
permissionset 50100 "PM365 - Full Access"
|
||||
{
|
||||
Assignable = true;
|
||||
Permissions =
|
||||
tabledata "PM Project" = RIMD,
|
||||
tabledata "PM Setup" = RIMD, // admin data mixed with user data
|
||||
tabledata "PM Project Task" = RIMD,
|
||||
codeunit "PM Post Codeunit" = X;
|
||||
}
|
||||
```
|
||||
// Violation: flat "full access" set bundles user and admin access
|
||||
permissionset 50100 "PM365 - Full Access"
|
||||
{
|
||||
Assignable = true;
|
||||
Permissions =
|
||||
tabledata "PM Project" = RIMD,
|
||||
tabledata "PM Setup" = RIMD, // admin data mixed with user data
|
||||
tabledata "PM Project Task" = RIMD,
|
||||
codeunit "PM Post Codeunit" = X;
|
||||
}
|
||||
|
||||
## BCApps Reference
|
||||
|
||||
BCApps Business Foundation defines exactly this tiered pattern:
|
||||
|
||||
```al
|
||||
// BusFoundEdit.PermissionSet.al
|
||||
permissionset 4 "Bus. Found. - Edit"
|
||||
{
|
||||
Access = Public;
|
||||
Assignable = true;
|
||||
Caption = 'Business Foundation - Edit';
|
||||
IncludedPermissionSets = "Bus. Found. - View";
|
||||
}
|
||||
```
|
||||
|
||||
Microsoft uses Admin, Edit, View, Obj, and Read tiers with `IncludedPermissionSets` throughout BCApps — never a single flat "full access" set.
|
||||
BCApps Business Foundation defines exactly this tiered pattern: Microsoft uses
|
||||
Admin, Edit, View, Obj, and Read tiers with `IncludedPermissionSets` throughout —
|
||||
never a single flat "full access" set. Each tier inherits from the tier below;
|
||||
Admin sets use `Assignable = false` to prevent accidental assignment to regular
|
||||
users.
|
||||
|
||||
- **Source:** https://github.com/microsoft/BCApps/tree/main/src/Business%20Foundation/App/Permissions
|
||||
- **Files:** `BusFoundAdmin`, `BusFoundEdit`, `BusFoundView`, `BusFoundObj`, `BusFoundRead`
|
||||
- **Pattern:** Each tier inherits from the tier below via `IncludedPermissionSets`. Admin sets use `Assignable = false` to prevent accidental assignment to regular users.
|
||||
|
||||
## Verification
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
---
|
||||
name: shared-project-memory-must-be-in-repo
|
||||
description: >
|
||||
Project-level memory (business rules, architectural decisions, scope boundaries)
|
||||
must be stored in a version-controlled projectmemory/ folder, not in a user's
|
||||
local Claude memory store, so all team members benefit from shared knowledge.
|
||||
layer: 2
|
||||
category: architecture
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [projectmemory, shared-memory, repo, team-knowledge]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Shared Project Memory Must Be in the Repository
|
||||
|
|
@ -22,39 +21,33 @@ a different machine. Version-controlled memory is shared, attributed, and persis
|
|||
|
||||
## Anti Pattern
|
||||
|
||||
```
|
||||
# Stored only on Michael's laptop — Tod and SJG never see this
|
||||
~/.claude/projects/d--MyProject/memory/project-pricing-vat-scope.md
|
||||
```
|
||||
# Stored only on Michael's laptop — Tod and SJG never see this
|
||||
~/.claude/projects/d--MyProject/memory/project-pricing-vat-scope.md
|
||||
|
||||
A rule observed by one developer stays siloed. The next session on another machine —
|
||||
or by another team member — starts from zero.
|
||||
|
||||
## Best Practice
|
||||
|
||||
```
|
||||
# In the git repository — committed, shared, visible to all
|
||||
projectmemory/
|
||||
memoryupdates_mid.md ← Michael's observations
|
||||
memoryupdates_tod.md ← Tod's observations
|
||||
memoryupdates_sjg.md ← SJG's observations
|
||||
```
|
||||
# In the git repository — committed, shared, visible to all
|
||||
projectmemory/
|
||||
memoryupdates_mid.md ← Michael's observations
|
||||
memoryupdates_tod.md ← Tod's observations
|
||||
memoryupdates_sjg.md ← SJG's observations
|
||||
|
||||
Each file is named after the user who triggered the observation. All files are read
|
||||
by every team member's Claude session at start, via an instruction in `CLAUDE.md`:
|
||||
|
||||
```markdown
|
||||
## Shared project memory
|
||||
## Shared project memory
|
||||
|
||||
At session start, read **all files** in `projectmemory/` — they contain shared
|
||||
project observations from all team members and are version-controlled in git.
|
||||
At session start, read **all files** in `projectmemory/` — they contain shared
|
||||
project observations from all team members and are version-controlled in git.
|
||||
|
||||
When you learn something project-relevant, write it to
|
||||
`projectmemory/memoryupdates_<username>.md` for the active user.
|
||||
When you learn something project-relevant, write it to
|
||||
`projectmemory/memoryupdates_<username>.md` for the active user.
|
||||
|
||||
User-specific preferences (tone, workflow habits) stay in the local
|
||||
`~/.claude/projects/.../memory/` folder as before.
|
||||
```
|
||||
User-specific preferences (tone, workflow habits) stay in the local
|
||||
`~/.claude/projects/.../memory/` folder as before.
|
||||
|
||||
## What belongs in projectmemory vs local memory
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [xliff, translation, xlf, caption, tooltip, enu, da-dk, de-de, no-nb, sv-se, de-at]
|
||||
keywords: [xliff, translation, xlf, caption, tooltip, enu, da-dk, de-de, no-nb, sv-se]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
|
|
@ -51,13 +51,11 @@ The following must remain in English in all locales:
|
|||
|
||||
## Trans-unit structure
|
||||
|
||||
```xml
|
||||
<trans-unit id="..." size-unit="char" translate="yes" xml:space="preserve">
|
||||
<source>Post</source>
|
||||
<target state="translated">Bogfør</target> ← da-DK example
|
||||
<note from="Developer" annotates="source" priority="2">Button caption</note>
|
||||
</trans-unit>
|
||||
```
|
||||
<trans-unit id="..." size-unit="char" translate="yes" xml:space="preserve">
|
||||
<source>Post</source>
|
||||
<target state="translated">Bogfør</target> ← da-DK example
|
||||
<note from="Developer" annotates="source" priority="2">Button caption</note>
|
||||
</trans-unit>
|
||||
|
||||
State must always be `translated` — never `needs-translation` or `new`.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [mcp, agent, business-process, status, write-scope]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# CURABIS MCP: Agents Must Not Write Business Process Status Fields
|
||||
|
||||
## Core Principle
|
||||
## Description
|
||||
|
||||
MCP agents must only write developer-managed tracking fields — never fields that drive business process workflows such as invoicing, approval, or time registration. Writing a business status field from an agent can block downstream operations for users working in Business Central.
|
||||
|
||||
|
|
@ -22,10 +30,8 @@ Developer tracking fields are independent of BC workflow. Business process statu
|
|||
|
||||
## Example Agent Instruction
|
||||
|
||||
```
|
||||
Write only gitHubDevStatus and gitHubBranch on tasks.
|
||||
Never write Status — it controls the invoicing workflow.
|
||||
```
|
||||
Write only gitHubDevStatus and gitHubBranch on tasks.
|
||||
Never write Status — it controls the invoicing workflow.
|
||||
|
||||
## Verification
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
---
|
||||
rule-id: CURABIS-MCP-007
|
||||
title: Agent must resolve developer identity from BC
|
||||
category: mcp
|
||||
severity: warning
|
||||
applies-to: [agent-files, bc-mcp]
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [mcp, s2s, developer-identity, users-api, bc]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Agent must resolve developer identity from BC
|
||||
|
||||
## Rule
|
||||
## Description
|
||||
|
||||
Agent files must not contain static employee-to-code mappings.
|
||||
Developer identity must always be resolved at runtime from the BC users tool (PAG6102903).
|
||||
|
|
@ -45,4 +45,4 @@ Static employee tables in agent files are forbidden:
|
|||
## Exceptions
|
||||
|
||||
None. If the users tool is temporarily unavailable, say so and stop -- do not fall back
|
||||
to a hardcoded table.
|
||||
to a hardcoded table.
|
||||
|
|
|
|||
|
|
@ -1,144 +1,95 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [ai-eval, scores, bc-table, hill-climbing, telemetry]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# CURABIS-MCP-008 — AI eval scores must be posted to the BC posting table
|
||||
|
||||
## Rule
|
||||
## Description
|
||||
|
||||
When an AI agent completes a hill climbing eval iteration on a BC sub-task, all
|
||||
resulting scores — compile result, test score, BCQuality score, F1 score, verdict,
|
||||
and model identity — must be posted to the `CUR Project AI Score` table in Business
|
||||
Central via the designated MCP tool (`bc_post_ai_score`).
|
||||
|
||||
Scores must **not** be stored as:
|
||||
- task comments
|
||||
- local files or agent memory
|
||||
- inline in agent files or knowledge files
|
||||
- any other location outside the BC posting table
|
||||
Central via the designated MCP tool (`bc_post_ai_score`). Scores must **not** be
|
||||
stored as task comments, local files, agent memory, inline in agent or knowledge
|
||||
files, or any other location outside the BC posting table.
|
||||
|
||||
## Why
|
||||
|
||||
The `CUR Project AI Score` table is a **posting table**: one immutable entry per
|
||||
iteration, with a clustered key on `Entry No.`. It is the single source of truth for
|
||||
hill climbing history on a sub-task.
|
||||
|
||||
Storing scores elsewhere breaks this guarantee:
|
||||
|
||||
| Alternate location | Problem |
|
||||
|---|---|
|
||||
| Task comment | 250-char limit, unstructured, not queryable, mixed with human notes |
|
||||
| Local file | Session-scoped, repo-specific, invisible to other agents and BC reporting |
|
||||
| Agent memory | Volatile, not persisted between sessions |
|
||||
| Hard-coded in agent file | Frozen at time of writing, violates CURABIS-MCP-007 pattern |
|
||||
|
||||
The BC posting table enables:
|
||||
1. Reporting across tasks and projects (MatchRate over time)
|
||||
2. The Court reviewing objective score data from Edison
|
||||
3. The orchestrator reading prior iterations via `bc_get_ai_scores` to decide verdict
|
||||
4. BC users seeing hill climbing progress directly on the sub-task
|
||||
iteration, clustered on `Entry No.` — the single source of truth for hill climbing
|
||||
history on a sub-task. Alternate locations all break that guarantee: task comments
|
||||
are 250-char, unstructured and unqueryable; local files are session- and
|
||||
repo-scoped; agent memory is volatile; scores hard-coded in agent files are frozen
|
||||
at time of writing. The BC table is what enables cross-project reporting, the
|
||||
Court reviewing Edison's score data, the orchestrator reading prior iterations via
|
||||
`bc_get_ai_scores`, and BC users seeing progress directly on the sub-task.
|
||||
|
||||
## Compliant
|
||||
|
||||
After each eval iteration, the orchestrator calls:
|
||||
|
||||
```
|
||||
bc_post_ai_score(
|
||||
projectNo = "DEV2026-00010",
|
||||
subTaskNo = "0014",
|
||||
iterationNo = 3,
|
||||
compile = true,
|
||||
testScore = 0.80,
|
||||
bcquality = 0.86,
|
||||
f1Score = 0.83,
|
||||
verdict = "Keep",
|
||||
model = "claude-sonnet-4-6"
|
||||
)
|
||||
```
|
||||
bc_post_ai_score(
|
||||
projectNo = "DEV2026-00010",
|
||||
subTaskNo = "0014",
|
||||
iterationNo = 3,
|
||||
compile = true,
|
||||
testScore = 0.80,
|
||||
bcquality = 0.86,
|
||||
f1Score = 0.83,
|
||||
verdict = "Keep",
|
||||
model = "claude-sonnet-4-6"
|
||||
)
|
||||
|
||||
BC sets `Eval DateTime` automatically. The orchestrator may additionally post a
|
||||
brief human-readable comment ("Iteration 3: F1=0.83 → Keep") — this is allowed,
|
||||
as it communicates progress; the score itself is in BC.
|
||||
BC sets `Eval DateTime` automatically. A brief human-readable comment in addition
|
||||
("Iteration 3: F1=0.83 → Keep") is allowed — the score itself is in BC.
|
||||
|
||||
## Non-compliant
|
||||
|
||||
```
|
||||
# Storing score as task comment only
|
||||
bc_add_comment(
|
||||
projectNo = "DEV2026-00010",
|
||||
subTaskNo = "0014",
|
||||
comment = "Iter 3: compile ✅ tests 4/5 BCQ 6/7 F1=0.83 Keep"
|
||||
)
|
||||
# → Score is unstructured text. Not queryable. Lost to reporting.
|
||||
```
|
||||
# Storing score as task comment only
|
||||
bc_add_comment(projectNo = "DEV2026-00010", subTaskNo = "0014",
|
||||
comment = "Iter 3: compile OK tests 4/5 BCQ 6/7 F1=0.83 Keep")
|
||||
# -> unstructured text; not queryable; lost to reporting
|
||||
|
||||
```
|
||||
# Storing score in agent file
|
||||
## Hill climbing log
|
||||
- Iteration 1: F1=0.43 Revert
|
||||
- Iteration 2: F1=0.71 Keep
|
||||
- Iteration 3: F1=0.83 Keep ← frozen, session-specific, wrong location
|
||||
```
|
||||
# Storing score in an agent file's "Hill climbing log" section
|
||||
# -> frozen, session-specific, wrong location
|
||||
|
||||
## False positive
|
||||
|
||||
An agent that posts a human-readable summary comment **in addition to** calling
|
||||
`bc_post_ai_score` is **not** violating this rule. The comment is human
|
||||
communication; the score is in BC. Both are permitted.
|
||||
|
||||
The violation is using the comment or any other location **instead of** posting to
|
||||
the BC table.
|
||||
Posting a human-readable summary comment **in addition to** calling
|
||||
`bc_post_ai_score` is not a violation. The violation is using the comment or any
|
||||
other location **instead of** the BC table.
|
||||
|
||||
## API reference
|
||||
|
||||
- Page: `CUR MCP Project AI Scores` (PAG6102906)
|
||||
- Entity: `projectAIScores`
|
||||
- Page: `CUR MCP Project AI Scores` (PAG6102906), entity `projectAIScores`
|
||||
- Publisher: `curabis`, Group: `projectMgmt`, Version: `v2.0`
|
||||
- Insert: allowed. Modify: never. Delete: never.
|
||||
- `Eval DateTime` is set by BC `OnInsertRecord` — do not pass it.
|
||||
|
||||
## Applies to
|
||||
|
||||
Agent files that implement hill climbing eval loops on BC sub-tasks.
|
||||
|
||||
## Eval at task boundaries (hill-climbing baseline and final)
|
||||
|
||||
To generate meaningful hill-climbing data, the project's eval script MUST be
|
||||
run at two specific moments per task:
|
||||
To generate meaningful hill-climbing data, the project's eval script MUST run at
|
||||
two moments per task:
|
||||
|
||||
| Moment | When | Verdict to post |
|
||||
|---|---|---|
|
||||
| **Baseline** | Before the first code change for a task | `"Baseline"` |
|
||||
| **Final** | After all changes are complete, before merging to track branch | `"Final"` |
|
||||
| **Final** | After all changes, before merging to track branch | `"Final"` |
|
||||
|
||||
The delta `Final.score - Baseline.score` is the quality impact of the task:
|
||||
The delta `Final.score - Baseline.score` is the task's quality impact: positive
|
||||
means improved quality; negative means technical debt was introduced (note it in
|
||||
the BC task comment); zero is neutral. Never skip the baseline "because the task
|
||||
is small" — without it the delta cannot be computed and history is incomplete.
|
||||
|
||||
- **Positive delta** -- the task improved code quality.
|
||||
- **Negative delta** -- technical debt was introduced; note it in the BC task comment.
|
||||
- **Zero or negligible delta** -- neutral; no action required.
|
||||
Each project declares its eval script in `CLAUDE.md`; that script emits the score
|
||||
posted via `bc_post_ai_score` and appends to the project's eval history.
|
||||
|
||||
### Project eval script
|
||||
## Applies to
|
||||
|
||||
Each project declares its eval script in `CLAUDE.md`. That script emits a score
|
||||
and appends to the project's eval history. The score posted to `bc_post_ai_score`
|
||||
is the score emitted by that project-specific script.
|
||||
|
||||
### Non-compliant
|
||||
|
||||
```
|
||||
# Skipping the baseline "because the task is small"
|
||||
# delta cannot be computed; hill-climbing history is incomplete
|
||||
```
|
||||
|
||||
### Compliant
|
||||
|
||||
```
|
||||
# Task start: run eval -> post baseline
|
||||
bc_post_ai_score(projectNo, subTaskNo, iterationNo, ..., verdict="Baseline")
|
||||
|
||||
# ... implement the task ...
|
||||
|
||||
# Task end (before merge): run eval -> post final
|
||||
bc_post_ai_score(projectNo, subTaskNo, iterationNo, ..., verdict="Final")
|
||||
```
|
||||
|
||||
### Scope
|
||||
|
||||
Applies to all tasks where the project has an eval script declared in `CLAUDE.md`.
|
||||
Documentation-only tasks (no code change) are exempt.
|
||||
Agent files implementing hill climbing eval loops on BC sub-tasks, and all tasks
|
||||
where the project declares an eval script in `CLAUDE.md`. Documentation-only
|
||||
tasks (no code change) are exempt.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [api-page, flowfield, calcfields, odata]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# CURABIS MCP: FlowFields on API Pages Rule Summary
|
||||
|
||||
## The Rule
|
||||
## Description
|
||||
**FlowFields on API pages must be explicitly calculated** via `CalcFields()` in the `OnAfterGetRecord` trigger, or they return empty values in OData responses.
|
||||
|
||||
## Key Points
|
||||
|
|
@ -15,12 +23,10 @@
|
|||
|
||||
The provided example demonstrates proper implementation:
|
||||
|
||||
```al
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
Rec.CalcFields("Elapsed time (Chargeable)", "Customer Name");
|
||||
end;
|
||||
```
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
Rec.CalcFields("Elapsed time (Chargeable)", "Customer Name");
|
||||
end;
|
||||
|
||||
## Verification Approach
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [api-page, key-fields, editable, insert, odata]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# CURABIS MCP: ODataKeyFields Editability Rule
|
||||
|
||||
## The Rule
|
||||
## Description
|
||||
|
||||
Key fields declared in `ODataKeyFields` cannot have `Editable = false` when the API page permits inserts and **the field is consumer-provided**. This restriction causes the OData layer to reject the field as an unknown property during POST operations.
|
||||
|
||||
|
|
@ -11,20 +19,16 @@ When a field is marked read-only, Business Central removes it from the OData wri
|
|||
## Problematic vs. Correct Approach
|
||||
|
||||
**Incorrect:**
|
||||
```al
|
||||
field(projectNo; Rec."Project No.")
|
||||
{
|
||||
Editable = false; // prevents API inserts when consumer must supply the value
|
||||
}
|
||||
```
|
||||
field(projectNo; Rec."Project No.")
|
||||
{
|
||||
Editable = false; // prevents API inserts when consumer must supply the value
|
||||
}
|
||||
|
||||
**Correct:**
|
||||
```al
|
||||
field(projectNo; Rec."Project No.")
|
||||
{
|
||||
// No Editable = false — consumer supplies this on POST
|
||||
}
|
||||
```
|
||||
field(projectNo; Rec."Project No.")
|
||||
{
|
||||
// No Editable = false — consumer supplies this on POST
|
||||
}
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [api-page, least-privilege, write-access, odata, security]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# CURABIS MCP: API Pages Must Use Least-Privilege Write Access
|
||||
|
||||
## Core Principle
|
||||
## Description
|
||||
|
||||
A general-purpose API page that exposes many fields should not be widened to allow writes on a single additional field. Instead, create a dedicated minimal API page that exposes only the fields the consumer needs to read and write. This limits the blast radius of any agent or integration mistake.
|
||||
|
||||
|
|
@ -10,26 +18,22 @@ An MCP agent operates with the permissions of its service identity, not an indiv
|
|||
|
||||
## Pattern to Avoid
|
||||
|
||||
```al
|
||||
// WRONG: General page widened with write access to one field
|
||||
// Now the agent can accidentally (or intentionally) write to all other fields too
|
||||
field(status; Rec.Status) { } // should be read-only
|
||||
field(gitHubRepository; Rec."GitHub Repository") { } // the one field we want writable
|
||||
field(estimatedHours; Rec."Estimated Hours") { } // should be read-only
|
||||
```
|
||||
// WRONG: General page widened with write access to one field
|
||||
// Now the agent can accidentally (or intentionally) write to all other fields too
|
||||
field(status; Rec.Status) { } // should be read-only
|
||||
field(gitHubRepository; Rec."GitHub Repository") { } // the one field we want writable
|
||||
field(estimatedHours; Rec."Estimated Hours") { } // should be read-only
|
||||
|
||||
## Correct Pattern
|
||||
|
||||
Create a separate, minimal API page:
|
||||
|
||||
```al
|
||||
page 6102904 "CUR MCP Project Repository"
|
||||
{
|
||||
// Only two fields: the key and the one writable field
|
||||
field(no; Rec."No.") { Editable = false; }
|
||||
field(gitHubRepository; Rec."GitHub Repository") { }
|
||||
}
|
||||
```
|
||||
page 6102904 "CUR MCP Project Repository"
|
||||
{
|
||||
// Only two fields: the key and the one writable field
|
||||
field(no; Rec."No.") { Editable = false; }
|
||||
field(gitHubRepository; Rec."GitHub Repository") { }
|
||||
}
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
---
|
||||
name: bc-mcp-find-active-task-for-branch
|
||||
description: >
|
||||
Standard recipe for finding the BC sub-task linked to the current git branch,
|
||||
including exact action names and field names for each BC MCP endpoint.
|
||||
layer: 2
|
||||
category: mcp
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [mcp, bc-task, branch, active-task, recipe]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# BC MCP: Find Active Task for Branch
|
||||
|
|
@ -38,16 +38,14 @@ Derived from the AL page source (EntityName property + PAG + page ID):
|
|||
|
||||
## Standard recipe: find task for current branch
|
||||
|
||||
```
|
||||
1. git branch --show-current → e.g. "PriceLookup"
|
||||
2. git remote get-url origin → e.g. "https://github.com/Curabis/Wareco.git"
|
||||
3. bc_actions_invoke List_project_PAG6102901
|
||||
filter: "gitHubRepository eq 'https://github.com/Curabis/Wareco.git'"
|
||||
→ get projectNo (e.g. "W-2024-001")
|
||||
4. bc_actions_invoke List_activeTask_PAG6102900
|
||||
filter: "projectNo eq 'W-2024-001' and gitHubBranch eq 'PriceLookup'"
|
||||
→ get taskId (global commit-message ID), taskNo, description, status
|
||||
```
|
||||
1. git branch --show-current → e.g. "PriceLookup"
|
||||
2. git remote get-url origin → e.g. "https://github.com/Curabis/Wareco.git"
|
||||
3. bc_actions_invoke List_project_PAG6102901
|
||||
filter: "gitHubRepository eq 'https://github.com/Curabis/Wareco.git'"
|
||||
→ get projectNo (e.g. "W-2024-001")
|
||||
4. bc_actions_invoke List_activeTask_PAG6102900
|
||||
filter: "projectNo eq 'W-2024-001' and gitHubBranch eq 'PriceLookup'"
|
||||
→ get taskId (global commit-message ID), taskNo, description, status
|
||||
|
||||
If step 3 returns no project, the repo is not linked — see `[[bc-mcp-link-repo-to-project]]`.
|
||||
If step 4 returns no task, the branch has no registered task — create one or ask the PM.
|
||||
|
|
@ -75,15 +73,13 @@ Never write `gitHubRepository` on the task (obsolete, will be removed in v29).
|
|||
|
||||
The bridge runs as app identity `BC_DevelopmentMCP`. To attribute work:
|
||||
|
||||
```
|
||||
1. git config user.email → developer's git email
|
||||
2. bc_actions_invoke List_consultant_PAG50009
|
||||
filter: "email eq 'mic.dieringer@gmail.com'"
|
||||
→ get employeeCode (e.g. "MID")
|
||||
3. Use employeeCode to filter "my tasks":
|
||||
List_activeTask_PAG6102900 filter: "taskResponsible eq 'MID'"
|
||||
4. Sign status comments: end with "— Michael" so attribution survives S2S
|
||||
```
|
||||
1. git config user.email → developer's git email
|
||||
2. bc_actions_invoke List_consultant_PAG50009
|
||||
filter: "email eq 'mic.dieringer@gmail.com'"
|
||||
→ get employeeCode (e.g. "MID")
|
||||
3. Use employeeCode to filter "my tasks":
|
||||
List_activeTask_PAG6102900 filter: "taskResponsible eq 'MID'"
|
||||
4. Sign status comments: end with "— Michael" so attribution survives S2S
|
||||
|
||||
Some developers use personal email for git but have a Curabis email as secondary
|
||||
on GitHub. If the git email doesn't match, try the `@curabis.dk` variant.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
---
|
||||
name: bc-mcp-scope-tasks-to-repository
|
||||
description: >
|
||||
When a developer asks for their open tasks, scope the result to the current
|
||||
git repository only. If no BC project is linked to the repo, raise it as a
|
||||
flag instead of returning all tasks.
|
||||
layer: 2
|
||||
category: mcp
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [mcp, bc-task, repository-scope, project]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# BC MCP: Scope Task Lists to the Current Repository
|
||||
|
||||
## Rule
|
||||
## Description
|
||||
|
||||
When a developer asks "what tasks do I have", "what are my open tasks", or any
|
||||
equivalent question about their work queue, **only return tasks that belong to
|
||||
|
|
@ -29,20 +28,18 @@ wrong project.
|
|||
|
||||
## Standard recipe
|
||||
|
||||
```
|
||||
1. git remote get-url origin
|
||||
→ e.g. "https://github.com/Curabis/Wareco.git"
|
||||
1. git remote get-url origin
|
||||
→ e.g. "https://github.com/Curabis/Wareco.git"
|
||||
|
||||
2. List_ProjectRepositories_PAG6102904
|
||||
filter: "gitHubRepository eq '<url>'"
|
||||
→ get projectNo(s) for this repo
|
||||
2. List_ProjectRepositories_PAG6102904
|
||||
filter: "gitHubRepository eq '<url>'"
|
||||
→ get projectNo(s) for this repo
|
||||
|
||||
3. IF no projects found → STOP and flag (see "No linked project" below)
|
||||
3. IF no projects found → STOP and flag (see "No linked project" below)
|
||||
|
||||
4. List_ActiveTasks_PAG6102900
|
||||
filter: "projectNo eq '<projectNo>' and taskResponsible eq '<employeeCode>'"
|
||||
→ return only tasks in this repo's project(s)
|
||||
```
|
||||
4. List_ActiveTasks_PAG6102900
|
||||
filter: "projectNo eq '<projectNo>' and taskResponsible eq '<employeeCode>'"
|
||||
→ return only tasks in this repo's project(s)
|
||||
|
||||
For developer identity (resolving `employeeCode` from git email), see
|
||||
`[[bc-mcp-find-active-task-for-branch]]`.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [mcp, tools, preload, session-start, bc]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
---
|
||||
rule: bc-mcp-tools-must-be-preloaded
|
||||
title: BC MCP tool schemas must be pre-loaded at session start
|
||||
|
|
@ -7,14 +15,12 @@ severity: required
|
|||
|
||||
# BC MCP tool schemas must be pre-loaded at session start
|
||||
|
||||
## Rule
|
||||
## Description
|
||||
|
||||
When the `bc-mcp.agent.md` agent is invoked, the very first action must be to load
|
||||
the BC MCP tool schemas via `ToolSearch` — before producing any user-visible output.
|
||||
|
||||
```
|
||||
ToolSearch query: select:mcp__businesscentral__bc_actions_search,mcp__businesscentral__bc_actions_invoke,mcp__businesscentral__bc_actions_describe
|
||||
```
|
||||
ToolSearch query: select:mcp__businesscentral__bc_actions_search,mcp__businesscentral__bc_actions_invoke,mcp__businesscentral__bc_actions_describe
|
||||
|
||||
This call must complete before the agent responds to the user.
|
||||
|
||||
|
|
@ -35,16 +41,14 @@ and eliminates mid-task delays entirely.
|
|||
|
||||
## Correct pattern
|
||||
|
||||
```
|
||||
# bc-mcp.agent.md session start
|
||||
# bc-mcp.agent.md session start
|
||||
|
||||
1. ToolSearch: select:mcp__businesscentral__bc_actions_search,
|
||||
mcp__businesscentral__bc_actions_invoke,
|
||||
mcp__businesscentral__bc_actions_describe
|
||||
2. [proceed with user request]
|
||||
```
|
||||
1. ToolSearch: select:mcp__businesscentral__bc_actions_search,
|
||||
mcp__businesscentral__bc_actions_invoke,
|
||||
mcp__businesscentral__bc_actions_describe
|
||||
2. [proceed with user request]
|
||||
|
||||
## Scope
|
||||
|
||||
Applies to every invocation of `bc-mcp.agent.md` in every CURABIS project that uses
|
||||
the Business Central MCP bridge (`bc-mcp-bridge.js`).
|
||||
the Business Central MCP bridge (`bc-mcp-bridge.js`).
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
---
|
||||
rule: CURABIS-BCMCP-008
|
||||
title: Git lifecycle must sync BC subtask dev status
|
||||
severity: warning
|
||||
domain: git, mcp, bc-integration
|
||||
applies-to: [feature branches, bugfix branches, hotfix branches]
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [git, lifecycle, bc-status, dev-status, sync]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Git lifecycle must sync BC subtask dev status
|
||||
|
||||
## Description
|
||||
|
||||
Every AL feature branch is linked to a BC subtask. The `gitHubDevStatus` and
|
||||
`gitHubBranch` fields on the subtask must reflect the real state of the branch
|
||||
at all times — automatically, without manual steps.
|
||||
|
||||
## Track branch
|
||||
|
||||
Each project declares its **track branch** in `CLAUDE.md` — the branch that is
|
||||
the merge target for all feature branches in the current development track:
|
||||
Each project declares its **track branch** in `CLAUDE.md` — the merge target for
|
||||
all feature branches in the current development track:
|
||||
|
||||
| Declaration in CLAUDE.md | Meaning |
|
||||
|---|---|
|
||||
|
|
@ -27,33 +30,16 @@ feature branch is merged into the track branch — not necessarily `main`.
|
|||
|
||||
## Branch naming convention
|
||||
|
||||
Branches must follow this pattern so automation can parse the BC task reference:
|
||||
Branches must follow this pattern so automation can parse the BC task reference —
|
||||
type is `feature`/`bugfix`/`hotfix`, projectNo matches `[A-Z]{2,4}\d{4}-\d{5}`,
|
||||
taskNo is a plain or zero-padded integer, description is optional:
|
||||
|
||||
```
|
||||
<type>/<projectNo>-<taskNo>[-optional-description]
|
||||
```
|
||||
<type>/<projectNo>-<taskNo>[-optional-description]
|
||||
|
||||
| Segment | Format | Example |
|
||||
| --- | --- | --- |
|
||||
| type | `feature`, `bugfix`, `hotfix` | `feature` |
|
||||
| projectNo | `[A-Z]{2,4}\d{4}-\d{5}` | `DEV2023-00027` |
|
||||
| taskNo | zero-padded or plain integer | `004` or `4` |
|
||||
| description | optional, hyphen-separated | `bc-agent-semantic-tools` |
|
||||
|
||||
**Valid examples:**
|
||||
```
|
||||
feature/DEV2023-00027-004-bc-agent-semantic-tools
|
||||
bugfix/DEV2023-00027-003-odata-string-key
|
||||
hotfix/DEV2023-00012-001-invoicing-crash
|
||||
feature/DEV2023-00027-4
|
||||
```
|
||||
|
||||
**Invalid (no automation):**
|
||||
```
|
||||
my-feature
|
||||
fix-thing
|
||||
DEV2023-00027
|
||||
```
|
||||
Valid: feature/DEV2023-00027-004-bc-agent-semantic-tools
|
||||
bugfix/DEV2023-00027-003-odata-string-key
|
||||
feature/DEV2023-00027-4
|
||||
Invalid: my-feature / fix-thing / DEV2023-00027 (no automation)
|
||||
|
||||
## Status mapping
|
||||
|
||||
|
|
@ -66,47 +52,34 @@ DEV2023-00027
|
|||
|
||||
## Automated implementation (git hooks)
|
||||
|
||||
Automation is provided by two git hooks in `.githooks/` (activated via
|
||||
`git config core.hooksPath .githooks`) that call
|
||||
`Scripts/Invoke-BCGitSync.ps1`:
|
||||
|
||||
- `post-checkout` — detects branch creation and branch abandonment
|
||||
- `post-commit` — detects commits/merges on the track branch
|
||||
|
||||
`Invoke-BCGitSync.ps1` calls the BC OData API directly (same credentials as
|
||||
`bc-agent.js`) and never blocks the git operation — all errors are swallowed
|
||||
with a warning.
|
||||
|
||||
Git hooks require that branch names follow the `<type>/<projectNo>-<taskNo>`
|
||||
naming convention. Branches that do not follow this format are ignored by hooks.
|
||||
Two git hooks in `.githooks/` (activated via `git config core.hooksPath .githooks`)
|
||||
call `Scripts/Invoke-BCGitSync.ps1`: `post-checkout` detects branch creation and
|
||||
abandonment; `post-commit` detects commits/merges on the track branch. The script
|
||||
calls the BC OData API directly (same credentials as `bc-agent.js`), never blocks
|
||||
the git operation, and ignores branches that do not follow the naming convention.
|
||||
|
||||
## Claude-driven synchronization
|
||||
|
||||
When Claude executes git operations, the git hooks may not fire — either because
|
||||
hooks are not configured, or because the branch name does not follow the
|
||||
`<type>/<projectNo>-<taskNo>` convention.
|
||||
|
||||
**Claude MUST call BC MCP explicitly at two points:**
|
||||
When Claude executes git operations, the hooks may not fire — hooks unconfigured,
|
||||
or branch name outside the convention. **Claude MUST call BC MCP explicitly at
|
||||
two points:**
|
||||
|
||||
| Moment | BC MCP action |
|
||||
|---|---|
|
||||
| Feature branch created | `gitHubDevStatus = "In Progress"`, `gitHubBranch = <branch>` |
|
||||
| Feature branch merged to track branch | `gitHubDevStatus = "Done"`, `gitHubBranch = <track-branch>` |
|
||||
|
||||
Steps:
|
||||
1. Find the active task using the recipe in `[[bc-mcp-find-active-task-for-branch]]`
|
||||
2. Call `Modify_activeTask_PAG6102900` with the two writable fields
|
||||
|
||||
This requirement applies regardless of branch naming format and regardless of
|
||||
whether git hooks are also active. If both run, there is no conflict — they write
|
||||
identical values.
|
||||
Steps: find the active task using the recipe in
|
||||
`[[bc-mcp-find-active-task-for-branch]]`, then call
|
||||
`Modify_activeTask_PAG6102900` with the two writable fields. This applies
|
||||
regardless of branch naming and regardless of whether hooks are also active —
|
||||
if both run they write identical values.
|
||||
|
||||
## Safety rules
|
||||
|
||||
CURABIS-BCMCP-008 The sync script NEVER writes BC subtask `status`
|
||||
(Created/Accepted/In progress/Finished/Invoiced). It only writes
|
||||
`gitHubDevStatus` and `gitHubBranch`. These are the only two fields
|
||||
the agent is allowed to modify (see CURABIS-BCMCP-001).
|
||||
`gitHubDevStatus` and `gitHubBranch` (see CURABIS-BCMCP-001).
|
||||
|
||||
CURABIS-BCMCP-009 The sync script exits 0 on all errors. It must never
|
||||
block a git commit, checkout, or merge. BC sync is best-effort.
|
||||
|
|
@ -117,7 +90,6 @@ CURABIS-BCMCP-010 Only tasks in `activeTasks` (status = Accepted or In progress)
|
|||
|
||||
## BCApps reference
|
||||
|
||||
Branch naming conventions and git workflow integration follow the patterns used
|
||||
in [microsoft/BCApps](https://github.com/microsoft/BCApps) — see
|
||||
`.github/CONTRIBUTING.md` for Microsoft's own conventions on feature branches
|
||||
and PR titles that reference work items.
|
||||
Branch naming and git workflow integration follow
|
||||
[microsoft/BCApps](https://github.com/microsoft/BCApps) conventions — see its
|
||||
`.github/CONTRIBUTING.md` for feature-branch and work-item-referencing patterns.
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
---
|
||||
rule: CURABIS-MCP-003
|
||||
title: MCP bridge JavaScript-filer skal gemmes uden UTF-8 BOM
|
||||
category: mcp
|
||||
severity: high
|
||||
tags: [mcp, encoding, node, bridge, windows]
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [mcp, bridge, encoding, utf-8, stdio]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CURABIS-MCP-003 — MCP bridge JavaScript-filer skal gemmes uden UTF-8 BOM
|
||||
|
||||
## Regel
|
||||
## Description
|
||||
|
||||
JavaScript-filer der fungerer som MCP bridge-scripts (fx `bc-mcp-bridge.js`) skal gemmes med UTF-8-enkodning **uden** BOM (Byte Order Mark). En UTF-8 BOM (0xEF 0xBB 0xBF) placeret foran shebang-linjen får Node.js til at crashe med `SyntaxError: Invalid or unexpected token`, og MCP-serveren starter aldrig — uden at producere en brugbar fejlbesked til udvikleren.
|
||||
|
||||
|
|
@ -25,28 +26,22 @@ BOM introduceres typisk på Windows via:
|
|||
|
||||
**Download og gem korrekt (uden BOM):**
|
||||
|
||||
```powershell
|
||||
$content = (Invoke-WebRequest -Uri $url -UseBasicParsing).Content
|
||||
[System.IO.File]::WriteAllText($destPath, $content, [System.Text.UTF8Encoding]::new($false))
|
||||
```
|
||||
$content = (Invoke-WebRequest -Uri $url -UseBasicParsing).Content
|
||||
[System.IO.File]::WriteAllText($destPath, $content, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
**Verifikation efter gem:**
|
||||
|
||||
```powershell
|
||||
$bytes = [System.IO.File]::ReadAllBytes($filePath)
|
||||
if ($bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
||||
throw "BOM detected in $filePath — file cannot be used as Node.js entry point"
|
||||
}
|
||||
```
|
||||
$bytes = [System.IO.File]::ReadAllBytes($filePath)
|
||||
if ($bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
||||
throw "BOM detected in $filePath — file cannot be used as Node.js entry point"
|
||||
}
|
||||
|
||||
**Strip af eksisterende BOM (remediation):**
|
||||
|
||||
```powershell
|
||||
$bytes = [System.IO.File]::ReadAllBytes($path)
|
||||
if ($bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
||||
[System.IO.File]::WriteAllBytes($path, $bytes[3..($bytes.Length - 1)])
|
||||
}
|
||||
```
|
||||
$bytes = [System.IO.File]::ReadAllBytes($path)
|
||||
if ($bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
||||
[System.IO.File]::WriteAllBytes($path, $bytes[3..($bytes.Length - 1)])
|
||||
}
|
||||
|
||||
## Hvad der IKKE må ske
|
||||
|
||||
|
|
@ -63,18 +58,14 @@ Setup scripts der installerer MCP bridge-filer (fx curabis-standard.agent.md) sk
|
|||
Symptom: MCP-server er konfigureret i `.mcp.json`, men eksponerer ingen tools i sessionen.
|
||||
|
||||
Diagnose:
|
||||
```powershell
|
||||
# Tjek første bytes
|
||||
$b = [System.IO.File]::ReadAllBytes("path\to\bridge.js")
|
||||
"0x{0:X2} 0x{1:X2} 0x{2:X2}" -f $b[0], $b[1], $b[2]
|
||||
# Hvis output er "0xEF 0xBB 0xBF" er BOM årsagen
|
||||
```
|
||||
# Tjek første bytes
|
||||
$b = [System.IO.File]::ReadAllBytes("path\to\bridge.js")
|
||||
"0x{0:X2} 0x{1:X2} 0x{2:X2}" -f $b[0], $b[1], $b[2]
|
||||
# Hvis output er "0xEF 0xBB 0xBF" er BOM årsagen
|
||||
|
||||
```bash
|
||||
# Kør bridge direkte og se om Node.js fejler
|
||||
node path/to/bridge.js 2>&1 | head -5
|
||||
```
|
||||
# Kør bridge direkte og se om Node.js fejler
|
||||
node path/to/bridge.js 2>&1 | head -5
|
||||
|
||||
## Evidens
|
||||
|
||||
Observeret i to separate projekter inden for én uge (2026-06-28). I begge tilfælde var BC MCP-tools utilgængelige i alle sessioner. Fejlen kræver manuel byte-inspektion at diagnosticere.
|
||||
Observeret i to separate projekter inden for én uge (2026-06-28). I begge tilfælde var BC MCP-tools utilgængelige i alle sessioner. Fejlen kræver manuel byte-inspektion at diagnosticere.
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
---
|
||||
rule: mcp-config-must-not-hardcode-developer-paths
|
||||
title: Shared MCP configuration must not hardcode developer-specific paths
|
||||
category: mcp
|
||||
severity: error
|
||||
version: 1
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [mcp-config, hardcoded-paths, userprofile, portability]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Shared MCP configuration must not hardcode developer-specific paths
|
||||
|
||||
## Rule
|
||||
## Description
|
||||
|
||||
A git-committed `.mcp.json` must not hardcode an absolute path that is specific to
|
||||
one developer's machine — a repository clone location on a particular drive or
|
||||
|
|
@ -39,20 +40,18 @@ Claude Code expands two forms of variable inside `.mcp.json`'s `command`, `args`
|
|||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"example": {
|
||||
"command": "powershell",
|
||||
"args": ["-File", "${CLAUDE_PROJECT_DIR:-.}\\.vscode\\launch-server.ps1"]
|
||||
},
|
||||
"bridge": {
|
||||
"command": "node",
|
||||
"args": ["${USERPROFILE}\\.claude\\bridge.js"]
|
||||
{
|
||||
"mcpServers": {
|
||||
"example": {
|
||||
"command": "powershell",
|
||||
"args": ["-File", "${CLAUDE_PROJECT_DIR:-.}\\.vscode\\launch-server.ps1"]
|
||||
},
|
||||
"bridge": {
|
||||
"command": "node",
|
||||
"args": ["${USERPROFILE}\\.claude\\bridge.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What NOT to do
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
---
|
||||
rule: mcp-server-must-be-verified-at-session-start
|
||||
title: MCP server availability must be verified at session start
|
||||
category: mcp
|
||||
severity: error
|
||||
version: 1
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [mcp-server, verification, session-start]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# MCP server availability must be verified at session start
|
||||
|
||||
## Rule
|
||||
## Description
|
||||
|
||||
When an MCP server is configured in `.mcp.json`, the agent must at session start verify
|
||||
that the server's tools appear in the active deferred-tools list. If they are missing,
|
||||
|
|
@ -52,4 +53,4 @@ At session start, before using any MCP-dependent tool:
|
|||
|
||||
## Applies to
|
||||
|
||||
All CURABIS projects that configure MCP servers in `.mcp.json`.
|
||||
All CURABIS projects that configure MCP servers in `.mcp.json`.
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
---
|
||||
rule: mcp-tool-invocation-must-be-documented
|
||||
title: MCP tool documentation must include the invocation model
|
||||
category: mcp
|
||||
severity: warning
|
||||
version: 1
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [mcp, tool-invocation, documentation, transparency]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# MCP tool documentation must include the invocation model
|
||||
|
||||
## Rule
|
||||
## Description
|
||||
|
||||
An MCP agent's documentation must describe the actual invocation model — including
|
||||
whether a tool call is direct or wrapped via a generic action tool with a parameter value.
|
||||
|
|
@ -47,4 +48,4 @@ value passed to `bc_actions_invoke`, this documentation will cause agents to fai
|
|||
|
||||
## Applies to
|
||||
|
||||
Any CURABIS agent documentation that describes how to use an MCP tool.
|
||||
Any CURABIS agent documentation that describes how to use an MCP tool.
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [api-page, derived-fields, exposure, odata]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# CURABIS MCP: Stored Derived Fields Must Be Recalculated in OnAfterGetRecord
|
||||
|
||||
## Core Principle
|
||||
## Description
|
||||
|
||||
A stored field whose value is derived from other fields via `OnValidate` triggers can be stale. When the source data changes (e.g., new time entries posted), the stored derived field is not updated automatically — it only recalculates when a specific trigger fires. Exposing such a field directly via an API page returns a value that may be hours, days, or weeks out of date.
|
||||
|
||||
## Pattern to Avoid
|
||||
|
||||
```al
|
||||
// WRONG: Exposes the stored snapshot — may be stale
|
||||
field(timeLeft; Rec."Time left") { }
|
||||
```
|
||||
// WRONG: Exposes the stored snapshot — may be stale
|
||||
field(timeLeft; Rec."Time left") { }
|
||||
|
||||
`"Time left"` is recalculated only when `"Estimated time"` is validated. If new time entries are posted, the stored value does not update.
|
||||
|
||||
|
|
@ -17,20 +23,18 @@ field(timeLeft; Rec."Time left") { }
|
|||
|
||||
Recalculate in `OnAfterGetRecord` using a page variable:
|
||||
|
||||
```al
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
Rec.CalcFields("Elapsed time (Chargeable)");
|
||||
TimeLeftCalc := Rec."Estimated time" - Rec."Elapsed time (Chargeable)";
|
||||
end;
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
Rec.CalcFields("Elapsed time (Chargeable)");
|
||||
TimeLeftCalc := Rec."Estimated time" - Rec."Elapsed time (Chargeable)";
|
||||
end;
|
||||
|
||||
var
|
||||
TimeLeftCalc: Decimal;
|
||||
var
|
||||
TimeLeftCalc: Decimal;
|
||||
|
||||
// In layout:
|
||||
field(timeLeft; TimeLeftCalc) { } // live value
|
||||
field(elapsedTime; Rec."Elapsed time (Chargeable)") { } // source FlowField
|
||||
```
|
||||
// In layout:
|
||||
field(timeLeft; TimeLeftCalc) { } // live value
|
||||
field(elapsedTime; Rec."Elapsed time (Chargeable)") { } // source FlowField
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
---
|
||||
id: CURABIS-MCP-SHEBANG-001
|
||||
title: Shebang-integritet ved deploy af script-filer
|
||||
category: mcp
|
||||
severity: error
|
||||
applies-to: [claude-code, windows, mcp-setup]
|
||||
bc-version: [all]
|
||||
domain: mcp
|
||||
keywords: [write, shebang, script-integrity, encoding]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Shebang-integritet ved deploy af script-filer
|
||||
|
||||
## Regel
|
||||
## Description
|
||||
|
||||
Når en script-fil med shebang-linje (`.js`, `.sh`, `.ps1`) skrives via Claude Codes
|
||||
`Write`-værktøj på Windows, skal linje 1 i den deployede fil verificeres umiddelbart
|
||||
|
|
@ -16,9 +17,7 @@ efter skrivning.
|
|||
|
||||
Den verificerede linje skal matche den forventede shebang præcist, f.eks.:
|
||||
|
||||
```
|
||||
#!/usr/bin/env node
|
||||
```
|
||||
#!/usr/bin/env node
|
||||
|
||||
## Baggrund
|
||||
|
||||
|
|
@ -39,11 +38,9 @@ Brugeren ser ingen fejlbesked i Claude Code — kaldet afvises blot.
|
|||
|
||||
## Verifikation (påkrævet efter enhver write af script-fil)
|
||||
|
||||
```python
|
||||
with open(deployed_path, "r", encoding="utf-8") as f:
|
||||
line1 = f.readline().rstrip()
|
||||
assert line1 == expected_shebang, f"Shebang fejl: forventet {expected_shebang!r}, fik {line1!r}"
|
||||
```
|
||||
with open(deployed_path, "r", encoding="utf-8") as f:
|
||||
line1 = f.readline().rstrip()
|
||||
assert line1 == expected_shebang, f"Shebang fejl: forventet {expected_shebang!r}, fik {line1!r}"
|
||||
|
||||
Alternativt med Read-værktøjet: læs linje 1 og sammenlign med forventet shebang.
|
||||
Stop setup-processen og genskriv filen hvis de ikke matcher.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: testing
|
||||
keywords: [bcpt, performance-test, scenarios, app-specific]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# CURABIS Testing: BCPT Scenarios Must Be App-Specific
|
||||
|
||||
## Core Rule
|
||||
## Description
|
||||
|
||||
A PerformanceTest app must include BCPT scenario codeunits that exercise the **host app's own business flows** — not only the generic Microsoft scenarios (sales orders, purchase orders, GL entries). Generic scenarios measure BC's baseline performance; app-specific scenarios are the only way to detect performance regressions in the extension's own code.
|
||||
|
||||
|
|
@ -19,51 +27,49 @@ For every major business flow in the host app, create a corresponding `BCPT*` co
|
|||
|
||||
## Example: Project Management App
|
||||
|
||||
```al
|
||||
codeunit 80100 "BCPT Create Project" implements "BCPT Test Param. Provider"
|
||||
{
|
||||
SingleInstance = true;
|
||||
codeunit 80100 "BCPT Create Project" implements "BCPT Test Param. Provider"
|
||||
{
|
||||
SingleInstance = true;
|
||||
|
||||
trigger OnRun()
|
||||
begin
|
||||
if not IsInitialized then begin
|
||||
InitTest();
|
||||
IsInitialized := true;
|
||||
trigger OnRun()
|
||||
begin
|
||||
if not IsInitialized then begin
|
||||
InitTest();
|
||||
IsInitialized := true;
|
||||
end;
|
||||
CreateProject(GlobalBCPTTestContext);
|
||||
end;
|
||||
CreateProject(GlobalBCPTTestContext);
|
||||
end;
|
||||
|
||||
var
|
||||
GlobalBCPTTestContext: Codeunit "BCPT Test Context";
|
||||
IsInitialized: Boolean;
|
||||
var
|
||||
GlobalBCPTTestContext: Codeunit "BCPT Test Context";
|
||||
IsInitialized: Boolean;
|
||||
|
||||
local procedure InitTest()
|
||||
begin
|
||||
// Set up any required BC configuration
|
||||
end;
|
||||
local procedure InitTest()
|
||||
begin
|
||||
// Set up any required BC configuration
|
||||
end;
|
||||
|
||||
local procedure CreateProject(var BCPTTestContext: Codeunit "BCPT Test Context")
|
||||
begin
|
||||
BCPTTestContext.StartScenario('Create Project Header');
|
||||
// ... create project
|
||||
BCPTTestContext.EndScenario('Create Project Header');
|
||||
BCPTTestContext.UserWait();
|
||||
local procedure CreateProject(var BCPTTestContext: Codeunit "BCPT Test Context")
|
||||
begin
|
||||
BCPTTestContext.StartScenario('Create Project Header');
|
||||
// ... create project
|
||||
BCPTTestContext.EndScenario('Create Project Header');
|
||||
BCPTTestContext.UserWait();
|
||||
|
||||
BCPTTestContext.StartScenario('Add Project Task');
|
||||
// ... add task
|
||||
BCPTTestContext.EndScenario('Add Project Task');
|
||||
end;
|
||||
BCPTTestContext.StartScenario('Add Project Task');
|
||||
// ... add task
|
||||
BCPTTestContext.EndScenario('Add Project Task');
|
||||
end;
|
||||
|
||||
procedure GetDefaultParameters(): Text[1000]
|
||||
begin
|
||||
exit('');
|
||||
end;
|
||||
procedure GetDefaultParameters(): Text[1000]
|
||||
begin
|
||||
exit('');
|
||||
end;
|
||||
|
||||
procedure ValidateParameters(Parameters: Text[1000])
|
||||
begin
|
||||
end;
|
||||
}
|
||||
```
|
||||
procedure ValidateParameters(Parameters: Text[1000])
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
||||
## Suggested Scenarios for Project Management Apps
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: testing
|
||||
keywords: [testing, test-data, random, library, any]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# CURABIS Test Data Guidelines
|
||||
|
||||
## Core Principle
|
||||
## Description
|
||||
|
||||
"CURABIS tests assume an empty database. All test data must be created programmatically — never assume existing records or hardcode codes, numbers, or names that may or may not exist in a given environment."
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: testing
|
||||
keywords: [test, feature, scenario, given, when, then, tags, bdd, atdd, comments, structure]
|
||||
keywords: [test, feature, scenario, given, when, then, tags, bdd, atdd, comments]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
|
|
@ -31,69 +31,52 @@ what is covered without reading AL.
|
|||
|
||||
## Anti Pattern
|
||||
|
||||
```al
|
||||
// WRONG: no structure — tests as anonymous procedures
|
||||
codeunit 99006 "Find Price Testing"
|
||||
{
|
||||
Subtype = Test;
|
||||
// WRONG: no structure — tests as anonymous procedures
|
||||
codeunit 99006 "Find Price Testing"
|
||||
{
|
||||
Subtype = Test;
|
||||
|
||||
[Test]
|
||||
procedure Test1() // what does this test?
|
||||
begin
|
||||
// setup mixed with assertions, no clear layers
|
||||
Customer.Insert(false);
|
||||
FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', Price, Disc);
|
||||
Assert.AreEqual(100, Price, '');
|
||||
end;
|
||||
}
|
||||
```
|
||||
[Test]
|
||||
procedure Test1() // what does this test?
|
||||
begin
|
||||
// setup mixed with assertions, no clear layers
|
||||
Customer.Insert(false);
|
||||
FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', Price, Disc);
|
||||
Assert.AreEqual(100, Price, '');
|
||||
end;
|
||||
}
|
||||
|
||||
## Best Practice
|
||||
|
||||
```al
|
||||
// [FEATURE] Find Price — price cascade (Customer → Price Group → All Customers)
|
||||
codeunit 99006 "Find Price Testing"
|
||||
{
|
||||
Subtype = Test;
|
||||
// [FEATURE] Find Price — price cascade (Customer → Price Group → All Customers)
|
||||
codeunit 99006 "Find Price Testing"
|
||||
{
|
||||
Subtype = Test;
|
||||
|
||||
var
|
||||
WarecoLib: Codeunit "Wareco Test Library";
|
||||
Assert: Codeunit "Library Assert";
|
||||
var
|
||||
WarecoLib: Codeunit "Wareco Test Library";
|
||||
Assert: Codeunit "Library Assert";
|
||||
|
||||
// [SCENARIO] Customer with a specific price list line gets that unit price
|
||||
[Test]
|
||||
procedure GetPrice_CustomerPrice_ReturnsUnitPrice()
|
||||
var
|
||||
Customer: Record Customer;
|
||||
Item: Record Item;
|
||||
UnitPrice, LineDiscPct: Decimal;
|
||||
begin
|
||||
// [GIVEN] a customer with a price list line at 100 LCY
|
||||
WarecoLib.GivenCustomerWithPrice(Customer, Item, '', 100);
|
||||
// [WHEN]
|
||||
FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct);
|
||||
// [THEN]
|
||||
Assert.AreEqual(100, UnitPrice, 'Unit price must match customer price list');
|
||||
end;
|
||||
// [SCENARIO] Customer with a specific price list line gets that unit price
|
||||
[Test]
|
||||
procedure GetPrice_CustomerPrice_ReturnsUnitPrice()
|
||||
var
|
||||
Customer: Record Customer;
|
||||
Item: Record Item;
|
||||
UnitPrice, LineDiscPct: Decimal;
|
||||
begin
|
||||
// [GIVEN] a customer with a price list line at 100 LCY
|
||||
WarecoLib.GivenCustomerWithPrice(Customer, Item, '', 100);
|
||||
// [WHEN]
|
||||
FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct);
|
||||
// [THEN]
|
||||
Assert.AreEqual(100, UnitPrice, 'Unit price must match customer price list');
|
||||
end;
|
||||
}
|
||||
|
||||
// [SCENARIO] Customer with no price list line falls back to item unit price
|
||||
[Test]
|
||||
procedure GetPrice_NoCustomerPrice_FallsBackToItemPrice()
|
||||
var
|
||||
Customer: Record Customer;
|
||||
Item: Record Item;
|
||||
UnitPrice, LineDiscPct: Decimal;
|
||||
begin
|
||||
// [GIVEN] a customer with no price list, item priced at 200
|
||||
WarecoLib.GivenCustomer(Customer);
|
||||
WarecoLib.GivenItem(Item, 200);
|
||||
// [WHEN]
|
||||
FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct);
|
||||
// [THEN]
|
||||
Assert.AreEqual(200, UnitPrice, 'Must fall back to item unit price');
|
||||
end;
|
||||
}
|
||||
```
|
||||
Every further `[Test]` procedure in the codeunit repeats the same pattern: its
|
||||
own `[SCENARIO]` comment above the attribute, and `[GIVEN]`/`[WHEN]`/`[THEN]`
|
||||
layers inside the body.
|
||||
|
||||
## Relationship to procedure naming
|
||||
|
||||
|
|
|
|||
|
|
@ -25,55 +25,51 @@ belongs in `[WHEN]`.
|
|||
|
||||
## Anti Pattern
|
||||
|
||||
```al
|
||||
// WRONG: two actions in one test
|
||||
[Test]
|
||||
procedure GetPrice_ThenGetDiscount_ReturnsCorrectValues()
|
||||
var
|
||||
UnitPrice, LineDiscPct: Decimal;
|
||||
begin
|
||||
// [GIVEN] ...
|
||||
// [WHEN] first action
|
||||
FindPriceMgt.GetSalesPrice(CustomerNo, ItemNo, '', UnitPrice, LineDiscPct);
|
||||
// [WHEN] second action — this is a second test in disguise
|
||||
FindPriceMgt.GetSalesPriceTiers(CustomerNo, ItemNo, '', TempBuffer);
|
||||
// [THEN] asserting two unrelated things
|
||||
Assert.AreEqual(100, UnitPrice, '');
|
||||
Assert.IsFalse(TempBuffer.IsEmpty(), '');
|
||||
end;
|
||||
```
|
||||
// WRONG: two actions in one test
|
||||
[Test]
|
||||
procedure GetPrice_ThenGetDiscount_ReturnsCorrectValues()
|
||||
var
|
||||
UnitPrice, LineDiscPct: Decimal;
|
||||
begin
|
||||
// [GIVEN] ...
|
||||
// [WHEN] first action
|
||||
FindPriceMgt.GetSalesPrice(CustomerNo, ItemNo, '', UnitPrice, LineDiscPct);
|
||||
// [WHEN] second action — this is a second test in disguise
|
||||
FindPriceMgt.GetSalesPriceTiers(CustomerNo, ItemNo, '', TempBuffer);
|
||||
// [THEN] asserting two unrelated things
|
||||
Assert.AreEqual(100, UnitPrice, '');
|
||||
Assert.IsFalse(TempBuffer.IsEmpty(), '');
|
||||
end;
|
||||
|
||||
## Best Practice
|
||||
|
||||
```al
|
||||
// CORRECT: split into two focused tests
|
||||
// CORRECT: split into two focused tests
|
||||
|
||||
[Test]
|
||||
procedure GetPrice_CustomerPrice_ReturnsCorrectUnitPrice()
|
||||
var
|
||||
UnitPrice, LineDiscPct: Decimal;
|
||||
begin
|
||||
// [GIVEN] a customer with a price list line at 100
|
||||
WarecoLib.GivenCustomerWithPrice(Customer, Item, '', 100);
|
||||
// [WHEN]
|
||||
FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct);
|
||||
// [THEN]
|
||||
Assert.AreEqual(100, UnitPrice, 'Unit price must match price list');
|
||||
end;
|
||||
[Test]
|
||||
procedure GetPrice_CustomerPrice_ReturnsCorrectUnitPrice()
|
||||
var
|
||||
UnitPrice, LineDiscPct: Decimal;
|
||||
begin
|
||||
// [GIVEN] a customer with a price list line at 100
|
||||
WarecoLib.GivenCustomerWithPrice(Customer, Item, '', 100);
|
||||
// [WHEN]
|
||||
FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct);
|
||||
// [THEN]
|
||||
Assert.AreEqual(100, UnitPrice, 'Unit price must match price list');
|
||||
end;
|
||||
|
||||
[Test]
|
||||
procedure GetPriceTiers_CustomerTier_ReturnsOneTierLine()
|
||||
var
|
||||
TempBuffer: Record "Find Price Tier Buffer" temporary;
|
||||
begin
|
||||
// [GIVEN] a customer with a tier price at min qty 10
|
||||
WarecoLib.GivenCustomerWithTierPrice(Customer, Item, '', 10, 90);
|
||||
// [WHEN]
|
||||
FindPriceMgt.GetSalesPriceTiers(Customer."No.", Item."No.", '', TempBuffer);
|
||||
// [THEN]
|
||||
Assert.AreEqual(1, TempBuffer.Count(), 'Exactly one tier line expected');
|
||||
end;
|
||||
```
|
||||
[Test]
|
||||
procedure GetPriceTiers_CustomerTier_ReturnsOneTierLine()
|
||||
var
|
||||
TempBuffer: Record "Find Price Tier Buffer" temporary;
|
||||
begin
|
||||
// [GIVEN] a customer with a tier price at min qty 10
|
||||
WarecoLib.GivenCustomerWithTierPrice(Customer, Item, '', 10, 90);
|
||||
// [WHEN]
|
||||
FindPriceMgt.GetSalesPriceTiers(Customer."No.", Item."No.", '', TempBuffer);
|
||||
// [THEN]
|
||||
Assert.AreEqual(1, TempBuffer.Count(), 'Exactly one tier line expected');
|
||||
end;
|
||||
|
||||
## Naming implication
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: testing
|
||||
keywords: [testing, test-setup, library-codeunit, initialization]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# CURABIS Test Library Standards
|
||||
|
||||
## Core Rules
|
||||
## Description
|
||||
|
||||
The documentation establishes three critical testing practices for CURABIS AL applications:
|
||||
|
||||
|
|
|
|||
|
|
@ -32,37 +32,31 @@ Writing a test that adapts to existing code is **not** the same as writing a
|
|||
test that accepts wrong behaviour silently. If the production code contains a
|
||||
bug that contradicts the business specification, flag it explicitly:
|
||||
|
||||
```
|
||||
// ⚠️ NOTE: This assertion reflects current code behaviour.
|
||||
// Business spec says 1792,00 but code currently produces 1800,00.
|
||||
// Flagged for review — do not merge until resolved.
|
||||
```
|
||||
// ⚠️ NOTE: This assertion reflects current code behaviour.
|
||||
// Business spec says 1792,00 but code currently produces 1800,00.
|
||||
// Flagged for review — do not merge until resolved.
|
||||
|
||||
Never silently adjust an assertion to make a test green when the discrepancy
|
||||
is a real business logic error.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
```al
|
||||
// WRONG: Writing the "ideal" test without reading the production code,
|
||||
// then leaving it failing and saying "the code needs to be fixed"
|
||||
[THEN]
|
||||
Assert.AreEqual(1792, ActualAmount, 'Total should be 1792');
|
||||
// Test fails. Agent says: "You need to fix SVPost to produce 1792."
|
||||
// This is not what was asked for.
|
||||
```
|
||||
// WRONG: Writing the "ideal" test without reading the production code,
|
||||
// then leaving it failing and saying "the code needs to be fixed"
|
||||
[THEN]
|
||||
Assert.AreEqual(1792, ActualAmount, 'Total should be 1792');
|
||||
// Test fails. Agent says: "You need to fix SVPost to produce 1792."
|
||||
// This is not what was asked for.
|
||||
|
||||
## Best Practice
|
||||
|
||||
```al
|
||||
// CORRECT: Read SVPost, understand what it produces, write the test to match.
|
||||
// If the number is 1792 in both spec and code → assert 1792.
|
||||
// If the number differs → flag it, don't silently change it.
|
||||
// CORRECT: Read SVPost, understand what it produces, write the test to match.
|
||||
// If the number is 1792 in both spec and code → assert 1792.
|
||||
// If the number differs → flag it, don't silently change it.
|
||||
|
||||
// [GIVEN] Read SVPost.Codeunit.al and SV Test Library before writing assertions.
|
||||
// [THEN] Assert what the code actually produces, verified by reading the source.
|
||||
Assert.AreEqual(ExpectedAmount, ActualAmount, 'Net payout to vendor must match');
|
||||
```
|
||||
// [GIVEN] Read SVPost.Codeunit.al and SV Test Library before writing assertions.
|
||||
// [THEN] Assert what the code actually produces, verified by reading the source.
|
||||
Assert.AreEqual(ExpectedAmount, ActualAmount, 'Net payout to vendor must match');
|
||||
|
||||
## Workflow when asked to write a passing test
|
||||
|
||||
|
|
|
|||
|
|
@ -30,71 +30,65 @@ in the same codeunit.
|
|||
|
||||
## Anti Pattern
|
||||
|
||||
```al
|
||||
// WRONG: UI test codeunit without _UT suffix
|
||||
codeunit 99007 "Find Price Page Testing"
|
||||
{
|
||||
Subtype = Test;
|
||||
// contains TestPage calls — should be named "Find Price Testing_UT"
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
```al
|
||||
// WRONG: mixing direct codeunit calls and TestPage calls in the same codeunit
|
||||
codeunit 99007 "Find Price Testing"
|
||||
{
|
||||
Subtype = Test;
|
||||
|
||||
[Test]
|
||||
procedure GetPrice_LogicTest() // logic test — fine here
|
||||
begin
|
||||
FindPriceMgt.GetSalesPrice(...);
|
||||
end;
|
||||
|
||||
[Test]
|
||||
procedure Page_ShowsPrice_UT() // UI test — belongs in separate _UT codeunit
|
||||
var
|
||||
FindPricePage: TestPage "Find Price";
|
||||
begin
|
||||
FindPricePage.OpenNew();
|
||||
// WRONG: UI test codeunit without _UT suffix
|
||||
codeunit 99007 "Find Price Page Testing"
|
||||
{
|
||||
Subtype = Test;
|
||||
// contains TestPage calls — should be named "Find Price Testing_UT"
|
||||
...
|
||||
end;
|
||||
}
|
||||
```
|
||||
}
|
||||
|
||||
// WRONG: mixing direct codeunit calls and TestPage calls in the same codeunit
|
||||
codeunit 99007 "Find Price Testing"
|
||||
{
|
||||
Subtype = Test;
|
||||
|
||||
[Test]
|
||||
procedure GetPrice_LogicTest() // logic test — fine here
|
||||
begin
|
||||
FindPriceMgt.GetSalesPrice(...);
|
||||
end;
|
||||
|
||||
[Test]
|
||||
procedure Page_ShowsPrice_UT() // UI test — belongs in separate _UT codeunit
|
||||
var
|
||||
FindPricePage: TestPage "Find Price";
|
||||
begin
|
||||
FindPricePage.OpenNew();
|
||||
...
|
||||
end;
|
||||
}
|
||||
|
||||
## Best Practice
|
||||
|
||||
```al
|
||||
// CORRECT: separate codeunits per layer
|
||||
// CORRECT: separate codeunits per layer
|
||||
|
||||
// Logic tests — no _UT suffix
|
||||
codeunit 99006 "Find Price Testing"
|
||||
{
|
||||
Subtype = Test;
|
||||
[Test]
|
||||
procedure GetPrice_CustomerPrice_ReturnsUnitPrice()
|
||||
begin
|
||||
FindPriceMgt.GetSalesPrice(...);
|
||||
end;
|
||||
}
|
||||
// Logic tests — no _UT suffix
|
||||
codeunit 99006 "Find Price Testing"
|
||||
{
|
||||
Subtype = Test;
|
||||
[Test]
|
||||
procedure GetPrice_CustomerPrice_ReturnsUnitPrice()
|
||||
begin
|
||||
FindPriceMgt.GetSalesPrice(...);
|
||||
end;
|
||||
}
|
||||
|
||||
// UI tests — _UT suffix
|
||||
codeunit 99007 "Find Price Testing_UT"
|
||||
{
|
||||
Subtype = Test;
|
||||
[Test]
|
||||
procedure Page_EnterCustomerAndItem_FactBoxShowsPrice()
|
||||
var
|
||||
FindPricePage: TestPage "Find Price";
|
||||
begin
|
||||
FindPricePage.OpenNew();
|
||||
FindPricePage.CustomerNo.SetValue(Customer."No.");
|
||||
FindPricePage.ItemNo.SetValue(Item."No.");
|
||||
Assert.AreEqual('100,00', FindPricePage.FindPriceInfo.UnitPrice.Value(), '');
|
||||
end;
|
||||
}
|
||||
```
|
||||
// UI tests — _UT suffix
|
||||
codeunit 99007 "Find Price Testing_UT"
|
||||
{
|
||||
Subtype = Test;
|
||||
[Test]
|
||||
procedure Page_EnterCustomerAndItem_FactBoxShowsPrice()
|
||||
var
|
||||
FindPricePage: TestPage "Find Price";
|
||||
begin
|
||||
FindPricePage.OpenNew();
|
||||
FindPricePage.CustomerNo.SetValue(Customer."No.");
|
||||
FindPricePage.ItemNo.SetValue(Item."No.");
|
||||
Assert.AreEqual('100,00', FindPricePage.FindPriceInfo.UnitPrice.Value(), '');
|
||||
end;
|
||||
}
|
||||
|
||||
## Object ID allocation
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue