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:
Michael Dieringer 2026-07-01 23:49:31 +02:00 • committed by GitHub
commit 4989e3ec4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 729 additions and 814 deletions

View file

@ -1,7 +1,7 @@
--- ---
bc-version: [all] bc-version: [all]
domain: architecture 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] technologies: [al]
countries: [w1] countries: [w1]
application-area: [all] 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. Configure the build output path to a dedicated subfolder that is excluded from language server scanning.
In `.vscode/settings.json`: In `.vscode/settings.json`:
```json {
{ "al.outputPath": ".output"
"al.outputPath": ".output" }
}
```
When using the MCP `al_build` tool, pass `outputPath` explicitly: 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. 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 # 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." 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: 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: Before I proceed, I need clarification on:
1. [specific question] 1. [specific question]
2. [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 partial code while waiting. Do not write "placeholder" code.
Do not write code with TODO comments where a business decision is needed. 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 ## Anti Pattern
``` // WRONG: Assuming and building
// WRONG: Assuming and building // Task: "write a test for the vendor flow"
// Task: "write a test for the vendor flow" // Agent assumes: Vendor, Incoming, Handle+Invoice, no contact
// Agent assumes: Vendor, Incoming, Handle+Invoice, no contact // Writes 80 lines of code
// Writes 80 lines of code // Developer says: "I meant outgoing flow with a customer"
// Developer says: "I meant outgoing flow with a customer" // Result: wasted work, possible production code changes to revert
// Result: wasted work, possible production code changes to revert
```
## Best Practice ## Best Practice
``` // CORRECT: Clarify first
// CORRECT: Clarify first // Task: "write a test for the vendor flow"
// Task: "write a test for the vendor flow"
I understand the task as: writing a test codeunit for a Settlement Voucher I understand the task as: writing a test codeunit for a Settlement Voucher
flow where the counterparty is a Vendor. flow where the counterparty is a Vendor.
Before I proceed, I need clarification on: Before I proceed, I need clarification on:
1. Incoming (vendor delivers goods) or Outgoing (vendor picks up goods)? 1. Incoming (vendor delivers goods) or Outgoing (vendor picks up goods)?
2. Handle-only, Invoice-only, or combined Handle+Invoice in one run? 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 3. Should the test use an existing vendor from the database or create one
via LibraryPurchase.CreateVendor? 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] bc-version: [all]
domain: architecture domain: architecture
keywords: [claude-md, agents, visibility, setup, mode-b, curabis-standard] 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: 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 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. 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: 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. Claude kan ikke kalde denne agent medmindre den tilføjes til CLAUDE.md.
Vil du have mig til at tilføje den nu? Vil du have mig til at tilføje den nu?
```
Do not continue with other activity until the developer has responded. 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 name: commit-message-must-include-bc-task-id
description: > description: >
@ -21,19 +29,15 @@ used across Curabis teams.
## Anti Pattern ## 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. No traceability. Impossible to find the BC task from git history.
## Best Practice ## Best Practice
``` [#8738] Add Price Lookup feature — FindPrice page, tier prices, currency conversion
[#8738] Add Price Lookup feature — FindPrice page, tier prices, currency conversion [#8738] Add 22 UI tests for PRICING LOOKUP feature
[#8738] Add 22 UI tests for PRICING LOOKUP feature [#8738] Add translations, shared project memory and cspell config
[#8738] Add translations, shared project memory and cspell config
```
## The two task numbers — use taskId, not taskNo ## 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 ## Scope
All commits that reach the main branch — feature, fix, test, chore, docs. 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] bc-version: [all]
domain: architecture 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] technologies: [al]
countries: [w1] countries: [w1]
application-area: [all] application-area: [all]
@ -49,26 +49,22 @@ the current project.
## Anti Pattern ## Anti Pattern
``` // WRONG: reverse-engineering the compiled symbol package instead of reading source
// WRONG: reverse-engineering the compiled symbol package instead of reading source // Agent parses SymbolReference.json from .alpackages/*.app to learn
// Agent parses SymbolReference.json from .alpackages/*.app to learn // Contract Management table fields and public procedure signatures.
// Contract Management table fields and public procedure signatures. // Result: incomplete picture, missed validation logic, excluded feature from tests.
// Result: incomplete picture, missed validation logic, excluded feature from tests.
```
## Best Practice ## Best Practice
``` // CORRECT: add the source repo and read it directly
// CORRECT: add the source repo and read it directly add_repo Curabis/ContractMgmt365app
add_repo Curabis/ContractMgmt365app
// Then read the actual table definitions, codeunits, and any Test Library // Then read the actual table definitions, codeunits, and any Test Library
// codeunits that may already exist in the repo's own test app. // codeunits that may already exist in the repo's own test app.
// If no Test Library exists in the dependency's 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 // 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. // based on the REAL table field definitions and trigger logic you can now read.
```
## When to apply this rule ## 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 # 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 **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 at least one permission set shipped by that app. "Exposed" means any object reachable from
outside the app's own UI: outside the app's own UI:

View file

@ -1,13 +1,15 @@
--- ---
name: feature-branch-must-merge-to-track-branch bc-version: [all]
title: Feature branches must merge into the project's declared track branch domain: architecture
category: architecture keywords: [git, feature-branch, track-branch, merge, workflow]
severity: required technologies: [al]
countries: [w1]
application-area: [all]
--- ---
# Feature branches must merge into the project's declared track branch # 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 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 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`: The track branch is declared once in `CLAUDE.md`:
```yaml # Declares the integration target for this development sprint/module
# Declares the integration target for this development sprint/module trackBranch: purchase
trackBranch: purchase
```
If no `trackBranch` is declared, `main` is the default and feature branches If no `trackBranch` is declared, `main` is the default and feature branches
merge there directly. merge there directly.
@ -46,33 +46,27 @@ The rule protects the invariant: **`main` is deployable at any moment.**
## The branching model ## The branching model
``` main
main └── <track-branch> (e.g. "purchase" — lives for one sprint/module)
└── <track-branch> (e.g. "purchase" — lives for one sprint/module) └── feature/<name> ← development happens here
└── feature/<name> ← development happens here └── feature/<name>
└── feature/<name> └── bugfix/<name>
└── bugfix/<name> └── hotfix/<name> ← branches from main, merges back to main
└── hotfix/<name> ← branches from main, merges back to main
```
At release: track-branch → main (via PR, after full QA). At release: track-branch → main (via PR, after full QA).
## Non-compliant ## Non-compliant
```bash # Merging a feature directly to main when a track branch is declared in CLAUDE.md
# Merging a feature directly to main when a track branch is declared in CLAUDE.md git checkout main
git checkout main git merge feature/my-feature # violates rule
git merge feature/my-feature # violates rule
```
## Compliant ## Compliant
```bash # Read track branch from CLAUDE.md → merge there
# Read track branch from CLAUDE.md → merge there git checkout purchase
git checkout purchase git merge --no-ff feature/my-feature
git merge --no-ff feature/my-feature # Then sync BC: gitHubDevStatus = "Done"
# Then sync BC: gitHubDevStatus = "Done"
```
## Scope ## Scope

