Custom-laget bestaar nu begge CI-checks: 72 validator-fejl -> 0

Normalisering af alle 39 custom knowledge-filer til READ-kontraktens
skema (validate_frontmatter.py + Test-KnowledgeIndex.ps1 begge groenne):

- R01/R02: 28 filer manglede frontmatter eller brugte aeldre skemaer
  (title/category/severity/rule-id m.fl.) - alle har nu praecis de 6
  kraevede noegler; keywords haandskrevet pr. fil da de driver
  worklist-selektionen i INDEX/knowledge-index
- R09: manglende Description-sektion - regel-agtige foersteoverskrifter
  (Core Rule/Rule/Regel/Core Principle) omdoebt, eller sektion indsat
  efter titlen hvor intro-tekst fandtes
- R10: fenced code blocks konverteret til 4-space indrykkede blokke
  i alle filer (indhold uaendret)
- R11: 4 filer over 100 linjer fortaettet redaktionelt uden semantisk
  tab (ai-eval-scores 143->100, git-lifecycle 121->97,
  permission-sets 113->99, test-feature-scenario-tags 105->91)
- R05: AL0197->al0197, add_repo->add-repo; keyword-lister trimmet
  til maks 10

Ingen regler er fjernet eller aendret i betydning - kun form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Michael Dieringer 2026-07-01 23:47:31 +02:00
parent ec2892f0ab
commit dd5637b1db
39 changed files with 729 additions and 814 deletions

View file

@ -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.

View file

@ -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."

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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`.

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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]]`.

View file

@ -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`).

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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`.

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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."

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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