View file

@ -48,9 +48,7 @@ whether Mode B executes for a given repository.
At session start, compare: 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 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 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: When a per-repo reconciliation gap is found, output exactly this before continuing:
``` ⚠️ Dette repository er ikke reconciled mod seneste BCQuality-SHA, selvom den
⚠️ Dette repository er ikke reconciled mod seneste BCQuality-SHA, selvom den globale versions-fil allerede er opdateret (formentlig af et andet projekt).
globale versions-fil allerede er opdateret (formentlig af et andet projekt). Kører Mode B-reconciliation for dette repo nu.
Kører Mode B-reconciliation for dette repo nu.
```
Do not silently skip Mode B just because the global marker looks current. 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 After any Mode B run, compare the list of files in `curabis-standard.agent.md`'s
Mode B template table against: 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 — 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 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: 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. 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 # 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. 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: 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. 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 # 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. 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 # 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. 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 ## Implementation Pattern
```al permissionset 50100 "PM365 - View"
permissionset 50100 "PM365 - View" {
{ Access = Public;
Access = Public; Assignable = true;
Assignable = true; Caption = 'Project Mgmt 365 - View';
Caption = 'Project Mgmt 365 - View'; Permissions =
Permissions = tabledata "PM Project" = R,
tabledata "PM Project" = R, page "PM Project List" = X;
tabledata "PM Project Task" = R, }
page "PM Project List" = X,
page "PM Project Card" = X;
}
permissionset 50101 "PM365 - Edit" permissionset 50101 "PM365 - Edit"
{ {
Access = Public; Access = Public;
Assignable = true; Assignable = true;
Caption = 'Project Mgmt 365 - Edit'; Caption = 'Project Mgmt 365 - Edit';
IncludedPermissionSets = "PM365 - View"; IncludedPermissionSets = "PM365 - View";
Permissions = Permissions =
tabledata "PM Project" = RIMD, tabledata "PM Project" = RIMD,
tabledata "PM Project Task" = RIMD, tabledata "PM Project Task" = RIMD,
codeunit "PM Project Management" = X; codeunit "PM Project Management" = X;
} }
permissionset 50102 "PM365 - Admin" permissionset 50102 "PM365 - Admin"
{ {
Access = Public; Access = Public;
Assignable = false; Assignable = false;
Caption = 'Project Mgmt 365 - Admin'; Caption = 'Project Mgmt 365 - Admin';
IncludedPermissionSets = "PM365 - Edit"; IncludedPermissionSets = "PM365 - Edit";
Permissions = Permissions =
tabledata "PM Setup" = RIMD, tabledata "PM Setup" = RIMD,
page "PM Setup" = X; page "PM Setup" = X;
} }
```
## Relationship to CURABIS-ARCH-011 ## Relationship to CURABIS-ARCH-011
This rule is a **companion to CURABIS-ARCH-011** (`exposed-objects-must-be-in-a-permission-set`): 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
- **CURABIS-ARCH-011**: Every exposed object *must exist* in at least one permission set requires the sets *themselves* to follow the tiered least-privilege structure.
- **This rule**: Permission sets *themselves* must follow the hierarchical least-privilege structure Both must hold — objects in a set that grants excessive access is not enough.
Both must be satisfied simultaneously: it is not enough that objects appear in a permission set if that set grants excessive access.
## Anti-Pattern ## Anti-Pattern
```al // Violation: flat "full access" set bundles user and admin access
// Violation: flat "full access" set bundles user and admin access permissionset 50100 "PM365 - Full Access"
permissionset 50100 "PM365 - Full Access" {
{ Assignable = true;
Assignable = true; Permissions =
Permissions = tabledata "PM Project" = RIMD,
tabledata "PM Project" = RIMD, tabledata "PM Setup" = RIMD, // admin data mixed with user data
tabledata "PM Setup" = RIMD, // admin data mixed with user data tabledata "PM Project Task" = RIMD,
tabledata "PM Project Task" = RIMD, codeunit "PM Post Codeunit" = X;
codeunit "PM Post Codeunit" = X; }
}
```
## BCApps Reference ## BCApps Reference
BCApps Business Foundation defines exactly this tiered pattern: BCApps Business Foundation defines exactly this tiered pattern: Microsoft uses
Admin, Edit, View, Obj, and Read tiers with `IncludedPermissionSets` throughout —
```al never a single flat "full access" set. Each tier inherits from the tier below;
// BusFoundEdit.PermissionSet.al Admin sets use `Assignable = false` to prevent accidental assignment to regular
permissionset 4 "Bus. Found. - Edit" users.
{
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.
- **Source:** https://github.com/microsoft/BCApps/tree/main/src/Business%20Foundation/App/Permissions - **Source:** https://github.com/microsoft/BCApps/tree/main/src/Business%20Foundation/App/Permissions
- **Files:** `BusFoundAdmin`, `BusFoundEdit`, `BusFoundView`, `BusFoundObj`, `BusFoundRead` - **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 ## Verification

View file

@ -1,11 +1,10 @@
--- ---
name: shared-project-memory-must-be-in-repo bc-version: [all]
description: > domain: architecture
Project-level memory (business rules, architectural decisions, scope boundaries) keywords: [projectmemory, shared-memory, repo, team-knowledge]
must be stored in a version-controlled projectmemory/ folder, not in a user's technologies: [al]
local Claude memory store, so all team members benefit from shared knowledge. countries: [w1]
layer: 2 application-area: [all]
category: architecture
--- ---
# Shared Project Memory Must Be in the Repository # 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 ## Anti Pattern
``` # Stored only on Michael's laptop — Tod and SJG never see this
# Stored only on Michael's laptop — Tod and SJG never see this ~/.claude/projects/d--MyProject/memory/project-pricing-vat-scope.md
~/.claude/projects/d--MyProject/memory/project-pricing-vat-scope.md
```
A rule observed by one developer stays siloed. The next session on another machine — A rule observed by one developer stays siloed. The next session on another machine —
or by another team member — starts from zero. or by another team member — starts from zero.
## Best Practice ## Best Practice
``` # In the git repository — committed, shared, visible to all
# In the git repository — committed, shared, visible to all projectmemory/
projectmemory/ memoryupdates_mid.md ← Michael's observations
memoryupdates_mid.md ← Michael's observations memoryupdates_tod.md ← Tod's observations
memoryupdates_tod.md ← Tod's observations memoryupdates_sjg.md ← SJG's observations
memoryupdates_sjg.md ← SJG's observations
```
Each file is named after the user who triggered the observation. All files are read 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`: 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 At session start, read **all files** in `projectmemory/` — they contain shared
project observations from all team members and are version-controlled in git. project observations from all team members and are version-controlled in git.
When you learn something project-relevant, write it to When you learn something project-relevant, write it to
`projectmemory/memoryupdates_<username>.md` for the active user. `projectmemory/memoryupdates_<username>.md` for the active user.
User-specific preferences (tone, workflow habits) stay in the local User-specific preferences (tone, workflow habits) stay in the local
`~/.claude/projects/.../memory/` folder as before. `~/.claude/projects/.../memory/` folder as before.
```
## What belongs in projectmemory vs local memory ## What belongs in projectmemory vs local memory

View file

@ -1,7 +1,7 @@
--- ---
bc-version: [all] bc-version: [all]
domain: architecture 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] technologies: [al]
countries: [w1] countries: [w1]
application-area: [all] application-area: [all]
@ -51,13 +51,11 @@ The following must remain in English in all locales:
## Trans-unit structure ## Trans-unit structure
```xml <trans-unit id="..." size-unit="char" translate="yes" xml:space="preserve">
<trans-unit id="..." size-unit="char" translate="yes" xml:space="preserve"> <source>Post</source>
<source>Post</source> <target state="translated">Bogfør</target> ← da-DK example
<target state="translated">Bogfør</target> ← da-DK example <note from="Developer" annotates="source" priority="2">Button caption</note>
<note from="Developer" annotates="source" priority="2">Button caption</note> </trans-unit>
</trans-unit>
```
State must always be `translated` — never `needs-translation` or `new`. 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 # 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. 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 ## Example Agent Instruction
``` Write only gitHubDevStatus and gitHubBranch on tasks.
Write only gitHubDevStatus and gitHubBranch on tasks. Never write Status — it controls the invoicing workflow.
Never write Status — it controls the invoicing workflow.
```
## Verification ## 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] 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 # Agent must resolve developer identity from BC
## Rule ## Description
Agent files must not contain static employee-to-code mappings. Agent files must not contain static employee-to-code mappings.
Developer identity must always be resolved at runtime from the BC users tool (PAG6102903). 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 ## Exceptions
None. If the users tool is temporarily unavailable, say so and stop -- do not fall back 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 # 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 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, 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 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`). 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
Scores must **not** be stored as: files, or any other location outside the BC posting table.
- task comments
- local files or agent memory
- inline in agent files or knowledge files
- any other location outside the BC posting table
## Why ## Why
The `CUR Project AI Score` table is a **posting table**: one immutable entry per 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 iteration, clustered on `Entry No.` — the single source of truth for hill climbing
hill climbing history on a sub-task. history on a sub-task. Alternate locations all break that guarantee: task comments
are 250-char, unstructured and unqueryable; local files are session- and
Storing scores elsewhere breaks this guarantee: 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
| Alternate location | Problem | 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.
| 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
## Compliant ## Compliant
After each eval iteration, the orchestrator calls: After each eval iteration, the orchestrator calls:
``` bc_post_ai_score(
bc_post_ai_score( projectNo = "DEV2026-00010",
projectNo = "DEV2026-00010", subTaskNo = "0014",
subTaskNo = "0014", iterationNo = 3,
iterationNo = 3, compile = true,
compile = true, testScore = 0.80,
testScore = 0.80, bcquality = 0.86,
bcquality = 0.86, f1Score = 0.83,
f1Score = 0.83, verdict = "Keep",
verdict = "Keep", model = "claude-sonnet-4-6"
model = "claude-sonnet-4-6" )
)
```
BC sets `Eval DateTime` automatically. The orchestrator may additionally post a BC sets `Eval DateTime` automatically. A brief human-readable comment in addition
brief human-readable comment ("Iteration 3: F1=0.83 → Keep") — this is allowed, ("Iteration 3: F1=0.83 → Keep") is allowed — the score itself is in BC.
as it communicates progress; the score itself is in BC.
## Non-compliant ## Non-compliant
``` # Storing score as task comment only
# Storing score as task comment only bc_add_comment(projectNo = "DEV2026-00010", subTaskNo = "0014",
bc_add_comment( comment = "Iter 3: compile OK tests 4/5 BCQ 6/7 F1=0.83 Keep")
projectNo = "DEV2026-00010", # -> unstructured text; not queryable; lost to reporting
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 in an agent file's "Hill climbing log" section
# Storing score in agent file # -> frozen, session-specific, wrong location
## 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
```
## False positive ## False positive
An agent that posts a human-readable summary comment **in addition to** calling Posting a human-readable summary comment **in addition to** calling
`bc_post_ai_score` is **not** violating this rule. The comment is human `bc_post_ai_score` is not a violation. The violation is using the comment or any
communication; the score is in BC. Both are permitted. other location **instead of** the BC table.
The violation is using the comment or any other location **instead of** posting to
the BC table.
## API reference ## API reference
- Page: `CUR MCP Project AI Scores` (PAG6102906) - Page: `CUR MCP Project AI Scores` (PAG6102906), entity `projectAIScores`
- Entity: `projectAIScores`
- Publisher: `curabis`, Group: `projectMgmt`, Version: `v2.0` - Publisher: `curabis`, Group: `projectMgmt`, Version: `v2.0`
- Insert: allowed. Modify: never. Delete: never. - Insert: allowed. Modify: never. Delete: never.
- `Eval DateTime` is set by BC `OnInsertRecord` — do not pass it. - `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) ## Eval at task boundaries (hill-climbing baseline and final)
To generate meaningful hill-climbing data, the project's eval script MUST be To generate meaningful hill-climbing data, the project's eval script MUST run at
run at two specific moments per task: two moments per task:
| Moment | When | Verdict to post | | Moment | When | Verdict to post |
|---|---|---| |---|---|---|
| **Baseline** | Before the first code change for a task | `"Baseline"` | | **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. Each project declares its eval script in `CLAUDE.md`; that script emits the score
- **Negative delta** -- technical debt was introduced; note it in the BC task comment. posted via `bc_post_ai_score` and appends to the project's eval history.
- **Zero or negligible delta** -- neutral; no action required.
### Project eval script ## Applies to
Each project declares its eval script in `CLAUDE.md`. That script emits a score Agent files implementing hill climbing eval loops on BC sub-tasks, and all tasks
and appends to the project's eval history. The score posted to `bc_post_ai_score` where the project declares an eval script in `CLAUDE.md`. Documentation-only
is the score emitted by that project-specific script. tasks (no code change) are exempt.
### 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.

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 # 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. **FlowFields on API pages must be explicitly calculated** via `CalcFields()` in the `OnAfterGetRecord` trigger, or they return empty values in OData responses.
## Key Points ## Key Points
@ -15,12 +23,10 @@
The provided example demonstrates proper implementation: The provided example demonstrates proper implementation:
```al trigger OnAfterGetRecord()
trigger OnAfterGetRecord() begin
begin Rec.CalcFields("Elapsed time (Chargeable)", "Customer Name");
Rec.CalcFields("Elapsed time (Chargeable)", "Customer Name"); end;
end;
```
## Verification Approach ## 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 # 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. 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 ## Problematic vs. Correct Approach
**Incorrect:** **Incorrect:**
```al field(projectNo; Rec."Project No.")
field(projectNo; Rec."Project No.") {
{ Editable = false; // prevents API inserts when consumer must supply the value
Editable = false; // prevents API inserts when consumer must supply the value }
}
```
**Correct:** **Correct:**
```al field(projectNo; Rec."Project No.")
field(projectNo; Rec."Project No.") {
{ // No Editable = false — consumer supplies this on POST
// No Editable = false — consumer supplies this on POST }
}
```
## Key Takeaways ## 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 # 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. 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 ## Pattern to Avoid
```al // WRONG: General page widened with write access to one field
// WRONG: General page widened with write access to one field // Now the agent can accidentally (or intentionally) write to all other fields too
// Now the agent can accidentally (or intentionally) write to all other fields too field(status; Rec.Status) { } // should be read-only
field(status; Rec.Status) { } // should be read-only field(gitHubRepository; Rec."GitHub Repository") { } // the one field we want writable
field(gitHubRepository; Rec."GitHub Repository") { } // the one field we want writable field(estimatedHours; Rec."Estimated Hours") { } // should be read-only
field(estimatedHours; Rec."Estimated Hours") { } // should be read-only
```
## Correct Pattern ## Correct Pattern
Create a separate, minimal API page: Create a separate, minimal API page:
```al page 6102904 "CUR MCP Project Repository"
page 6102904 "CUR MCP Project Repository" {
{ // Only two fields: the key and the one writable field
// Only two fields: the key and the one writable field field(no; Rec."No.") { Editable = false; }
field(no; Rec."No.") { Editable = false; } field(gitHubRepository; Rec."GitHub Repository") { }
field(gitHubRepository; Rec."GitHub Repository") { } }
}
```
## Requirements ## Requirements

View file

@ -1,10 +1,10 @@
--- ---
name: bc-mcp-find-active-task-for-branch bc-version: [all]
description: > domain: mcp
Standard recipe for finding the BC sub-task linked to the current git branch, keywords: [mcp, bc-task, branch, active-task, recipe]
including exact action names and field names for each BC MCP endpoint. technologies: [al]
layer: 2 countries: [w1]
category: mcp application-area: [all]
--- ---
# BC MCP: Find Active Task for Branch # 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 ## Standard recipe: find task for current branch
``` 1. git branch --show-current → e.g. "PriceLookup"
1. git branch --show-current → e.g. "PriceLookup" 2. git remote get-url origin → e.g. "https://github.com/Curabis/Wareco.git"
2. git remote get-url origin → e.g. "https://github.com/Curabis/Wareco.git" 3. bc_actions_invoke List_project_PAG6102901
3. bc_actions_invoke List_project_PAG6102901 filter: "gitHubRepository eq 'https://github.com/Curabis/Wareco.git'"
filter: "gitHubRepository eq 'https://github.com/Curabis/Wareco.git'" → get projectNo (e.g. "W-2024-001")
→ get projectNo (e.g. "W-2024-001") 4. bc_actions_invoke List_activeTask_PAG6102900
4. bc_actions_invoke List_activeTask_PAG6102900 filter: "projectNo eq 'W-2024-001' and gitHubBranch eq 'PriceLookup'"
filter: "projectNo eq 'W-2024-001' and gitHubBranch eq 'PriceLookup'" → get taskId (global commit-message ID), taskNo, description, status
→ 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 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. 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: The bridge runs as app identity `BC_DevelopmentMCP`. To attribute work:
``` 1. git config user.email → developer's git email
1. git config user.email → developer's git email 2. bc_actions_invoke List_consultant_PAG50009
2. bc_actions_invoke List_consultant_PAG50009 filter: "email eq 'mic.dieringer@gmail.com'"
filter: "email eq 'mic.dieringer@gmail.com'" → get employeeCode (e.g. "MID")
→ get employeeCode (e.g. "MID") 3. Use employeeCode to filter "my tasks":
3. Use employeeCode to filter "my tasks": List_activeTask_PAG6102900 filter: "taskResponsible eq 'MID'"
List_activeTask_PAG6102900 filter: "taskResponsible eq 'MID'" 4. Sign status comments: end with "— Michael" so attribution survives S2S
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 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. 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 bc-version: [all]
description: > domain: mcp
When a developer asks for their open tasks, scope the result to the current keywords: [mcp, bc-task, repository-scope, project]
git repository only. If no BC project is linked to the repo, raise it as a technologies: [al]
flag instead of returning all tasks. countries: [w1]
layer: 2 application-area: [all]
category: mcp
--- ---
# BC MCP: Scope Task Lists to the Current Repository # 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 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 equivalent question about their work queue, **only return tasks that belong to
@ -29,20 +28,18 @@ wrong project.
## Standard recipe ## Standard recipe
``` 1. git remote get-url origin
1. git remote get-url origin → e.g. "https://github.com/Curabis/Wareco.git"
→ e.g. "https://github.com/Curabis/Wareco.git"
2. List_ProjectRepositories_PAG6102904 2. List_ProjectRepositories_PAG6102904
filter: "gitHubRepository eq '<url>'" filter: "gitHubRepository eq '<url>'"
→ get projectNo(s) for this repo → 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 4. List_ActiveTasks_PAG6102900
filter: "projectNo eq '<projectNo>' and taskResponsible eq '<employeeCode>'" filter: "projectNo eq '<projectNo>' and taskResponsible eq '<employeeCode>'"
→ return only tasks in this repo's project(s) → return only tasks in this repo's project(s)
```
For developer identity (resolving `employeeCode` from git email), see For developer identity (resolving `employeeCode` from git email), see
`[[bc-mcp-find-active-task-for-branch]]`. `[[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 rule: bc-mcp-tools-must-be-preloaded
title: BC MCP tool schemas must be pre-loaded at session start 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 # 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 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. 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. This call must complete before the agent responds to the user.
@ -35,16 +41,14 @@ and eliminates mid-task delays entirely.
## Correct pattern ## Correct pattern
``` # bc-mcp.agent.md session start
# bc-mcp.agent.md session start
1. ToolSearch: select:mcp__businesscentral__bc_actions_search, 1. ToolSearch: select:mcp__businesscentral__bc_actions_search,
mcp__businesscentral__bc_actions_invoke, mcp__businesscentral__bc_actions_invoke,
mcp__businesscentral__bc_actions_describe mcp__businesscentral__bc_actions_describe
2. [proceed with user request] 2. [proceed with user request]
```
## Scope ## Scope
Applies to every invocation of `bc-mcp.agent.md` in every CURABIS project that uses 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 bc-version: [all]
title: Git lifecycle must sync BC subtask dev status domain: mcp
severity: warning keywords: [git, lifecycle, bc-status, dev-status, sync]
domain: git, mcp, bc-integration technologies: [al]
applies-to: [feature branches, bugfix branches, hotfix branches] countries: [w1]
application-area: [all]
--- ---
# Git lifecycle must sync BC subtask dev status # Git lifecycle must sync BC subtask dev status
## Description
Every AL feature branch is linked to a BC subtask. The `gitHubDevStatus` and 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 `gitHubBranch` fields on the subtask must reflect the real state of the branch
at all times — automatically, without manual steps. at all times — automatically, without manual steps.
## Track branch ## Track branch
Each project declares its **track branch** in `CLAUDE.md` — the branch that is Each project declares its **track branch** in `CLAUDE.md` — the merge target for
the merge target for all feature branches in the current development track: all feature branches in the current development track:
| Declaration in CLAUDE.md | Meaning | | Declaration in CLAUDE.md | Meaning |
|---|---| |---|---|
@ -27,33 +30,16 @@ feature branch is merged into the track branch — not necessarily `main`.
## Branch naming convention ## 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 | Valid: feature/DEV2023-00027-004-bc-agent-semantic-tools
| --- | --- | --- | bugfix/DEV2023-00027-003-odata-string-key
| type | `feature`, `bugfix`, `hotfix` | `feature` | feature/DEV2023-00027-4
| projectNo | `[A-Z]{2,4}\d{4}-\d{5}` | `DEV2023-00027` | Invalid: my-feature / fix-thing / DEV2023-00027 (no automation)
| 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
```
## Status mapping ## Status mapping
@ -66,47 +52,34 @@ DEV2023-00027
## Automated implementation (git hooks) ## Automated implementation (git hooks)
Automation is provided by two git hooks in `.githooks/` (activated via Two git hooks in `.githooks/` (activated via `git config core.hooksPath .githooks`)
`git config core.hooksPath .githooks`) that call call `Scripts/Invoke-BCGitSync.ps1`: `post-checkout` detects branch creation and
`Scripts/Invoke-BCGitSync.ps1`: 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
- `post-checkout` — detects branch creation and branch abandonment the git operation, and ignores branches that do not follow the naming convention.
- `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.
## Claude-driven synchronization ## Claude-driven synchronization
When Claude executes git operations, the git hooks may not fire — either because When Claude executes git operations, the hooks may not fire — hooks unconfigured,
hooks are not configured, or because the branch name does not follow the or branch name outside the convention. **Claude MUST call BC MCP explicitly at
`<type>/<projectNo>-<taskNo>` convention. two points:**
**Claude MUST call BC MCP explicitly at two points:**
| Moment | BC MCP action | | Moment | BC MCP action |
|---|---| |---|---|
| Feature branch created | `gitHubDevStatus = "In Progress"`, `gitHubBranch = <branch>` | | Feature branch created | `gitHubDevStatus = "In Progress"`, `gitHubBranch = <branch>` |
| Feature branch merged to track branch | `gitHubDevStatus = "Done"`, `gitHubBranch = <track-branch>` | | Feature branch merged to track branch | `gitHubDevStatus = "Done"`, `gitHubBranch = <track-branch>` |
Steps: Steps: find the active task using the recipe in
1. Find the active task using the recipe in `[[bc-mcp-find-active-task-for-branch]]` `[[bc-mcp-find-active-task-for-branch]]`, then call
2. Call `Modify_activeTask_PAG6102900` with the two writable fields `Modify_activeTask_PAG6102900` with the two writable fields. This applies
regardless of branch naming and regardless of whether hooks are also active —
This requirement applies regardless of branch naming format and regardless of if both run they write identical values.
whether git hooks are also active. If both run, there is no conflict — they write
identical values.
## Safety rules ## Safety rules
CURABIS-BCMCP-008 The sync script NEVER writes BC subtask `status` CURABIS-BCMCP-008 The sync script NEVER writes BC subtask `status`
(Created/Accepted/In progress/Finished/Invoiced). It only writes (Created/Accepted/In progress/Finished/Invoiced). It only writes
`gitHubDevStatus` and `gitHubBranch`. These are the only two fields `gitHubDevStatus` and `gitHubBranch` (see CURABIS-BCMCP-001).
the agent is allowed to modify (see CURABIS-BCMCP-001).
CURABIS-BCMCP-009 The sync script exits 0 on all errors. It must never 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. 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 ## BCApps reference
Branch naming conventions and git workflow integration follow the patterns used Branch naming and git workflow integration follow
in [microsoft/BCApps](https://github.com/microsoft/BCApps) — see [microsoft/BCApps](https://github.com/microsoft/BCApps) conventions — see its
`.github/CONTRIBUTING.md` for Microsoft's own conventions on feature branches `.github/CONTRIBUTING.md` for feature-branch and work-item-referencing patterns.
and PR titles that reference work items.

View file

@ -1,14 +1,15 @@
--- ---
rule: CURABIS-MCP-003 bc-version: [all]
title: MCP bridge JavaScript-filer skal gemmes uden UTF-8 BOM domain: mcp
category: mcp keywords: [mcp, bridge, encoding, utf-8, stdio]
severity: high technologies: [al]
tags: [mcp, encoding, node, bridge, windows] countries: [w1]
application-area: [all]
--- ---
# CURABIS-MCP-003 — MCP bridge JavaScript-filer skal gemmes uden UTF-8 BOM # 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. 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):** **Download og gem korrekt (uden BOM):**
```powershell $content = (Invoke-WebRequest -Uri $url -UseBasicParsing).Content
$content = (Invoke-WebRequest -Uri $url -UseBasicParsing).Content [System.IO.File]::WriteAllText($destPath, $content, [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($destPath, $content, [System.Text.UTF8Encoding]::new($false))
```
**Verifikation efter gem:** **Verifikation efter gem:**
```powershell $bytes = [System.IO.File]::ReadAllBytes($filePath)
$bytes = [System.IO.File]::ReadAllBytes($filePath) if ($bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
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"
throw "BOM detected in $filePath — file cannot be used as Node.js entry point" }
}
```
**Strip af eksisterende BOM (remediation):** **Strip af eksisterende BOM (remediation):**
```powershell $bytes = [System.IO.File]::ReadAllBytes($path)
$bytes = [System.IO.File]::ReadAllBytes($path) if ($bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
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)])
[System.IO.File]::WriteAllBytes($path, $bytes[3..($bytes.Length - 1)]) }
}
```
## Hvad der IKKE må ske ## 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. Symptom: MCP-server er konfigureret i `.mcp.json`, men eksponerer ingen tools i sessionen.
Diagnose: Diagnose:
```powershell # Tjek første bytes
# Tjek første bytes $b = [System.IO.File]::ReadAllBytes("path\to\bridge.js")
$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]
"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
# Hvis output er "0xEF 0xBB 0xBF" er BOM årsagen
```
```bash # Kør bridge direkte og se om Node.js fejler
# Kør bridge direkte og se om Node.js fejler node path/to/bridge.js 2>&1 | head -5
node path/to/bridge.js 2>&1 | head -5
```
## Evidens ## 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 bc-version: [all]
title: Shared MCP configuration must not hardcode developer-specific paths domain: mcp
category: mcp keywords: [mcp-config, hardcoded-paths, userprofile, portability]
severity: error technologies: [al]
version: 1 countries: [w1]
application-area: [all]
--- ---
# Shared MCP configuration must not hardcode developer-specific paths # 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 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 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: Example:
```json {
{ "mcpServers": {
"mcpServers": { "example": {
"example": { "command": "powershell",
"command": "powershell", "args": ["-File", "${CLAUDE_PROJECT_DIR:-.}\\.vscode\\launch-server.ps1"]
"args": ["-File", "${CLAUDE_PROJECT_DIR:-.}\\.vscode\\launch-server.ps1"] },
}, "bridge": {
"bridge": { "command": "node",
"command": "node", "args": ["${USERPROFILE}\\.claude\\bridge.js"]
"args": ["${USERPROFILE}\\.claude\\bridge.js"] }
}
} }
}
}
```
## What NOT to do ## What NOT to do

View file

@ -1,14 +1,15 @@
--- ---
rule: mcp-server-must-be-verified-at-session-start bc-version: [all]
title: MCP server availability must be verified at session start domain: mcp
category: mcp keywords: [mcp-server, verification, session-start]
severity: error technologies: [al]
version: 1 countries: [w1]
application-area: [all]
--- ---
# MCP server availability must be verified at session start # 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 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, 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 ## 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 bc-version: [all]
title: MCP tool documentation must include the invocation model domain: mcp
category: mcp keywords: [mcp, tool-invocation, documentation, transparency]
severity: warning technologies: [al]
version: 1 countries: [w1]
application-area: [all]
--- ---
# MCP tool documentation must include the invocation model # MCP tool documentation must include the invocation model
## Rule ## Description
An MCP agent's documentation must describe the actual invocation model — including 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. 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 ## 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 # 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. 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 ## Pattern to Avoid
```al // WRONG: Exposes the stored snapshot — may be stale
// WRONG: Exposes the stored snapshot — may be stale field(timeLeft; Rec."Time left") { }
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. `"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: Recalculate in `OnAfterGetRecord` using a page variable:
```al trigger OnAfterGetRecord()
trigger OnAfterGetRecord() begin
begin Rec.CalcFields("Elapsed time (Chargeable)");
Rec.CalcFields("Elapsed time (Chargeable)"); TimeLeftCalc := Rec."Estimated time" - Rec."Elapsed time (Chargeable)";
TimeLeftCalc := Rec."Estimated time" - Rec."Elapsed time (Chargeable)"; end;
end;
var var
TimeLeftCalc: Decimal; TimeLeftCalc: Decimal;
// In layout: // In layout:
field(timeLeft; TimeLeftCalc) { } // live value field(timeLeft; TimeLeftCalc) { } // live value
field(elapsedTime; Rec."Elapsed time (Chargeable)") { } // source FlowField field(elapsedTime; Rec."Elapsed time (Chargeable)") { } // source FlowField
```
## Requirements ## Requirements

View file

@ -1,14 +1,15 @@
--- ---
id: CURABIS-MCP-SHEBANG-001 bc-version: [all]
title: Shebang-integritet ved deploy af script-filer domain: mcp
category: mcp keywords: [write, shebang, script-integrity, encoding]
severity: error technologies: [al]
applies-to: [claude-code, windows, mcp-setup] countries: [w1]
application-area: [all]
--- ---
# Shebang-integritet ved deploy af script-filer # Shebang-integritet ved deploy af script-filer
## Regel ## Description
Når en script-fil med shebang-linje (`.js`, `.sh`, `.ps1`) skrives via Claude Codes 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 `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.: Den verificerede linje skal matche den forventede shebang præcist, f.eks.:
``` #!/usr/bin/env node
#!/usr/bin/env node
```
## Baggrund ## 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) ## Verifikation (påkrævet efter enhver write af script-fil)
```python with open(deployed_path, "r", encoding="utf-8") as f:
with open(deployed_path, "r", encoding="utf-8") as f: line1 = f.readline().rstrip()
line1 = f.readline().rstrip() assert line1 == expected_shebang, f"Shebang fejl: forventet {expected_shebang!r}, fik {line1!r}"
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. 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. 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 # 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. 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 ## Example: Project Management App
```al codeunit 80100 "BCPT Create Project" implements "BCPT Test Param. Provider"
codeunit 80100 "BCPT Create Project" implements "BCPT Test Param. Provider" {
{ SingleInstance = true;
SingleInstance = true;
trigger OnRun() trigger OnRun()
begin begin
if not IsInitialized then begin if not IsInitialized then begin
InitTest(); InitTest();
IsInitialized := true; IsInitialized := true;
end;
CreateProject(GlobalBCPTTestContext);
end; end;
CreateProject(GlobalBCPTTestContext);
end;
var var
GlobalBCPTTestContext: Codeunit "BCPT Test Context"; GlobalBCPTTestContext: Codeunit "BCPT Test Context";
IsInitialized: Boolean; IsInitialized: Boolean;
local procedure InitTest() local procedure InitTest()
begin begin
// Set up any required BC configuration // Set up any required BC configuration
end; end;
local procedure CreateProject(var BCPTTestContext: Codeunit "BCPT Test Context") local procedure CreateProject(var BCPTTestContext: Codeunit "BCPT Test Context")
begin begin
BCPTTestContext.StartScenario('Create Project Header'); BCPTTestContext.StartScenario('Create Project Header');
// ... create project // ... create project
BCPTTestContext.EndScenario('Create Project Header'); BCPTTestContext.EndScenario('Create Project Header');
BCPTTestContext.UserWait(); BCPTTestContext.UserWait();
BCPTTestContext.StartScenario('Add Project Task'); BCPTTestContext.StartScenario('Add Project Task');
// ... add task // ... add task
BCPTTestContext.EndScenario('Add Project Task'); BCPTTestContext.EndScenario('Add Project Task');
end; end;
procedure GetDefaultParameters(): Text[1000] procedure GetDefaultParameters(): Text[1000]
begin begin
exit(''); exit('');
end; end;
procedure ValidateParameters(Parameters: Text[1000]) procedure ValidateParameters(Parameters: Text[1000])
begin begin
end; end;
} }
```
## Suggested Scenarios for Project Management Apps ## 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 # 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." "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] bc-version: [all]
domain: testing 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] technologies: [al]
countries: [w1] countries: [w1]
application-area: [all] application-area: [all]
@ -31,69 +31,52 @@ what is covered without reading AL.
## Anti Pattern ## Anti Pattern
```al // WRONG: no structure — tests as anonymous procedures
// WRONG: no structure — tests as anonymous procedures codeunit 99006 "Find Price Testing"
codeunit 99006 "Find Price Testing" {
{ Subtype = Test;
Subtype = Test;
[Test] [Test]
procedure Test1() // what does this test? procedure Test1() // what does this test?
begin begin
// setup mixed with assertions, no clear layers // setup mixed with assertions, no clear layers
Customer.Insert(false); Customer.Insert(false);
FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', Price, Disc); FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', Price, Disc);
Assert.AreEqual(100, Price, ''); Assert.AreEqual(100, Price, '');
end; end;
} }
```
## Best Practice ## Best Practice
```al // [FEATURE] Find Price — price cascade (Customer → Price Group → All Customers)
// [FEATURE] Find Price — price cascade (Customer → Price Group → All Customers) codeunit 99006 "Find Price Testing"
codeunit 99006 "Find Price Testing" {
{ Subtype = Test;
Subtype = Test;
var var
WarecoLib: Codeunit "Wareco Test Library"; WarecoLib: Codeunit "Wareco Test Library";
Assert: Codeunit "Library Assert"; Assert: Codeunit "Library Assert";
// [SCENARIO] Customer with a specific price list line gets that unit price // [SCENARIO] Customer with a specific price list line gets that unit price
[Test] [Test]
procedure GetPrice_CustomerPrice_ReturnsUnitPrice() procedure GetPrice_CustomerPrice_ReturnsUnitPrice()
var var
Customer: Record Customer; Customer: Record Customer;
Item: Record Item; Item: Record Item;
UnitPrice, LineDiscPct: Decimal; UnitPrice, LineDiscPct: Decimal;
begin begin
// [GIVEN] a customer with a price list line at 100 LCY // [GIVEN] a customer with a price list line at 100 LCY
WarecoLib.GivenCustomerWithPrice(Customer, Item, '', 100); WarecoLib.GivenCustomerWithPrice(Customer, Item, '', 100);
// [WHEN] // [WHEN]
FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct); FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct);
// [THEN] // [THEN]
Assert.AreEqual(100, UnitPrice, 'Unit price must match customer price list'); Assert.AreEqual(100, UnitPrice, 'Unit price must match customer price list');
end; end;
}
// [SCENARIO] Customer with no price list line falls back to item unit price Every further `[Test]` procedure in the codeunit repeats the same pattern: its
[Test] own `[SCENARIO]` comment above the attribute, and `[GIVEN]`/`[WHEN]`/`[THEN]`
procedure GetPrice_NoCustomerPrice_FallsBackToItemPrice() layers inside the body.
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;
}
```
## Relationship to procedure naming ## Relationship to procedure naming

View file

@ -25,55 +25,51 @@ belongs in `[WHEN]`.
## Anti Pattern ## Anti Pattern
```al // WRONG: two actions in one test
// WRONG: two actions in one test [Test]
[Test] procedure GetPrice_ThenGetDiscount_ReturnsCorrectValues()
procedure GetPrice_ThenGetDiscount_ReturnsCorrectValues() var
var UnitPrice, LineDiscPct: Decimal;
UnitPrice, LineDiscPct: Decimal; begin
begin // [GIVEN] ...
// [GIVEN] ... // [WHEN] first action
// [WHEN] first action FindPriceMgt.GetSalesPrice(CustomerNo, ItemNo, '', UnitPrice, LineDiscPct);
FindPriceMgt.GetSalesPrice(CustomerNo, ItemNo, '', UnitPrice, LineDiscPct); // [WHEN] second action — this is a second test in disguise
// [WHEN] second action — this is a second test in disguise FindPriceMgt.GetSalesPriceTiers(CustomerNo, ItemNo, '', TempBuffer);
FindPriceMgt.GetSalesPriceTiers(CustomerNo, ItemNo, '', TempBuffer); // [THEN] asserting two unrelated things
// [THEN] asserting two unrelated things Assert.AreEqual(100, UnitPrice, '');
Assert.AreEqual(100, UnitPrice, ''); Assert.IsFalse(TempBuffer.IsEmpty(), '');
Assert.IsFalse(TempBuffer.IsEmpty(), ''); end;
end;
```
## Best Practice ## Best Practice
```al // CORRECT: split into two focused tests
// CORRECT: split into two focused tests
[Test] [Test]
procedure GetPrice_CustomerPrice_ReturnsCorrectUnitPrice() procedure GetPrice_CustomerPrice_ReturnsCorrectUnitPrice()
var var
UnitPrice, LineDiscPct: Decimal; UnitPrice, LineDiscPct: Decimal;
begin begin
// [GIVEN] a customer with a price list line at 100 // [GIVEN] a customer with a price list line at 100
WarecoLib.GivenCustomerWithPrice(Customer, Item, '', 100); WarecoLib.GivenCustomerWithPrice(Customer, Item, '', 100);
// [WHEN] // [WHEN]
FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct); FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct);
// [THEN] // [THEN]
Assert.AreEqual(100, UnitPrice, 'Unit price must match price list'); Assert.AreEqual(100, UnitPrice, 'Unit price must match price list');
end; end;
[Test] [Test]
procedure GetPriceTiers_CustomerTier_ReturnsOneTierLine() procedure GetPriceTiers_CustomerTier_ReturnsOneTierLine()
var var
TempBuffer: Record "Find Price Tier Buffer" temporary; TempBuffer: Record "Find Price Tier Buffer" temporary;
begin begin
// [GIVEN] a customer with a tier price at min qty 10 // [GIVEN] a customer with a tier price at min qty 10
WarecoLib.GivenCustomerWithTierPrice(Customer, Item, '', 10, 90); WarecoLib.GivenCustomerWithTierPrice(Customer, Item, '', 10, 90);
// [WHEN] // [WHEN]
FindPriceMgt.GetSalesPriceTiers(Customer."No.", Item."No.", '', TempBuffer); FindPriceMgt.GetSalesPriceTiers(Customer."No.", Item."No.", '', TempBuffer);
// [THEN] // [THEN]
Assert.AreEqual(1, TempBuffer.Count(), 'Exactly one tier line expected'); Assert.AreEqual(1, TempBuffer.Count(), 'Exactly one tier line expected');
end; end;
```
## Naming implication ## 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 # CURABIS Test Library Standards
## Core Rules ## Description
The documentation establishes three critical testing practices for CURABIS AL applications: 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 test that accepts wrong behaviour silently. If the production code contains a
bug that contradicts the business specification, flag it explicitly: bug that contradicts the business specification, flag it explicitly:
``` // ⚠️ NOTE: This assertion reflects current code behaviour.
// ⚠️ NOTE: This assertion reflects current code behaviour. // Business spec says 1792,00 but code currently produces 1800,00.
// Business spec says 1792,00 but code currently produces 1800,00. // Flagged for review — do not merge until resolved.
// Flagged for review — do not merge until resolved.
```
Never silently adjust an assertion to make a test green when the discrepancy Never silently adjust an assertion to make a test green when the discrepancy
is a real business logic error. is a real business logic error.
## Anti Pattern ## Anti Pattern
```al // WRONG: Writing the "ideal" test without reading the production code,
// WRONG: Writing the "ideal" test without reading the production code, // then leaving it failing and saying "the code needs to be fixed"
// then leaving it failing and saying "the code needs to be fixed" [THEN]
[THEN] Assert.AreEqual(1792, ActualAmount, 'Total should be 1792');
Assert.AreEqual(1792, ActualAmount, 'Total should be 1792'); // Test fails. Agent says: "You need to fix SVPost to produce 1792."
// Test fails. Agent says: "You need to fix SVPost to produce 1792." // This is not what was asked for.
// This is not what was asked for.
```
## Best Practice ## Best Practice
```al // CORRECT: Read SVPost, understand what it produces, write the test to match.
// 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 is 1792 in both spec and code → assert 1792. // If the number differs → flag it, don't silently change it.
// If the number differs → flag it, don't silently change it.
// [GIVEN] Read SVPost.Codeunit.al and SV Test Library before writing assertions. // [GIVEN] Read SVPost.Codeunit.al and SV Test Library before writing assertions.
// [THEN] Assert what the code actually produces, verified by reading the source. // [THEN] Assert what the code actually produces, verified by reading the source.
Assert.AreEqual(ExpectedAmount, ActualAmount, 'Net payout to vendor must match'); Assert.AreEqual(ExpectedAmount, ActualAmount, 'Net payout to vendor must match');
```
## Workflow when asked to write a passing test ## Workflow when asked to write a passing test

View file

@ -30,71 +30,65 @@ in the same codeunit.
## Anti Pattern ## Anti Pattern
```al // WRONG: UI test codeunit without _UT suffix
// WRONG: UI test codeunit without _UT suffix codeunit 99007 "Find Price Page Testing"
codeunit 99007 "Find Price Page Testing" {
{ Subtype = Test;
Subtype = Test; // contains TestPage calls — should be named "Find Price Testing_UT"
// 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();
... ...
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 ## Best Practice
```al // CORRECT: separate codeunits per layer
// CORRECT: separate codeunits per layer
// Logic tests — no _UT suffix // Logic tests — no _UT suffix
codeunit 99006 "Find Price Testing" codeunit 99006 "Find Price Testing"
{ {
Subtype = Test; Subtype = Test;
[Test] [Test]
procedure GetPrice_CustomerPrice_ReturnsUnitPrice() procedure GetPrice_CustomerPrice_ReturnsUnitPrice()
begin begin
FindPriceMgt.GetSalesPrice(...); FindPriceMgt.GetSalesPrice(...);
end; end;
} }
// UI tests — _UT suffix // UI tests — _UT suffix
codeunit 99007 "Find Price Testing_UT" codeunit 99007 "Find Price Testing_UT"
{ {
Subtype = Test; Subtype = Test;
[Test] [Test]
procedure Page_EnterCustomerAndItem_FactBoxShowsPrice() procedure Page_EnterCustomerAndItem_FactBoxShowsPrice()
var var
FindPricePage: TestPage "Find Price"; FindPricePage: TestPage "Find Price";
begin begin
FindPricePage.OpenNew(); FindPricePage.OpenNew();
FindPricePage.CustomerNo.SetValue(Customer."No."); FindPricePage.CustomerNo.SetValue(Customer."No.");
FindPricePage.ItemNo.SetValue(Item."No."); FindPricePage.ItemNo.SetValue(Item."No.");
Assert.AreEqual('100,00', FindPricePage.FindPriceInfo.UnitPrice.Value(), ''); Assert.AreEqual('100,00', FindPricePage.FindPriceInfo.UnitPrice.Value(), '');
end; end;
} }
```
## Object ID allocation ## Object ID allocation