From c4a9f8e88ea4fe7007bda59a6490f18d6ad74e38 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sat, 13 Jun 2026 13:46:40 +0200
Subject: [PATCH 01/54] update
---
.../pages-must-not-contain-business-logic.md | 65 +++++++++++++++++
.../test-setup-must-use-library-codeunit.md | 71 +++++++++++++++++++
2 files changed, 136 insertions(+)
create mode 100644 custom/knowledge/architecture/pages-must-not-contain-business-logic.md
create mode 100644 custom/knowledge/testing/test-setup-must-use-library-codeunit.md
diff --git a/custom/knowledge/architecture/pages-must-not-contain-business-logic.md b/custom/knowledge/architecture/pages-must-not-contain-business-logic.md
new file mode 100644
index 0000000..073a919
--- /dev/null
+++ b/custom/knowledge/architecture/pages-must-not-contain-business-logic.md
@@ -0,0 +1,65 @@
+---
+bc-version: [all]
+domain: architecture
+keywords: [page, trigger, onaction, modify, codeunit, logic]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## Description
+
+In CURABIS codebases, pages are pure presentation. Business logic, calculations,
+validations, and record modifications belong in codeunits — not in page triggers
+or actions. This is stricter than the general BC guidance and applies to all
+CURABIS PTE apps.
+
+A page procedure that calculates a value and assigns it to a field, calls
+`Rec.Modify()` directly, or implements business rules is an architecture violation
+even if it compiles.
+
+**Exceptions:**
+- Setup pages may read and write their own setup record directly.
+- The designated "Run Conversion" page may call the conversion codeunit directly.
+
+## Anti Pattern
+
+```al
+// WRONG: calculation and Modify in a page action
+trigger OnAction()
+begin
+ Rec."Total Amount" := Rec.Quantity * Rec."Unit Price";
+ Rec."VAT Amount" := Rec."Total Amount" * 0.25;
+ Rec.Modify();
+end;
+```
+
+```al
+// WRONG: validation logic in page trigger
+trigger OnValidate()
+begin
+ if Rec.Quantity < 0 then
+ Error('Quantity cannot be negative');
+ Rec."Total Amount" := Rec.Quantity * Rec."Unit Price";
+end;
+```
+
+## Best Practice
+
+```al
+// CORRECT: page delegates to codeunit
+trigger OnAction()
+begin
+ SVManagement.RecalculateLine(Rec);
+end;
+```
+
+```al
+// CORRECT: validation belongs in table or codeunit
+trigger OnValidate()
+begin
+ SVManagement.ValidateAndRecalculate(Rec);
+end;
+```
+
+The codeunit owns the logic. The page owns the presentation.
diff --git a/custom/knowledge/testing/test-setup-must-use-library-codeunit.md b/custom/knowledge/testing/test-setup-must-use-library-codeunit.md
new file mode 100644
index 0000000..722c8e9
--- /dev/null
+++ b/custom/knowledge/testing/test-setup-must-use-library-codeunit.md
@@ -0,0 +1,71 @@
+---
+bc-version: [all]
+domain: testing
+keywords: [test, library, setup, initialize, suppresscommit, asserterror]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## Description
+
+In CURABIS test apps, all test setup is centralized in a dedicated Test Library
+codeunit (e.g. `SV Test Library`). Individual test procedures must not call
+BC standard library codeunits (`LibrarySales`, `LibraryInventory`, etc.) directly.
+
+Additionally, two rules apply to every test that calls a posting codeunit:
+
+1. `SetSuppressCommit(true)` must be called before `Run()` to prevent data
+ from leaking between tests.
+2. `asserterror` must always be followed by `Assert.ExpectedErrorCode()` or
+ `Assert.ExpectedError()` — a naked `asserterror` passes on any error,
+ not just the expected one.
+
+## Anti Pattern
+
+```al
+// WRONG: inline setup bypassing the test library
+procedure MyTest()
+var
+ Item: Record Item;
+begin
+ LibraryInventory.CreateItem(Item); // do not call directly
+ // ...
+end;
+```
+
+```al
+// WRONG: posting without SuppressCommit
+SVPost.Run(SVHeader); // commits to test database
+```
+
+```al
+// WRONG: naked asserterror
+asserterror SVPost.Run(SVHeader);
+// no assertion follows — passes on any error
+```
+
+## Best Practice
+
+```al
+// CORRECT: delegate to test library
+procedure MyTest()
+var
+ Item: Record Item;
+begin
+ SVLib.GivenScrapItem(Item); // test library owns setup
+ // ...
+end;
+```
+
+```al
+// CORRECT: SuppressCommit before Run
+SVPost.SetSuppressCommit(true);
+SVPost.Run(SVHeader);
+```
+
+```al
+// CORRECT: asserterror followed by assertion
+asserterror SVPost.Run(SVHeader);
+Assert.ExpectedErrorCode('Dialog');
+```
From 5c138238503a10fd43c639e2b949cd3cfd6d232e Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sat, 13 Jun 2026 14:10:43 +0200
Subject: [PATCH 02/54] testdata
---
.../test-data-must-be-random-and-complete.md | 88 +++++++++++++++++++
1 file changed, 88 insertions(+)
create mode 100644 custom/knowledge/testing/test-data-must-be-random-and-complete.md
diff --git a/custom/knowledge/testing/test-data-must-be-random-and-complete.md b/custom/knowledge/testing/test-data-must-be-random-and-complete.md
new file mode 100644
index 0000000..74fa33f
--- /dev/null
+++ b/custom/knowledge/testing/test-data-must-be-random-and-complete.md
@@ -0,0 +1,88 @@
+---
+bc-version: [all]
+domain: testing
+keywords: [test, hardcode, random, library, no-series, setup, data]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## 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.
+
+Three concrete rules:
+
+**1. Use MS Library codeunits for standard BC objects.**
+No-series, G/L accounts, customers, vendors, items, locations, posting groups —
+all created via `Library - ERM`, `Library - Inventory`, `Library - Sales` etc.
+These tools generate random codes that do not collide across test runs.
+
+**2. Fill all required fields with random values.**
+A `Code[10]` field gets 10 random characters. A `Text[50]` field gets random text.
+Use `Library - Utility` or `Any` codeunit for random generation.
+Partial setup that leaves required fields empty is not acceptable.
+
+**3. Build your own tools for custom tables.**
+For CURABIS-specific tables (e.g. `Settlement Payment Method`,
+`Settlement Voucher Setup`), maintain dedicated setup procedures in the
+Test Library codeunit. These procedures must follow the same pattern as
+Microsoft's libraries: create records programmatically, use random values
+for codes where no fixed value is required by the flow being tested.
+
+**Exception — integration and flow tests.**
+When a test validates a specific integration contract (e.g. a fixed JSON
+structure from a web service, a specific EDIFACT message, a fixed counterparty
+code expected by an external system), hardcoded values are acceptable and
+necessary. The test is documenting the contract, not exercising random data.
+
+## Anti Pattern
+
+```al
+// WRONG: hardcoded code that may or may not exist
+if not PaymentMethod.Get('CASH') then begin
+ PaymentMethod.Code := 'CASH';
+ ...
+end;
+```
+
+```al
+// WRONG: hardcoded source code
+SourceCode.Code := 'SV-POST';
+```
+
+```al
+// WRONG: partial setup — Code[10] left short
+PaymentMethod.Code := 'C'; // not filled to capacity
+```
+
+## Best Practice
+
+```al
+// CORRECT: random code via LibraryUtility
+PaymentMethod.Code :=
+ CopyStr(LibraryUtility.GenerateRandomCode(
+ PaymentMethod.FieldNo(Code), DATABASE::"Settlement Payment Method"), 1, 10);
+PaymentMethod.Description := LibraryUtility.GenerateRandomText(50);
+PaymentMethod.Insert();
+```
+
+```al
+// CORRECT: source code created via standard MS pattern
+LibraryERM.CreateSourceCode(SourceCode);
+GlobalSourceCode := SourceCode.Code;
+// then assign to Source Code Setup
+```
+
+```al
+// CORRECT: no-series via MS library
+GlobalNoSeriesCode := LibraryUtility.GetGlobalNoSeriesCode();
+```
+
+```al
+// CORRECT: hardcoded in integration test — documenting a contract
+// [SCENARIO] Inbound ORDRSP with fixed order reference from Allnet Germany
+ExpectedOrderRef := 'ORD-2026-00001'; // fixed by integration contract
+```
From a49823a542cd295e5c58be182312fbfad9024b29 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sat, 13 Jun 2026 22:53:17 +0200
Subject: [PATCH 03/54] newrules
---
.altestrunner/config.json | 16 ++++
.../al-identifiers-must-be-english.md | 81 +++++++++++++++++
.../namespace-must-be-verified-from-source.md | 86 +++++++++++++++++++
.../tests-must-adapt-to-existing-code.md | 75 ++++++++++++++++
4 files changed, 258 insertions(+)
create mode 100644 .altestrunner/config.json
create mode 100644 custom/knowledge/architecture/al-identifiers-must-be-english.md
create mode 100644 custom/knowledge/architecture/namespace-must-be-verified-from-source.md
create mode 100644 custom/knowledge/testing/tests-must-adapt-to-existing-code.md
diff --git a/.altestrunner/config.json b/.altestrunner/config.json
new file mode 100644
index 0000000..034f2f5
--- /dev/null
+++ b/.altestrunner/config.json
@@ -0,0 +1,16 @@
+{
+ "containerResultPath": "",
+ "launchConfigName": "",
+ "securePassword": "",
+ "userName": "",
+ "companyName": "",
+ "testSuiteName": "",
+ "vmUserName": "",
+ "vmSecurePassword": "",
+ "remoteContainerName": "",
+ "dockerHost": "",
+ "newPSSessionOptions": "",
+ "testRunnerServiceUrl": "",
+ "codeCoveragePath": ".altestrunner\\codecoverage.json",
+ "culture": "en-US"
+}
\ No newline at end of file
diff --git a/custom/knowledge/architecture/al-identifiers-must-be-english.md b/custom/knowledge/architecture/al-identifiers-must-be-english.md
new file mode 100644
index 0000000..34191db
--- /dev/null
+++ b/custom/knowledge/architecture/al-identifiers-must-be-english.md
@@ -0,0 +1,81 @@
+---
+bc-version: [all]
+domain: architecture
+keywords: [naming, english, enu, variable, procedure, field, caption, translation, xliff]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## Description
+
+All AL identifiers must be written in English (ENU) regardless of the language
+used in conversation with the developer. Translations are handled separately
+via XLIFF files — never by writing Danish, German or other language identifiers
+in AL source code.
+
+This applies to:
+- Variable names
+- Procedure names
+- Parameter names
+- Field names
+- Object names (tables, codeunits, pages, enums, reports)
+- Enum value names
+- Local and global labels (Label data type) — both the identifier and the default text
+
+**Captions and ToolTips** may be in the target language in the source file,
+but must also be covered by XLIFF translations for all supported locales.
+
+## Anti Pattern
+
+```al
+// WRONG: Danish identifiers
+var
+ Kreditor: Record Vendor;
+ Beløb: Decimal;
+ AntalKilo: Decimal;
+
+procedure BeregnTotalbeløb(Antal: Decimal; Pris: Decimal): Decimal
+begin
+ exit(Antal * Pris);
+end;
+
+field(50101; "Indgående Mængde"; Decimal) { Caption = 'Indgående Mængde'; }
+```
+
+## Best Practice
+
+```al
+// CORRECT: English identifiers, Danish captions handled via XLIFF
+var
+ Vendor: Record Vendor;
+ Amount: Decimal;
+ QuantityKg: Decimal;
+
+procedure CalculateTotalAmount(Quantity: Decimal; UnitPrice: Decimal): Decimal
+begin
+ exit(Quantity * UnitPrice);
+end;
+
+field(50101; "Inbound Quantity"; Decimal) { Caption = 'Inbound Quantity'; }
+// Caption translation → da-DK XLIFF: 'Indgående Mængde'
+
+// WRONG: Danish label identifier and text
+var
+ BeløbFejlTxt: Label 'Beløbet må ikke være negativt';
+
+// CORRECT: English label identifier and default text — translated via XLIFF
+var
+ AmountMustNotBeNegativeErr: Label 'Amount must not be negative.', Comment = '%1 = Amount';
+```
+
+## Conversation vs. code
+
+The developer may describe requirements in Danish. The agent must translate
+the intent into English identifiers when writing AL code:
+
+- "opret en variabel til beløbet" → `var Amount: Decimal;`
+- "procedure der beregner lagerværdien" → `procedure CalculateInventoryValue(...)`
+- "felt til indgående mængde" → `field(... ; "Inbound Quantity"; Decimal)`
+
+Never echo Danish words from the conversation directly into AL identifiers.
diff --git a/custom/knowledge/architecture/namespace-must-be-verified-from-source.md b/custom/knowledge/architecture/namespace-must-be-verified-from-source.md
new file mode 100644
index 0000000..3948242
--- /dev/null
+++ b/custom/knowledge/architecture/namespace-must-be-verified-from-source.md
@@ -0,0 +1,86 @@
+---
+bc-version: [all]
+domain: architecture
+keywords: [namespace, using, compile, al-language, tablerelation, variable, codeunit]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## Description
+
+When an agent adds a variable referencing a BC or custom object, it must verify
+the correct namespace by reading the source file of that object — not by guessing
+or relying on its training data.
+
+An AL file that "compiles" in the agent's own build may still show as red in
+VS Code because the AL Language Server resolves namespaces differently.
+The authoritative source for a namespace is always the object's own source file.
+
+This rule applies to:
+- `using` declarations at the top of a codeunit, table, page or enum
+- Variable declarations that reference tables, codeunits, pages or enums
+- `TableRelation` and `CalcFormula` references
+
+## How to verify a namespace
+
+Before adding a `using` statement or a variable referencing an object, the agent
+must locate and read the source file for that object:
+
+```
+// Step 1: Find the source file
+Glob: "**/[ObjectName].*.al" or al_symbolsearch query: "[ObjectName]"
+
+// Step 2: Read the first line — the namespace declaration
+namespace SettlementVoucher.SettlementVoucher; ← this is what to use
+
+// Step 3: Add the using statement in the consuming file
+using SettlementVoucher.SettlementVoucher;
+```
+
+If the object is a Microsoft base application object, use `al_symbolsearch` to
+look up the correct namespace — do not assume it from the object name alone.
+Microsoft namespaces changed significantly from BC24 onwards.
+
+## Anti Pattern
+
+```al
+// WRONG: Guessing the namespace from the object name
+using Microsoft.Purchases.Vendor; // guessed — may be wrong
+using SettlementVoucher; // incomplete — missing sub-namespace
+
+var
+ Vendor: Record Vendor; // missing using → red in AL Language Server
+ SVPost: Codeunit "SV Post"; // wrong namespace → unresolved reference
+```
+
+## Best Practice
+
+```al
+// CORRECT: Read SVPost.Codeunit.al first → find: namespace SettlementVoucher.SettlementVoucher
+// CORRECT: Use al_symbolsearch to find Vendor → namespace Microsoft.Purchases.Vendor
+
+using Microsoft.Purchases.Vendor;
+using Microsoft.Finance.GeneralLedger.Ledger;
+using SettlementVoucher.SettlementVoucher;
+
+codeunit 50204 "SV Incoming Item Flow Tests"
+{
+ var
+ Vendor: Record Vendor;
+ GLEntry: Record "G/L Entry";
+ SVPost: Codeunit "SV Post";
+```
+
+## Verification step before delivering code
+
+After writing any AL file, the agent must:
+
+1. List every `using` statement in the file
+2. For each one: confirm the namespace was read from the actual source file
+ or looked up via `al_symbolsearch` — not assumed
+3. If any namespace was assumed rather than verified, re-read the source and correct it
+
+Never report "compiled successfully" based on a build that did not go through
+the AL Language Server in VS Code. The definitive compilation result is what
+VS Code shows — not the agent's internal build.
diff --git a/custom/knowledge/testing/tests-must-adapt-to-existing-code.md b/custom/knowledge/testing/tests-must-adapt-to-existing-code.md
new file mode 100644
index 0000000..906f02c
--- /dev/null
+++ b/custom/knowledge/testing/tests-must-adapt-to-existing-code.md
@@ -0,0 +1,75 @@
+---
+bc-version: [all]
+domain: testing
+keywords: [test, refactor, adapt, existing-code, green, failing, tdd]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## Description
+
+When a CURABIS developer asks for a test that "must work" or "must pass",
+the agent's job is to write a test that passes against the **existing production code**
+— not to write an idealized test and then report that the code needs changing.
+
+This rule applies in two distinct scenarios:
+
+**Scenario A — New test for existing behaviour**
+The production code is correct and stable. Write the test to match what the code
+actually does. Read the relevant codeunits before writing assertions. If the
+expected value in the story differs from what the code produces, surface the
+discrepancy and ask before assuming either is wrong.
+
+**Scenario B — Refactoring an existing test**
+The test exists but fails because the production code was changed. Adapt the
+test to match the new behaviour. Do not rewrite the production code to make
+the old test pass unless explicitly asked to refactor production code.
+
+## The distinction that matters
+
+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.
+```
+
+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.
+```
+
+## 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.
+
+// [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
+
+1. Read the relevant production codeunits (SVPost, SVApplyMgt, etc.)
+2. Trace the calculation path for the specific scenario
+3. Derive the expected values from the code — not only from the story
+4. If code and story agree → write the test with those values
+5. If code and story disagree → write the test with the code's values AND add
+ a clearly visible `// ⚠️ NOTE` comment explaining the discrepancy
+6. Never leave a test failing when the task was to write a passing test
From f0b473e106841fe798717cb045b7e9b3d553c1ce Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sun, 14 Jun 2026 15:56:45 +0200
Subject: [PATCH 04/54] translate
---
.../architecture/clarify-before-building.md | 94 ++++++++++++++++++
.../xliff-translation-workflow.md | 96 +++++++++++++++++++
2 files changed, 190 insertions(+)
create mode 100644 custom/knowledge/architecture/clarify-before-building.md
create mode 100644 custom/knowledge/architecture/xliff-translation-workflow.md
diff --git a/custom/knowledge/architecture/clarify-before-building.md b/custom/knowledge/architecture/clarify-before-building.md
new file mode 100644
index 0000000..8767219
--- /dev/null
+++ b/custom/knowledge/architecture/clarify-before-building.md
@@ -0,0 +1,94 @@
+---
+bc-version: [all]
+domain: architecture
+keywords: [clarify, ambiguity, requirements, questions, before-coding, task-evaluation]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## Description
+
+Before writing any AL code, the agent must evaluate whether the task is
+unambiguously defined. If the task can be interpreted in more than one way,
+the agent must ask clarifying questions and wait for answers before proceeding.
+
+No code may be written, edited or deleted until the task is 100% clear.
+
+This rule exists because AL code changes affect compiled extensions, running
+BC environments and test databases. An incorrect assumption costs more to
+undo than a clarifying question costs to ask.
+
+## When to ask
+
+Ask before coding if any of the following is true:
+
+- The task mentions an object, field or flow that does not exist yet and the
+ design is not specified
+- The expected behaviour could match more than one existing code path
+- The task involves a business rule (amounts, thresholds, VAT, posting groups)
+ where an assumption could produce silently wrong ledger entries
+- The scope is unclear: "fix this" or "make it work" without specifying what
+ correct behaviour looks like
+- The counterparty type, document type, direction or posting flags are not
+ stated and cannot be unambiguously inferred from context
+- The test assertions reference calculated values that depend on setup data
+ the agent has not yet read
+
+## How to ask
+
+State what you understand the task to be, then list the specific questions:
+
+```
+I understand the task as: [one sentence summary]
+
+Before I proceed, I need clarification on:
+1. [specific question]
+2. [specific question]
+
+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.
+
+## When NOT to ask
+
+Do not ask if:
+- The task is fully specified with object names, field names, expected values
+ and posting behaviour
+- The answer can be determined by reading existing source files or .md
+ documentation in the project
+- The ambiguity is purely cosmetic (formatting, comment wording)
+
+If the answer is in the project's source files or documentation, read those
+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
+```
+
+## Best Practice
+
+```
+// 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.
+
+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.
+```
diff --git a/custom/knowledge/architecture/xliff-translation-workflow.md b/custom/knowledge/architecture/xliff-translation-workflow.md
new file mode 100644
index 0000000..68a8279
--- /dev/null
+++ b/custom/knowledge/architecture/xliff-translation-workflow.md
@@ -0,0 +1,96 @@
+---
+bc-version: [all]
+domain: architecture
+keywords: [xliff, translation, xlf, caption, tooltip, enu, da-dk, de-de, no-nb, sv-se, de-at]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## Description
+
+CURABIS apps support the following locales: da-DK, de-DE, de-AT, nb-NO, sv-SE.
+XLIFF translation is a batch operation — never line-by-line. The agent must
+translate all trans-units in one pass without asking questions per string.
+
+## Tone and register
+
+Follow Microsoft Business Central's translation tone for each locale:
+
+- **da-DK**: Kort, direkte, professionel. Undgå høfligheds-De. Brug infinitiv
+ frem for bydeform. Brug BC-standardtermer: "Bogfør" ikke "Send til bogføring",
+ "Kreditor" ikke "Leverandør", "Finanspost" ikke "Finansregistrering".
+- **de-DE**: Formell, Sie-Form. BC-Standardterminologie: "Buchen", "Kreditor",
+ "Sachposten". Substantive großschreiben.
+- **de-AT**: Identisch mit de-DE. Keine österreichischen Dialektvarianten.
+- **nb-NO**: Kort og profesjonell. BC-standardtermer: "Bokfør", "Leverandør",
+ "Finanspost". Bruk infinitiv.
+- **sv-SE**: Kort, professionell. BC-standardtermer: "Bokför", "Leverantör",
+ "Redovisningspost". Undvik dialekt.
+
+## Workflow — one pass, no questions
+
+When asked to translate an XLIFF file:
+
+1. Read the entire source `.g.xlf` file in one operation
+2. Translate ALL trans-units in memory
+3. Write the complete translated file in one operation
+4. Do not ask questions about individual strings
+5. Do not pause between strings
+6. Do not ask for confirmation per trans-unit
+
+If a term is ambiguous, apply the BC standard term for that locale and add a
+single summary comment at the end — never interrupt the translation to ask.
+
+## Terms that must NOT be translated
+
+The following must remain in English in all locales:
+- Object names used as identifiers (e.g. "Settlement Voucher")
+- Field names that are part of the AL identifier (e.g. "Qty. to Invoice")
+- Company names, product names, app names
+
+## Trans-unit structure
+
+```xml
+
+ Post
+ Bogfør ← da-DK example
+ Button caption
+
+```
+
+State must always be `translated` — never `needs-translation` or `new`.
+
+## Common BC terms reference
+
+| ENU | da-DK | de-DE | nb-NO | sv-SE |
+|---|---|---|---|---|
+| Post | Bogfør | Buchen | Bokfør | Bokför |
+| Vendor | Kreditor | Kreditor | Leverandør | Leverantör |
+| Customer | Debitor | Debitor | Kunde | Kund |
+| Item | Vare | Artikel | Vare | Artikel |
+| G/L Entry | Finanspost | Sachposten | Finanspost | Redovisningspost |
+| Amount | Beløb | Betrag | Beløp | Belopp |
+| Quantity | Antal | Menge | Antall | Antal |
+| Invoice | Faktura | Rechnung | Faktura | Faktura |
+| Receipt | Kvittering | Empfangsschein | Kvittering | Inleverans |
+| Settlement | Afregning | Abrechnung | Avregning | Avräkning |
+| Voucher | Bilag | Beleg | Bilag | Verifikation |
+| Cash | Kontant | Bar | Kontant | Kontant |
+| Threshold | Grænse | Grenzwert | Grense | Gräns |
+| Incoming | Indgående | Eingehend | Inngående | Inkommande |
+| Outgoing | Udgående | Ausgehend | Utgående | Utgående |
+| Handle | Håndter | Verarbeiten | Håndter | Hantera |
+| Weighbridge | Vægt | Fahrzeugwaage | Vekt | Våg |
+| Scrap | Skrot | Schrott | Skrap | Skrot |
+
+## Error and warning messages
+
+Error messages follow the BC pattern:
+- da-DK: Start with capital, end with period. "Du kan ikke bogføre et tomt bilag."
+- de-DE: Formal, Sie-Form. "Sie können keinen leeren Beleg buchen."
+- nb-NO: "Du kan ikke bokføre et tomt bilag."
+- sv-SE: "Du kan inte bokföra ett tomt verifikat."
+
+ToolTip format (da-DK): "Angiver [hvad feltet indeholder]." — always starts
+with "Angiver" for fields, "Åbner" for actions that open pages.
From 0f7021a5b6597c4961593646fbf4612aa5a8cbfc Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Mon, 15 Jun 2026 14:26:40 +0200
Subject: [PATCH 05/54] code refresh
---
.../new-file-requires-vscode-refresh.md | 62 +++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 custom/knowledge/architecture/new-file-requires-vscode-refresh.md
diff --git a/custom/knowledge/architecture/new-file-requires-vscode-refresh.md b/custom/knowledge/architecture/new-file-requires-vscode-refresh.md
new file mode 100644
index 0000000..032d800
--- /dev/null
+++ b/custom/knowledge/architecture/new-file-requires-vscode-refresh.md
@@ -0,0 +1,62 @@
+---
+bc-version: [all]
+domain: architecture
+keywords: [workspace, compile, diagnostics, refresh, al-language, multi-project, new-file]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## Description
+
+When Claude Code creates a new AL file in a multi-project workspace
+(e.g. `Jernpladsen` + `Jernpladsen.Test`), the AL Language Server in VS Code
+may temporarily assign the new file to the wrong project. This causes false
+compilation errors such as:
+
+- Missing references to test codeunits (e.g. `Library Assert`)
+- Object ID out of range for the main app
+- Missing `app.json` dependencies
+
+These errors are **not real** — they disappear after VS Code refreshes its
+project context. Claude Code must not attempt to fix them.
+
+## Rule
+
+After creating a new AL file, Claude Code must:
+
+1. Stop all compilation and diagnostic activity immediately
+2. Instruct the developer to refresh VS Code:
+ `Ctrl+Shift+P → AL: Reload Extension`
+3. Wait for explicit confirmation from the developer that the refresh is done
+4. Only then run `al_getdiagnostics` or `al_compile` to check for real errors
+
+## What NOT to do
+
+- Do not investigate namespace errors that appear immediately after file creation
+- Do not modify `using` statements based on errors seen before a refresh
+- Do not move or rename the file based on pre-refresh diagnostics
+- Do not run `al_compile` or `al_build` immediately after creating a new file
+- Do not report "compilation failed" based on pre-refresh diagnostics
+
+## Signal to watch for
+
+If `al_getdiagnostics` returns errors referencing objects that clearly belong
+to the other project (e.g. `Library Assert` errors in a main app context,
+or ID range errors for a test codeunit), this is a pre-refresh false positive.
+
+Stop. Instruct the developer to refresh. Wait. Then re-run diagnostics.
+
+## Message to developer
+
+When this situation occurs, output exactly this message before stopping:
+
+```
+⚠️ VS Code needs a refresh before I can check for real compilation errors.
+
+Please run: Ctrl+Shift+P → AL: Reload Extension
+
+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.
From 6ccd60311261b10c46b8584333f801296d49aeff Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sat, 20 Jun 2026 08:54:21 +0200
Subject: [PATCH 06/54] Add CURABIS shared eval + evidence scripts to
custom/scripts
- Invoke-CurabisEval.ps1: general compile + analyzers quality eval (hill-climbing score,
reads each project's own al.codeAnalyzers + ruleset, logs .eval/history.jsonl).
- Invoke-CurabisEvidence.ps1: citation evidence validator that fails on hallucinated
knowledge-file or CURABIS rule-code references (cite-or-flag enforcement).
- README documenting both.
Fetched by Setup-CurabisAppSource.ps1 into each project's scripts folder.
Co-Authored-By: Claude Opus 4.8
---
custom/scripts/Invoke-CurabisEval.ps1 | 249 ++++++++++++++++++++++
custom/scripts/Invoke-CurabisEvidence.ps1 | 129 +++++++++++
custom/scripts/README.md | 15 ++
3 files changed, 393 insertions(+)
create mode 100644 custom/scripts/Invoke-CurabisEval.ps1
create mode 100644 custom/scripts/Invoke-CurabisEvidence.ps1
create mode 100644 custom/scripts/README.md
diff --git a/custom/scripts/Invoke-CurabisEval.ps1 b/custom/scripts/Invoke-CurabisEval.ps1
new file mode 100644
index 0000000..572a9c4
--- /dev/null
+++ b/custom/scripts/Invoke-CurabisEval.ps1
@@ -0,0 +1,249 @@
+# Invoke-CurabisEval.ps1
+#
+# Generel "hill climbing"-eval for ETHVERT CURABIS AL-projekt.
+# Maaler det objektive signal paa succesfuld udfoersel: kompilerer koden, og er
+# cop-analyzerne rene -> en score 0..1, logget over tid.
+#
+# Ingen projektspecifik logik:
+# - app-projekter auto-opdages via app.json
+# - analyzere + ruleset laeses fra projektets EGEN .vscode\settings.json
+# (saa scoren maales mod projektets bar, ikke harness'ens mening)
+#
+# Score:
+# - kompilerer ikke (errors > 0) -> 0.0 (du kan ikke klatre foer den bygger)
+# - ellers: 1 / (1 + WarnWeight * warnings) (falder bloedt, floorer ikke)
+# Koer den, aendr EN ting, koer igen, og se trenden i .eval\history.jsonl.
+#
+# Brug:
+# pwsh -File scripts\Invoke-CurabisEval.ps1
+# pwsh -File scripts\Invoke-CurabisEval.ps1 -FailUnder 0.5 # CI-gate
+# pwsh -File scripts\Invoke-CurabisEval.ps1 -AppPath ".apps\summatim"
+
+[CmdletBinding()]
+param(
+ # Repo-rod. Default: foraelder til scripts-mappen (altsaa projektroden).
+ [string]$ProjectRoot,
+
+ # Et eller flere app-projekter (mappe med app.json). Default: auto-opdag.
+ [string[]]$AppPath,
+
+ # Override af analyzere. Default: laes projektets egen al.codeAnalyzers.
+ [ValidateSet('AppSourceCop', 'CodeCop', 'UICop', 'PerTenantExtensionCop')]
+ [string[]]$Analyzers,
+
+ # Vaegt pr. warning i scoren. errors er altid hard-fail -> 0.
+ [double]$WarnWeight = 0.01,
+
+ # Hvis sat: exit 1 naar samlet score < dette tal (til CI).
+ [Nullable[double]]$FailUnder,
+
+ [switch]$Quiet
+)
+
+$ErrorActionPreference = 'Stop'
+
+function Write-Line([string]$msg, [string]$color = 'Gray') {
+ if (-not $Quiet) { Write-Host $msg -ForegroundColor $color }
+}
+
+# --- Find projektrod ---
+if (-not $ProjectRoot) { $ProjectRoot = Split-Path -Parent $PSScriptRoot }
+$ProjectRoot = (Resolve-Path $ProjectRoot).Path
+
+# --- Find AL-compiler + analyzere i nyeste AL Language extension ---
+$ext = Get-ChildItem "$env:USERPROFILE\.vscode\extensions" -Filter 'ms-dynamics-smb.al-*' -ErrorAction SilentlyContinue |
+ Sort-Object Name -Descending | Select-Object -First 1
+if (-not $ext) { throw 'AL Language extension ikke fundet. Installer ms-dynamics-smb.al.' }
+
+$alc = Join-Path $ext.FullName 'bin\win32\alc.exe'
+if (-not (Test-Path $alc)) { throw "alc.exe ikke fundet i $($ext.FullName)" }
+$analyzerDir = Join-Path $ext.FullName 'bin\Analyzers'
+
+# Map fra token/navn -> analyzer-DLL. Tager baade '${CodeCop}' og 'CodeCop'.
+$analyzerDll = @{
+ appsourcecop = 'Microsoft.Dynamics.Nav.AppSourceCop.dll'
+ codecop = 'Microsoft.Dynamics.Nav.CodeCop.dll'
+ uicop = 'Microsoft.Dynamics.Nav.UICop.dll'
+ pertenantextensioncop = 'Microsoft.Dynamics.Nav.PerTenantExtensionCop.dll'
+}
+
+function Read-JsonC([string]$path) {
+ # Laes JSON med // linje-kommentarer (VS Code settings er JSONC).
+ $lines = Get-Content $path | Where-Object { $_.TrimStart() -notlike '//*' }
+ ($lines -join "`n") | ConvertFrom-Json
+}
+
+function Resolve-AnalyzerEntry([string]$entry, [string]$appDir) {
+ # Oversaetter en al.codeAnalyzers-entry til en DLL-sti. Haandterer:
+ # - kendte tokens: ${CodeCop} / CodeCop / ${AppSourceCop} osv.
+ # - custom DLL'er: ${analyzerFolder}BusinessCentral.LinterCop.dll
+ # - relative/absolutte stier til en .dll
+ $key = ($entry -replace '[${}]', '').ToLower()
+ if ($analyzerDll.ContainsKey($key)) { return (Join-Path $analyzerDir $analyzerDll[$key]) }
+ $p = $entry -replace '\$\{analyzerFolder\}', ($analyzerDir + '\')
+ if ($p -match '\$\{') { Write-Line " !! kan ikke resolve analyzer: $entry" 'Yellow'; return $null }
+ if (-not [System.IO.Path]::IsPathRooted($p)) { $p = Join-Path $appDir $p }
+ return $p
+}
+
+function Resolve-Analyzers([string]$appDir) {
+ # Praeferer projektets egen .vscode\settings.json; ellers -Analyzers/default.
+ $entries = @()
+ $settings = Join-Path $appDir '.vscode\settings.json'
+ if (-not $Analyzers -and (Test-Path $settings)) {
+ $s = Read-JsonC $settings
+ $entries = @($s.'al.codeAnalyzers')
+ }
+ if (-not $entries -or $entries.Count -eq 0) {
+ $entries = if ($Analyzers) { $Analyzers } else { @('${AppSourceCop}', '${CodeCop}', '${UICop}') }
+ }
+ $dlls = @()
+ foreach ($e in $entries) {
+ if (-not $e) { continue }
+ $dll = Resolve-AnalyzerEntry $e $appDir
+ if ($dll) {
+ if (Test-Path $dll) { $dlls += $dll } else { Write-Line " !! analyzer-DLL findes ikke: $dll" 'Yellow' }
+ }
+ }
+ return ($dlls | Select-Object -Unique)
+}
+
+function Resolve-Ruleset([string]$appDir) {
+ $settings = Join-Path $appDir '.vscode\settings.json'
+ if (Test-Path $settings) {
+ $s = Read-JsonC $settings
+ $rs = $s.'al.ruleSetPath'
+ if ($rs) {
+ $p = if ([System.IO.Path]::IsPathRooted($rs)) { $rs } else { Join-Path $appDir $rs }
+ if (Test-Path $p) { return (Resolve-Path $p).Path }
+ }
+ }
+ return $null
+}
+
+# --- Find app-projekter ---
+if (-not $AppPath) {
+ $AppPath = Get-ChildItem -Path $ProjectRoot -Recurse -Filter 'app.json' -ErrorAction SilentlyContinue |
+ Where-Object { $_.FullName -notmatch '\\\.alpackages\\' } |
+ ForEach-Object { Split-Path $_.FullName -Parent }
+}
+else {
+ $AppPath = $AppPath | ForEach-Object {
+ if ([System.IO.Path]::IsPathRooted($_)) { $_ } else { Join-Path $ProjectRoot $_ }
+ }
+}
+if (-not $AppPath) { throw "Ingen app.json fundet under $ProjectRoot" }
+
+Write-Line "AL compiler : $alc" 'DarkGray'
+Write-Line ''
+
+$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("curabis-eval-" + [guid]::NewGuid().ToString('N'))
+New-Item -ItemType Directory -Path $tmp -Force | Out-Null
+
+$appResults = @()
+
+foreach ($app in $AppPath) {
+ $app = (Resolve-Path $app).Path
+ $manifest = Join-Path $app 'app.json'
+ if (-not (Test-Path $manifest)) { Write-Line " Springer over (ingen app.json): $app" 'Yellow'; continue }
+ $appJson = Get-Content $manifest -Raw | ConvertFrom-Json
+ $name = $appJson.name
+
+ $analyzerDlls = Resolve-Analyzers $app
+ $ruleset = Resolve-Ruleset $app
+ $analyzerNames = $analyzerDlls | ForEach-Object { [System.IO.Path]::GetFileNameWithoutExtension($_) -replace '^Microsoft\.Dynamics\.Nav\.', '' }
+
+ Write-Line "-> $name" 'Cyan'
+ Write-Line (" analyzere: {0}{1}" -f ($analyzerNames -join ', '), $(if ($ruleset) { " | ruleset: $(Split-Path $ruleset -Leaf)" } else { '' })) 'DarkGray'
+
+ $errorLog = Join-Path $tmp ((Split-Path $app -Leaf) + '.json')
+ $pkgCache = Join-Path $app '.alpackages'
+
+ $alcArgs = @(
+ "/project:$app",
+ "/packagecachepath:$pkgCache",
+ "/outfolder:$tmp",
+ "/errorlog:$errorLog",
+ '/loglevel:Warning'
+ )
+ foreach ($d in $analyzerDlls) { $alcArgs += "/analyzer:$d" }
+ if ($ruleset) { $alcArgs += "/ruleset:$ruleset" }
+
+ & $alc @alcArgs 2>&1 | Out-Null
+ $alcExit = $LASTEXITCODE
+
+ # --- Parse diagnostik ---
+ # alc /errorlog skriver legacy-format (version 0.2): { issues: [ { ruleId,
+ # properties.severity } ] }. Nyere compilere kan skrive SARIF 2.x
+ # (runs[].results[].level). Vi haandterer begge.
+ $errors = 0; $warnings = 0; $byRule = @{}
+ if (Test-Path $errorLog) {
+ $log = Get-Content $errorLog -Raw | ConvertFrom-Json
+ $diags = @()
+ if ($log.PSObject.Properties.Name -contains 'issues') {
+ foreach ($i in $log.issues) { $diags += [PSCustomObject]@{ rule = "$($i.ruleId)"; sev = "$($i.properties.severity)" } }
+ }
+ elseif ($log.PSObject.Properties.Name -contains 'runs') {
+ foreach ($run in $log.runs) { foreach ($r in $run.results) { $diags += [PSCustomObject]@{ rule = "$($r.ruleId)"; sev = "$($r.level)" } } }
+ }
+ foreach ($d in $diags) {
+ $sev = $d.sev.ToLower()
+ if ($sev -ne 'error' -and $sev -ne 'warning') { continue } # spring Info over
+ if ($sev -eq 'error') { $errors++ } else { $warnings++ }
+ if ($d.rule) { if ($byRule.ContainsKey($d.rule)) { $byRule[$d.rule]++ } else { $byRule[$d.rule] = 1 } }
+ }
+ }
+ elseif ($alcExit -ne 0) { $errors = 1 }
+
+ # --- Score: errors = hard fail (0). Ellers bloed warning-kurve. ---
+ if ($errors -gt 0) { $score = 0.0 }
+ else { $score = [Math]::Round(1.0 / (1.0 + $WarnWeight * $warnings), 3) }
+
+ $color = if ($errors -gt 0) { 'Red' } elseif ($warnings -gt 0) { 'Yellow' } else { 'Green' }
+ Write-Line (" errors={0} warnings={1} score={2}" -f $errors, $warnings, $score) $color
+
+ # Top-overtraedelser (hjaelper med at vide hvad man skal fixe foerst)
+ if (-not $Quiet -and $byRule.Count -gt 0) {
+ $top = $byRule.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 5
+ Write-Line (" top: " + (($top | ForEach-Object { "$($_.Key)x$($_.Value)" }) -join ' ')) 'DarkGray'
+ }
+
+ $appResults += [PSCustomObject]@{
+ app = $name
+ path = $app
+ analyzers = $analyzerNames
+ ruleset = $(if ($ruleset) { Split-Path $ruleset -Leaf } else { $null })
+ errors = $errors
+ warnings = $warnings
+ byRule = $byRule
+ score = $score
+ }
+}
+
+# --- Samlet score = gennemsnit over apps ---
+$overall = if ($appResults.Count -gt 0) { [Math]::Round(($appResults | Measure-Object -Property score -Average).Average, 3) } else { 0.0 }
+
+$run = [PSCustomObject]@{
+ timestamp = (Get-Date).ToString('o')
+ overall = $overall
+ apps = $appResults
+}
+
+# --- Skriv resultat + historik ---
+$evalDir = Join-Path $ProjectRoot '.eval'
+New-Item -ItemType Directory -Path $evalDir -Force | Out-Null
+$run | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $evalDir 'last-run.json') -Encoding UTF8
+($run | ConvertTo-Json -Depth 10 -Compress) | Add-Content (Join-Path $evalDir 'history.jsonl') -Encoding UTF8
+
+Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
+
+Write-Line ''
+Write-Line ("=== SAMLET SCORE: {0} ===" -f $overall) $(if ($overall -ge 0.9) { 'Green' } elseif ($overall -ge 0.5) { 'Yellow' } else { 'Red' })
+Write-Line "Historik: $($evalDir)\history.jsonl" 'DarkGray'
+
+# --- CI-gate ---
+if ($null -ne $FailUnder -and $overall -lt $FailUnder) {
+ Write-Line ("FAIL: score {0} < taerskel {1}" -f $overall, $FailUnder) 'Red'
+ exit 1
+}
+exit 0
diff --git a/custom/scripts/Invoke-CurabisEvidence.ps1 b/custom/scripts/Invoke-CurabisEvidence.ps1
new file mode 100644
index 0000000..725315a
--- /dev/null
+++ b/custom/scripts/Invoke-CurabisEvidence.ps1
@@ -0,0 +1,129 @@
+# Invoke-CurabisEvidence.ps1
+#
+# Validerer at hver citation i en gemt review/audit/triage-rapport peger paa noget
+# der FAKTISK findes - hegnet mod hallucinerede henvisninger (jf. CURABIS-TRIAGE-005
+# "cite or flag" og ALDC's validate_evidence.py).
+#
+# Tjekker to slags citationer:
+# 1. Knowledge-filer - BCQuality-URL'er eller relative stier (custom/.., microsoft/..)
+# -> skal resolve mod en lokal BCQuality-klon eller via HTTP.
+# 2. CURABIS-regelkoder - CURABIS-ARCH/TRIAGE/COMPLEXITY-NNN
+# -> skal vaere defineret i .github\.agents\*.agent.md.
+# (AL-diagnostikkoder som AS0084/AA0218 er Microsofts og valideres ikke her.)
+#
+# Exit 1 hvis en eneste citation ikke kan resolves (egnet til CI / PR-gate).
+#
+# Brug:
+# pwsh -File scripts\Invoke-CurabisEvidence.ps1 -ReportPath review.md
+# "AL Triage cited CURABIS-ARCH-002" | pwsh -File scripts\Invoke-CurabisEvidence.ps1
+
+[CmdletBinding()]
+param(
+ # Rapportfil der skal valideres. Kan ogsaa pipes ind paa stdin.
+ [Parameter(ValueFromPipeline = $true)]
+ [string]$ReportPath,
+
+ [string]$ProjectRoot,
+
+ # Lokal BCQuality-klon. Default: proev ..\bcquality og .\.bcquality.
+ [string]$BCQualityHome,
+
+ # Bruges naar der ikke er en lokal klon: knowledge-filer HTTP-tjekkes herfra.
+ [string]$RawBase = 'https://raw.githubusercontent.com/Curabis/BCQuality/main',
+
+ [switch]$Quiet
+)
+
+$ErrorActionPreference = 'Stop'
+
+function Write-Line([string]$msg, [string]$color = 'Gray') {
+ if (-not $Quiet) { Write-Host $msg -ForegroundColor $color }
+}
+
+# --- Projektrod ---
+if (-not $ProjectRoot) { $ProjectRoot = Split-Path -Parent $PSScriptRoot }
+$ProjectRoot = (Resolve-Path $ProjectRoot).Path
+
+# --- Hent rapport-tekst (fil eller stdin) ---
+$report = $null
+if ($ReportPath -and (Test-Path $ReportPath)) {
+ $report = Get-Content $ReportPath -Raw
+}
+elseif ($ReportPath) {
+ # Ikke en sti -> behandl som raa tekst (fx pipet ind)
+ $report = $ReportPath
+}
+if (-not $report) { throw "Ingen rapport. Angiv -ReportPath eller pipe tekst ind." }
+
+# --- Find lokal BCQuality-klon ---
+if (-not $BCQualityHome) {
+ foreach ($c in @((Join-Path $ProjectRoot '..\bcquality'), (Join-Path $ProjectRoot '.bcquality'))) {
+ if (Test-Path $c) { $BCQualityHome = (Resolve-Path $c).Path; break }
+ }
+}
+$useLocal = [bool]$BCQualityHome -and (Test-Path $BCQualityHome)
+Write-Line ("Validering: {0}" -f $(if ($useLocal) { "lokal klon $BCQualityHome" } else { "HTTP mod $RawBase" })) 'DarkGray'
+
+# --- Udtraek citationer ---
+# Knowledge-stier: fra URL'er (efter .../main/) og relative custom|microsoft|community-stier.
+$paths = New-Object System.Collections.Generic.HashSet[string]
+foreach ($m in [regex]::Matches($report, '(?<=BCQuality/(?:main|master)/)[^\s)\"''<>]+\.md')) { [void]$paths.Add($m.Value) }
+foreach ($m in [regex]::Matches($report, '(?]+\.md)')) { [void]$paths.Add($m.Groups[1].Value) }
+
+# CURABIS-regelkoder
+$codes = New-Object System.Collections.Generic.HashSet[string]
+foreach ($m in [regex]::Matches($report, 'CURABIS-[A-Z]+-\d+')) { [void]$codes.Add($m.Value) }
+
+# --- Byg saet af gyldige regelkoder fra agent-filerne ---
+$validCodes = New-Object System.Collections.Generic.HashSet[string]
+$agentDir = Join-Path $ProjectRoot '.github\.agents'
+if (Test-Path $agentDir) {
+ foreach ($f in Get-ChildItem $agentDir -Filter '*.agent.md') {
+ $txt = Get-Content $f.FullName -Raw
+ foreach ($m in [regex]::Matches($txt, 'CURABIS-[A-Z]+-\d+')) { [void]$validCodes.Add($m.Value) }
+ }
+}
+
+$results = @()
+
+# --- Valider knowledge-filer ---
+foreach ($p in $paths) {
+ $ok = $false
+ if ($useLocal) {
+ $ok = Test-Path (Join-Path $BCQualityHome ($p -replace '/', '\'))
+ }
+ else {
+ try {
+ $resp = Invoke-WebRequest -Uri "$RawBase/$p" -Method Head -UseBasicParsing -TimeoutSec 15
+ $ok = ($resp.StatusCode -eq 200)
+ } catch { $ok = $false }
+ }
+ $results += [PSCustomObject]@{ kind = 'file'; citation = $p; resolved = $ok }
+}
+
+# --- Valider regelkoder ---
+foreach ($c in $codes) {
+ $results += [PSCustomObject]@{ kind = 'rule'; citation = $c; resolved = $validCodes.Contains($c) }
+}
+
+# --- Rapport ---
+Write-Line ''
+if ($results.Count -eq 0) {
+ Write-Line 'Ingen citationer fundet i rapporten.' 'Yellow'
+ exit 0
+}
+
+$missing = 0
+foreach ($r in ($results | Sort-Object kind, citation)) {
+ if ($r.resolved) { Write-Line (" OK [{0}] {1}" -f $r.kind, $r.citation) 'Green' }
+ else { Write-Line (" MISSING [{0}] {1}" -f $r.kind, $r.citation) 'Red'; $missing++ }
+}
+
+Write-Line ''
+$total = $results.Count
+if ($missing -gt 0) {
+ Write-Line ("FAIL: {0}/{1} citationer kunne ikke resolves (hallucineret?)." -f $missing, $total) 'Red'
+ exit 1
+}
+Write-Line ("OK: alle {0} citationer resolver." -f $total) 'Green'
+exit 0
diff --git a/custom/scripts/README.md b/custom/scripts/README.md
new file mode 100644
index 0000000..22c8a58
--- /dev/null
+++ b/custom/scripts/README.md
@@ -0,0 +1,15 @@
+# custom/scripts
+
+CURABIS shared PowerShell tooling, fetched by `Setup-CurabisAppSource.ps1` into each
+project's `scripts\` (or `Scripts\`) folder.
+
+- **Invoke-CurabisEval.ps1** — general "hill climbing" quality eval. Compiles every app
+ with the project's own analyzers and emits a score (errors = hard fail; warnings lower it
+ on a soft curve), logged to `.eval\history.jsonl`. Run it, change one thing, run it again.
+ - `pwsh -File scripts\Invoke-CurabisEval.ps1`
+ - `pwsh -File scripts\Invoke-CurabisEval.ps1 -FailUnder 0.5` (CI gate)
+
+- **Invoke-CurabisEvidence.ps1** — enforces "cite or flag". Validates that every citation
+ in a saved review/triage report (knowledge files + `CURABIS-*` rule codes) actually
+ exists. Fails on hallucinated citations.
+ - `pwsh -File scripts\Invoke-CurabisEvidence.ps1 -ReportPath review.md`
From 09e4e591921ab36e2ffc262180aa2f63587c5d06 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sat, 20 Jun 2026 11:35:01 +0200
Subject: [PATCH 07/54] exposed
---
...sed-objects-must-be-in-a-permission-set.md | 41 +++++++++++++++++++
1 file changed, 41 insertions(+)
create mode 100644 custom/knowledge/architecture/exposed-objects-must-be-in-a-permission-set.md
diff --git a/custom/knowledge/architecture/exposed-objects-must-be-in-a-permission-set.md b/custom/knowledge/architecture/exposed-objects-must-be-in-a-permission-set.md
new file mode 100644
index 0000000..6012ca5
--- /dev/null
+++ b/custom/knowledge/architecture/exposed-objects-must-be-in-a-permission-set.md
@@ -0,0 +1,41 @@
+# Exposed objects must be in at least one permission set
+
+**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:
+
+- API pages (`PageType = API`)
+- Web-service-enabled pages and queries (`ServiceEnabled = true`, published web services)
+- API queries
+
+## Why
+
+An exposed object that is in no permission set is **unusable and invisible** to the users
+and service identities that are supposed to call it. This is exactly how the MCP API pages
+(`CUR MCP Projects`, `CUR MCP Active Tasks`, `CUR MCP Task Comments`) failed: the tables
+behind them were granted, but the pages themselves had no `= X` execute permission, so the
+MCP server could not see or call them.
+
+It is also a **governance gap**: an endpoint that nobody deliberately put in a permission
+set is an endpoint nobody is deciding who may reach. Exposure must be an explicit choice.
+
+## How to apply
+
+1. For every exposed object, add an execute entry (`page "..." = X`, `query "..." = X`) to
+ a permission set in the app.
+2. **Sensitive endpoints go in a dedicated admin permission set** (e.g. `CUR ... Admin`)
+ that is *not* part of the default assignable set — so reaching them is a deliberate grant,
+ not the default.
+3. If an object should not be reachable from outside at all, **remove the exposure** instead
+ (drop `PageType = API` / `ServiceEnabled`) rather than leaving an orphaned endpoint.
+
+## How to check
+
+Scan the app for exposed objects and verify each is referenced in a permission set:
+
+- find every `PageType = API`, `ServiceEnabled = true`, and API query
+- confirm each appears as a `page`/`query` `= X` entry in at least one `permissionset`
+- flag any exposed object with no permission-set membership
+
+A reviewer (or an automated check) should fail the change if an exposed object is missing
+from every permission set.
From 935d756f059e884253c7651a81164ec436f11057 Mon Sep 17 00:00:00 2001
From: Michael Dieringer
Date: Sat, 20 Jun 2026 13:48:06 +0200
Subject: [PATCH 08/54] Add mcp knowledge category with 5 rules
Rules derived from BC MCP API page development experience:
- api-page-flowfields-must-be-calcfields: FlowFields return empty on API
pages unless explicitly CalcFields'd in OnAfterGetRecord
- stored-derived-fields-must-not-be-exposed-directly: Stored fields updated
only via OnValidate triggers can be stale; recalculate live in OnAfterGetRecord
- api-page-key-fields-must-be-editable-on-insert: ODataKeyFields with
Editable=false are rejected as unknown properties on POST
- api-page-least-privilege-write-access: Create dedicated minimal pages per
write concern rather than widening general-purpose pages
- agent-must-not-write-business-process-status: Agents must only write
developer-tracking fields; business status fields affect invoicing/time registration
Co-Authored-By: Claude Sonnet 4.6
---
...-must-not-write-business-process-status.md | 32 ++++++++++++++
.../api-page-flowfields-must-be-calcfields.md | 32 ++++++++++++++
...e-key-fields-must-be-editable-on-insert.md | 40 +++++++++++++++++
.../api-page-least-privilege-write-access.md | 42 ++++++++++++++++++
...ved-fields-must-not-be-exposed-directly.md | 44 +++++++++++++++++++
5 files changed, 190 insertions(+)
create mode 100644 custom/knowledge/mcp/agent-must-not-write-business-process-status.md
create mode 100644 custom/knowledge/mcp/api-page-flowfields-must-be-calcfields.md
create mode 100644 custom/knowledge/mcp/api-page-key-fields-must-be-editable-on-insert.md
create mode 100644 custom/knowledge/mcp/api-page-least-privilege-write-access.md
create mode 100644 custom/knowledge/mcp/stored-derived-fields-must-not-be-exposed-directly.md
diff --git a/custom/knowledge/mcp/agent-must-not-write-business-process-status.md b/custom/knowledge/mcp/agent-must-not-write-business-process-status.md
new file mode 100644
index 0000000..4b992fb
--- /dev/null
+++ b/custom/knowledge/mcp/agent-must-not-write-business-process-status.md
@@ -0,0 +1,32 @@
+# CURABIS MCP: Agents Must Not Write Business Process Status Fields
+
+## Core Principle
+
+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.
+
+## The Distinction
+
+| Field type | Examples | Agent may write |
+|---|---|---|
+| Developer tracking | `gitHubDevStatus`, `gitHubBranch` | Yes |
+| Business process status | Task `Status` (Accepted, In progress, Done) | Never |
+
+Developer tracking fields are independent of BC workflow. Business process status fields control what users can do — for example, a task marked as ready for invoicing cannot receive new time entries.
+
+## Requirements
+
+- API pages exposed to MCP agents must mark business status fields as `Editable = false`
+- Agent instructions (`.agent.md` files) must explicitly list which fields the agent may write
+- Any field that affects time registration, posting, approval, or invoicing is a business process field and must be read-only for agents
+- Developer-managed fields (GitHub dev status, branch, comments) are the only writable surface
+
+## Example Agent Instruction
+
+```
+Write only gitHubDevStatus and gitHubBranch on tasks.
+Never write Status — it controls the invoicing workflow.
+```
+
+## Verification
+
+For each API page with write access, verify that fields controlling BC workflow transitions carry `Editable = false`. Review the agent instruction file to confirm it names the allowed writable fields explicitly and prohibits status fields.
diff --git a/custom/knowledge/mcp/api-page-flowfields-must-be-calcfields.md b/custom/knowledge/mcp/api-page-flowfields-must-be-calcfields.md
new file mode 100644
index 0000000..7dee481
--- /dev/null
+++ b/custom/knowledge/mcp/api-page-flowfields-must-be-calcfields.md
@@ -0,0 +1,32 @@
+# CURABIS MCP: FlowFields on API Pages Must Be CalcFields'd
+
+## Core Principle
+
+FlowFields on API pages return empty or zero unless explicitly calculated. Every FlowField exposed on a `PageType = API` page must be called via `CalcFields` in the `OnAfterGetRecord` trigger — otherwise the OData response will contain empty values regardless of what the underlying data contains.
+
+## Why This Happens
+
+FlowFields are not stored in the database. Business Central only calculates them on demand. Regular pages trigger calculation automatically as part of the page rendering pipeline. API pages do not — the agent or external consumer receives the raw stored (empty) value.
+
+## Requirements
+
+- All FlowFields exposed in the `layout` section of an API page must be listed in a `CalcFields()` call in `OnAfterGetRecord`
+- If multiple FlowFields are needed, they can be combined in a single call: `Rec.CalcFields(Field1, Field2)`
+- Stored fields (non-FlowField) do not need CalcFields
+
+## Example
+
+```al
+trigger OnAfterGetRecord()
+begin
+ Rec.CalcFields("Elapsed time (Chargeable)", "Customer Name");
+end;
+```
+
+## Verification
+
+When reviewing an API page, identify every field bound to a FlowField source expression. Confirm each appears in the `OnAfterGetRecord` CalcFields call. Any FlowField missing from CalcFields is a defect — it will silently return empty to the MCP consumer.
+
+## Related Rule
+
+CURABIS-MCP-002 — Stored derived fields must be recalculated in OnAfterGetRecord, not exposed directly.
diff --git a/custom/knowledge/mcp/api-page-key-fields-must-be-editable-on-insert.md b/custom/knowledge/mcp/api-page-key-fields-must-be-editable-on-insert.md
new file mode 100644
index 0000000..f7d05b7
--- /dev/null
+++ b/custom/knowledge/mcp/api-page-key-fields-must-be-editable-on-insert.md
@@ -0,0 +1,40 @@
+# CURABIS MCP: ODataKeyFields Must Be Editable for Create Operations
+
+## Core Principle
+
+Fields declared in `ODataKeyFields` that identify the record must not have `Editable = false` when the API page allows insert. If they are read-only, the OData API rejects them as unknown properties on POST — the create operation fails and the caller receives a `BadRequest` error.
+
+## Why This Happens
+
+`Editable = false` on a page field removes the field from the OData write schema entirely. When a consumer POSTs a new record and includes the key field in the body, BC cannot match it to any writable property and rejects the request.
+
+## Pattern to Avoid
+
+```al
+// WRONG: Key field marked Editable = false — cannot be set on create
+field(projectNo; Rec."Project No.")
+{
+ Caption = 'projectNo';
+ Editable = false; // blocks insert via API
+}
+```
+
+## Correct Pattern
+
+```al
+// CORRECT: No Editable = false — BC controls mutability after insert via ODataKeyFields
+field(projectNo; Rec."Project No.")
+{
+ Caption = 'projectNo';
+}
+```
+
+## Requirements
+
+- Fields listed in `ODataKeyFields` must not carry `Editable = false` on pages where `InsertAllowed = true`
+- Fields that should be read-only after creation but writable on insert need no special property — OData key semantics handle immutability after the record exists
+- Non-key fields that are genuinely read-only may still use `Editable = false`
+
+## Verification
+
+On any API page with `InsertAllowed = true`, confirm that every field referenced in `ODataKeyFields` does not have `Editable = false` in its field definition. A create test via the OData endpoint is the definitive check.
diff --git a/custom/knowledge/mcp/api-page-least-privilege-write-access.md b/custom/knowledge/mcp/api-page-least-privilege-write-access.md
new file mode 100644
index 0000000..6aa5be4
--- /dev/null
+++ b/custom/knowledge/mcp/api-page-least-privilege-write-access.md
@@ -0,0 +1,42 @@
+# CURABIS MCP: API Pages Must Use Least-Privilege Write Access
+
+## Core Principle
+
+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.
+
+## Why This Matters
+
+An MCP agent operates with the permissions of its service identity, not an individual user. A page that allows writing to many fields gives the agent broad power that is hard to audit and easy to misuse. A dedicated page with one writable field makes the intent explicit and the surface area auditable.
+
+## 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
+```
+
+## 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") { }
+}
+```
+
+## Requirements
+
+- Each distinct write concern (e.g., setting a GitHub repo, updating a dev status) should have its own API page or be deliberately grouped only with closely related fields
+- Read-only fields on write-enabled pages must carry `Editable = false`
+- The page description must document which fields are writable and why
+
+## Verification
+
+For each API page where `ModifyAllowed = true` (or default), list all fields without `Editable = false`. Confirm that every writable field is intentionally writable for the same consumer use case. If unrelated fields are writable on the same page, split the page.
diff --git a/custom/knowledge/mcp/stored-derived-fields-must-not-be-exposed-directly.md b/custom/knowledge/mcp/stored-derived-fields-must-not-be-exposed-directly.md
new file mode 100644
index 0000000..4480663
--- /dev/null
+++ b/custom/knowledge/mcp/stored-derived-fields-must-not-be-exposed-directly.md
@@ -0,0 +1,44 @@
+# CURABIS MCP: Stored Derived Fields Must Be Recalculated in OnAfterGetRecord
+
+## Core Principle
+
+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") { }
+```
+
+`"Time left"` is recalculated only when `"Estimated time"` is validated. If new time entries are posted, the stored value does not update.
+
+## Correct Pattern
+
+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;
+
+var
+ TimeLeftCalc: Decimal;
+
+// In layout:
+field(timeLeft; TimeLeftCalc) { } // live value
+field(elapsedTime; Rec."Elapsed time (Chargeable)") { } // source FlowField
+```
+
+## Requirements
+
+- Identify stored fields whose value is computed from other fields via triggers
+- Do not expose them directly in API pages
+- Recalculate from the authoritative source (FlowField or live query) in `OnAfterGetRecord`
+- Expose both the recalculated result and the source FlowField so the consumer can verify
+
+## Verification
+
+Inspect the source table for any field with `FieldClass = Normal` whose value is set inside an `OnValidate` trigger on another field. If that field is exposed on an API page, verify it is recalculated in `OnAfterGetRecord` rather than read from `Rec` directly.
From f058095decfce921d57dd2c6ad15806178ac5342 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sat, 20 Jun 2026 21:38:12 +0200
Subject: [PATCH 09/54] Add 3 testing knowledge files from book review
- test-one-when-per-test: one WHEN per test, split if multiple actions
- ui-test-codeunit-naming: _UT suffix for TestPage-based codeunits
- test-feature-scenario-tags: [FEATURE]/[SCENARIO] comment structure
Based on patterns from Automatiserede tests med Business Central (Dieringer).
Co-Authored-By: Claude Sonnet 4.6
---
.../testing/test-feature-scenario-tags.md | 108 ++++++++++++++++++
.../testing/test-one-when-per-test.md | 84 ++++++++++++++
.../testing/ui-test-codeunit-naming.md | 102 +++++++++++++++++
3 files changed, 294 insertions(+)
create mode 100644 custom/knowledge/testing/test-feature-scenario-tags.md
create mode 100644 custom/knowledge/testing/test-one-when-per-test.md
create mode 100644 custom/knowledge/testing/ui-test-codeunit-naming.md
diff --git a/custom/knowledge/testing/test-feature-scenario-tags.md b/custom/knowledge/testing/test-feature-scenario-tags.md
new file mode 100644
index 0000000..2cc64e1
--- /dev/null
+++ b/custom/knowledge/testing/test-feature-scenario-tags.md
@@ -0,0 +1,108 @@
+---
+bc-version: [all]
+domain: testing
+keywords: [test, feature, scenario, given, when, then, tags, bdd, atdd, comments, structure]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## Description
+
+CURABIS test codeunits follow a four-level comment structure that traces directly
+to BDD/ATDD (Behaviour-Driven / Acceptance-Test-Driven Development):
+
+| Tag | Scope | Purpose |
+|---|---|---|
+| `// [FEATURE]` | Codeunit header | The domain or module under test (e.g. `Find Price`) |
+| `// [SCENARIO]` | Per test procedure | One falsifiable business claim |
+| `// [GIVEN]` | Inside test body | Preconditions and test data setup |
+| `// [WHEN]` | Inside test body | The single action being tested |
+| `// [THEN]` | Inside test body | Assertions |
+
+The `[FEATURE]` tag appears once as a comment at the top of the codeunit (before
+`{`) and names the functional area — not the object name. The `[SCENARIO]` tag
+appears as a comment immediately above each `[Test]` procedure; it describes the
+business scenario in plain language, complementing the procedure name.
+
+This structure makes tests readable as a living specification. A product owner
+or QA engineer can scan the `[FEATURE]` + `[SCENARIO]` tags to understand
+what is covered without reading AL.
+
+## Anti Pattern
+
+```al
+// 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;
+}
+```
+
+## Best Practice
+
+```al
+// [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";
+
+ // [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;
+}
+```
+
+## Relationship to procedure naming
+
+The procedure name and the `[SCENARIO]` comment are complementary — they say the
+same thing in different registers:
+
+- Procedure name: `GetPrice_CustomerPrice_ReturnsUnitPrice` — machine-readable,
+ shows up in test runner output.
+- `[SCENARIO]` comment: `Customer with a specific price list line gets that unit price`
+ — human-readable, business language.
+
+Both must be present. Neither replaces the other.
diff --git a/custom/knowledge/testing/test-one-when-per-test.md b/custom/knowledge/testing/test-one-when-per-test.md
new file mode 100644
index 0000000..ee89ca8
--- /dev/null
+++ b/custom/knowledge/testing/test-one-when-per-test.md
@@ -0,0 +1,84 @@
+---
+bc-version: [all]
+domain: testing
+keywords: [test, when, scenario, single-action, bdd, atdd, given-when-then]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## Description
+
+Each test procedure must contain exactly **one** `[WHEN]` block — one action that
+triggers the behaviour under test. A test with multiple WHENs ("do A, then do B,
+then check C") is really two or more tests in disguise. Split them.
+
+This constraint serves two purposes:
+
+1. **Failure isolation** — when the test fails you know which action caused it.
+2. **Readable specification** — each test reads as a single, falsifiable claim
+ about the system's behaviour.
+
+A scenario that genuinely requires a precondition action (e.g. "post an order so
+that a ledger entry exists") belongs in `[GIVEN]`. Only the action being asserted
+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;
+```
+
+## Best Practice
+
+```al
+// 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 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
+
+The procedure name should make the single WHEN self-evident.
+A name with "And" or "Then" in the middle is a strong signal to split:
+
+- `GetPrice_AndDiscount_ReturnsValues` → split
+- `GetPrice_CustomerPrice_ReturnsUnitPrice` → good
diff --git a/custom/knowledge/testing/ui-test-codeunit-naming.md b/custom/knowledge/testing/ui-test-codeunit-naming.md
new file mode 100644
index 0000000..abde2ea
--- /dev/null
+++ b/custom/knowledge/testing/ui-test-codeunit-naming.md
@@ -0,0 +1,102 @@
+---
+bc-version: [all]
+domain: testing
+keywords: [test, ui, testpage, naming, suffix, codeunit, page-testing]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+---
+
+## Description
+
+Test codeunits that interact with pages via `TestPage` must carry a `_UT` suffix
+(Unit Test — UI layer) in their name. This distinguishes them from codeunits that
+test business logic directly by calling codeunit/table procedures.
+
+The suffix signals to every reader that the codeunit opens pages, uses
+`TestPage.OpenNew()`, reads FactBox parts, or drives field validates through
+the page's `OnValidate` triggers — i.e. it exercises the UI layer, not just
+the logic layer.
+
+**Convention:**
+
+| Layer tested | Suffix | Example |
+|---|---|---|
+| Business logic (codeunits, tables) | *(none)* | `FindPriceTesting` |
+| Page / UI layer (`TestPage`) | `_UT` | `FindPriceTesting_UT` |
+
+A codeunit may contain **only** UI tests or **only** logic tests — never mix both
+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();
+ ...
+ end;
+}
+```
+
+## Best Practice
+
+```al
+// 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;
+}
+
+// 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
+
+Allocate adjacent IDs for the two related codeunits (e.g. 99006 logic, 99007 UI)
+so they sort together in the object list and their relationship is self-evident.
From 42a02b9c0f33f6c8faf3a8d2e44aa3bc37636877 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sun, 21 Jun 2026 09:32:24 +0200
Subject: [PATCH 10/54] Add architecture rule: shared project memory must be in
repo
---
.../shared-project-memory-must-be-in-repo.md | 76 +++++++++++++++++++
1 file changed, 76 insertions(+)
create mode 100644 custom/knowledge/architecture/shared-project-memory-must-be-in-repo.md
diff --git a/custom/knowledge/architecture/shared-project-memory-must-be-in-repo.md b/custom/knowledge/architecture/shared-project-memory-must-be-in-repo.md
new file mode 100644
index 0000000..da5b939
--- /dev/null
+++ b/custom/knowledge/architecture/shared-project-memory-must-be-in-repo.md
@@ -0,0 +1,76 @@
+---
+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
+---
+
+# Shared Project Memory Must Be in the Repository
+
+## Description
+
+When Claude learns something relevant to the project — a business rule, an architectural
+decision, a known limitation, a scope boundary — that knowledge must be written to the
+repository's `projectmemory/` folder, not to the local user memory store
+(`~/.claude/projects/.../memory/`).
+
+Local memory is invisible to other team members and disappears when someone works on
+a different machine. Version-controlled memory is shared, attributed, and persistent.
+
+## Anti Pattern
+
+```
+# 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
+```
+
+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
+
+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_.md` for the active user.
+
+User-specific preferences (tone, workflow habits) stay in the local
+`~/.claude/projects/.../memory/` folder as before.
+```
+
+## What belongs in projectmemory vs local memory
+
+| projectmemory/ (shared, in git) | local memory (personal, not shared) |
+|---|---|
+| Business rules ("B2B only, no VAT") | User's preferred communication language |
+| Architectural decisions and rationale | Workflow habits and preferences |
+| Scope boundaries ("IC via ChangeCompany only") | Personal shortcuts or shortcuts |
+| Known technical debt and migration status | Role and seniority (already known by the team) |
+| Test coverage scope | |
+| Company/entity structure | |
+
+## Implementation checklist
+
+- [ ] Create `projectmemory/` folder in repo root
+- [ ] Add `projectmemory/**` to `cspell.json` ignorePaths (notes are often in the team's native language)
+- [ ] Add the read-at-session-start instruction to `CLAUDE.md`
+- [ ] When learning something project-relevant, write to `projectmemory/memoryupdates_.md`
+- [ ] Commit and push — knowledge is only shared once it is in git
From bdda44ef8affab43cc89764af41e3ccf45ade194 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sun, 21 Jun 2026 11:00:06 +0200
Subject: [PATCH 11/54] Add BC MCP task lookup recipe and commit message task
ID rule
---
.../commit-message-must-include-bc-task-id.md | 63 +++++++++++++
.../mcp/bc-mcp-find-active-task-for-branch.md | 89 +++++++++++++++++++
2 files changed, 152 insertions(+)
create mode 100644 custom/knowledge/architecture/commit-message-must-include-bc-task-id.md
create mode 100644 custom/knowledge/mcp/bc-mcp-find-active-task-for-branch.md
diff --git a/custom/knowledge/architecture/commit-message-must-include-bc-task-id.md b/custom/knowledge/architecture/commit-message-must-include-bc-task-id.md
new file mode 100644
index 0000000..a12aa09
--- /dev/null
+++ b/custom/knowledge/architecture/commit-message-must-include-bc-task-id.md
@@ -0,0 +1,63 @@
+---
+name: commit-message-must-include-bc-task-id
+description: >
+ Every commit message must begin with the BC task ID in [#id] format,
+ where id is the global taskId from the CUR MCP Active Tasks page.
+layer: 2
+category: architecture
+---
+
+# Commit Message Must Include BC Task ID
+
+## Description
+
+Every commit must reference the BC sub-task it belongs to by prefixing the
+message with `[#taskId]`, where `taskId` is the globally unique, sequential
+task identifier from the **CUR MCP Active Tasks** page (field `taskId`).
+
+This links code history directly to customer-facing work items in BC, enables
+time registration traceability, and is consistent with the convention already
+used across Curabis teams.
+
+## Anti Pattern
+
+```
+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
+```
+
+## The two task numbers — use taskId, not taskNo
+
+The sub-task has two numbers — do not confuse them:
+
+| Field | Description | Use for |
+|---|---|---|
+| `taskNo` | Sequential within the project (e.g. 42) | Referencing within a project |
+| `taskId` | Globally unique across all projects (e.g. 8738) | **Commit messages** |
+
+Always use `taskId` in commit messages. It is unambiguous across all projects
+and repos.
+
+## How to find the taskId before committing
+
+1. Get the current branch: `git branch --show-current`
+2. Find the linked project via BC MCP (see `[[bc-mcp-find-active-task-for-branch]]`)
+3. Read `taskId` from the matching active task
+4. Prefix every commit on this branch with `[#taskId]`
+
+If no task exists for the branch, create one first (see bc-mcp.agent.md
+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.
diff --git a/custom/knowledge/mcp/bc-mcp-find-active-task-for-branch.md b/custom/knowledge/mcp/bc-mcp-find-active-task-for-branch.md
new file mode 100644
index 0000000..33aead1
--- /dev/null
+++ b/custom/knowledge/mcp/bc-mcp-find-active-task-for-branch.md
@@ -0,0 +1,89 @@
+---
+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 MCP: Find Active Task for Branch
+
+## Description
+
+When committing, closing a branch, or updating dev status, the agent must
+look up the BC sub-task linked to the current branch. This recipe documents
+the exact steps and action names to do it efficiently with minimal roundtrips.
+
+Do **not** start with `bc_actions_search` — it fetches and searches the full
+action catalog and is slow. Use `bc_actions_describe` with the known action
+name to get a schema, then `bc_actions_invoke` to call it.
+
+## Known action names
+
+Derived from the AL page source (EntityName property + PAG + page ID):
+
+| Page | EntityName (AL) | List action | Modify action | Create action |
+|---|---|---|---|---|
+| 6102900 CUR MCP Active Tasks | `activeTask` | `List_activeTask_PAG6102900` | `Modify_activeTask_PAG6102900` | — |
+| 6102901 CUR MCP Projects | `project` | `List_project_PAG6102901` | — | — |
+| 6102902 CUR MCP Task Comments | `taskComment` | `List_taskComment_PAG6102902` | `Modify_taskComment_PAG6102902` | `Create_TaskComment_PAG6102902` |
+| 6102904 CUR MCP Project Repository | `projectRepository` | `List_projectRepository_PAG6102904` | `Modify_projectRepository_PAG6102904` | — |
+| 6102905 CUR MCP Create Task | `newTask` | — | — | `Create_newTask_PAG6102905` |
+| 50009 consultants | `consultant` | `List_consultant_PAG50009` | — | — |
+
+> **Note:** Verify action names after a fresh session with `bc_actions_search`
+> if any of the above return an error. The naming convention is
+> `{Verb}_{EntityNamePascalCase}_PAG{PageId}`.
+
+## 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
+```
+
+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.
+
+## Key fields on active tasks
+
+| Field | Description |
+|---|---|
+| `taskId` | **Global unique ID — use in commit messages** |
+| `taskNo` | Sequential within project — use for customer portal links |
+| `projectNo` | Parent project |
+| `description` | Task description |
+| `status` | BC-managed: Created → Accepted → In progress → Finished → Invoiced |
+| `gitHubBranch` | Writable — set when starting work |
+| `gitHubDevStatus` | Writable — Backlog / In Progress / Done / On Hold |
+| `gitHubRepository` | **Obsolete** — always read repo from project, not from task |
+
+## Writable fields — and what is forbidden
+
+Only write `gitHubBranch` and `gitHubDevStatus` on active tasks.
+Never write `status` — it controls time registration and invoicing in BC.
+Never write `gitHubRepository` on the task (obsolete, will be removed in v29).
+
+## Developer identity under S2S auth
+
+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
+```
+
+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.
From 985faf9e8db7f909575705856cea69de91bc487b Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sun, 21 Jun 2026 12:22:54 +0200
Subject: [PATCH 12/54] new global standard
---
custom/README.md | 65 ++++
custom/setup/bc-mcp-bridge.js | 168 +++++++++
custom/setup/curabis-standard.agent.md | 355 ++++++++++++++++++
custom/setup/machine/CLAUDE.md | 25 ++
.../setup/machine/bc-mcp.config.template.json | 6 +
custom/setup/templates/bcquality.agent.md | 61 +++
custom/setup/templates/cspell.json | 34 ++
custom/setup/templates/immanuel.agent.md | 104 +++++
8 files changed, 818 insertions(+)
create mode 100644 custom/setup/bc-mcp-bridge.js
create mode 100644 custom/setup/curabis-standard.agent.md
create mode 100644 custom/setup/machine/CLAUDE.md
create mode 100644 custom/setup/machine/bc-mcp.config.template.json
create mode 100644 custom/setup/templates/bcquality.agent.md
create mode 100644 custom/setup/templates/cspell.json
create mode 100644 custom/setup/templates/immanuel.agent.md
diff --git a/custom/README.md b/custom/README.md
index 28d7aa9..0b6c988 100644
--- a/custom/README.md
+++ b/custom/README.md
@@ -15,3 +15,68 @@ custom/
Fork or clone BCQuality into your own repository and add your content here. Knowledge files in `/custom/knowledge/` follow the same frontmatter schema and section requirements as every other layer. Action skills in `/custom/skills/` follow the Action Skill template defined in `/skills/`.
When agents consume BCQuality, the custom layer is loaded alongside Microsoft and Community — your overrides apply automatically.
+
+---
+
+# CURABIS — BCQuality customizations
+
+## Developer onboarding (new machine)
+
+Two files must be placed on the developer's machine. Everything else is automatic.
+
+### 1. Global Claude Code instructions
+
+Copy [`setup/machine/CLAUDE.md`](setup/machine/CLAUDE.md) to `~/.claude/CLAUDE.md`
+and fill in your name and username.
+
+This file tells Claude Code about CURABIS Standard in every session —
+including brand-new, unconfigured repositories.
+
+### 2. BC MCP credentials
+
+Create `~/.bc-mcp.config.json` with your BC service-to-service credentials:
+
+```json
+{
+ "tenantId": "",
+ "clientId": "",
+ "clientSecret": "",
+ "baseUrl": "https://api.businesscentral.dynamics.com"
+}
+```
+
+**Never commit this file.** It contains secrets.
+
+## Configuring a new project
+
+Once the two machine files are in place, open any AL-Go repository in VS Code
+and tell Claude Code:
+
+> "Konfigurer dette projekt til CURABIS Standard"
+
+Claude fetches [`setup/curabis-standard.agent.md`](setup/curabis-standard.agent.md)
+and writes all project files automatically:
+`CLAUDE.md`, `.mcp.json`, `.github/.agents/`, `cspell.json`, `projectmemory/`.
+
+The BC MCP bridge (`bc-mcp-bridge.js`) is also installed to `~/.claude/`
+from this repo — so it stays up to date every time setup is re-run.
+
+## Folder structure
+
+```
+custom/
+ README.md ← this file
+ knowledge/
+ architecture/ ← AL architecture rules
+ testing/ ← test quality rules
+ mcp/ ← BC MCP / API page rules
+ setup/
+ curabis-standard.agent.md ← project setup agent
+ bc-mcp-bridge.js ← BC MCP bridge (authoritative copy)
+ machine/
+ CLAUDE.md ← global Claude Code instructions template
+ templates/
+ bcquality.agent.md ← BCQuality review agent (per project)
+ immanuel.agent.md ← Rule guardian agent (per project)
+ cspell.json ← Standard spell-check config
+```
diff --git a/custom/setup/bc-mcp-bridge.js b/custom/setup/bc-mcp-bridge.js
new file mode 100644
index 0000000..d91a277
--- /dev/null
+++ b/custom/setup/bc-mcp-bridge.js
@@ -0,0 +1,168 @@
+#!/usr/bin/env node
+// bc-mcp-bridge.js
+// Lokal stdio <-> streamable-HTTP bro mellem Claude Code og BC MCP-serveren.
+// S2S-auth (client credentials) - ingen bruger-login. Broen henter + fornyer token selv,
+// og injicerer routing-headers. Claude Code taler stdio til broen (intet DCR/OAuth-problem).
+//
+// Config: Scripts/bc-mcp.config.json (GITIGNORED) eller env-vars:
+// BC_MCP_TENANT, BC_MCP_CLIENT_ID, BC_MCP_CLIENT_SECRET,
+// BC_MCP_ENVIRONMENT (default Production), BC_MCP_COMPANY, BC_MCP_CONFIG (default CURABIS_DEV)
+//
+// .mcp.json:
+// "businesscentral": { "command": "node", "args": ["Scripts/bc-mcp-bridge.js"] }
+
+const fs = require("fs");
+const path = require("path");
+
+const ENDPOINT = "https://mcp.businesscentral.dynamics.com";
+
+function die(m) { process.stderr.write(`[bc-mcp-bridge] ${m}\n`); process.exit(1); }
+
+function loadConfig() {
+ let c = {};
+ // Soeger: 1) repo-lokal Scripts/bc-mcp.config.json 2) pr-maskine ~/.bc-mcp.config.json
+ const home = process.env.USERPROFILE || process.env.HOME || "";
+ for (const f of [path.join(__dirname, "bc-mcp.config.json"), path.join(home, ".bc-mcp.config.json")]) {
+ if (fs.existsSync(f)) {
+ try { c = JSON.parse(fs.readFileSync(f, "utf8")); break; } catch (e) { die(`Kan ikke laese ${f}: ${e}`); }
+ }
+ }
+ const cfg = {
+ tenant: process.env.BC_MCP_TENANT || c.tenant,
+ clientId: process.env.BC_MCP_CLIENT_ID || c.clientId,
+ clientSecret: process.env.BC_MCP_CLIENT_SECRET || c.clientSecret,
+ environment: process.env.BC_MCP_ENVIRONMENT || c.environment || "Production",
+ company: process.env.BC_MCP_COMPANY || c.company,
+ config: process.env.BC_MCP_CONFIG || c.configurationName || "CURABIS_DEV",
+ };
+ for (const k of ["tenant", "clientId", "clientSecret", "company"]) {
+ if (!cfg[k]) die(`Mangler config '${k}' - saet i Scripts/bc-mcp.config.json eller env-var.`);
+ }
+ return cfg;
+}
+
+const cfg = loadConfig();
+let token = null, tokenExp = 0, sessionId = null;
+
+async function getToken() {
+ if (token && Date.now() < tokenExp - 60000) return token; // forny 1 min foer udloeb
+ const body = new URLSearchParams({
+ client_id: cfg.clientId,
+ client_secret: cfg.clientSecret,
+ scope: `${ENDPOINT}/.default`,
+ grant_type: "client_credentials",
+ });
+ const r = await fetch(`https://login.microsoftonline.com/${cfg.tenant}/oauth2/v2.0/token`, {
+ method: "POST",
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
+ body,
+ });
+ if (!r.ok) throw new Error(`token ${r.status}: ${await r.text()}`);
+ const j = await r.json();
+ token = j.access_token;
+ tokenExp = Date.now() + (j.expires_in || 3600) * 1000;
+ return token;
+}
+
+// BC kraever Base64 hvis header-vaerdien har ikke-ASCII (ae/oe/aa).
+const enc = v => /[^\x00-\x7F]/.test(v) ? `=?base64?${Buffer.from(v, "utf8").toString("base64")}?=` : v;
+
+function parseSSE(text) {
+ const msgs = [];
+ for (const block of text.split(/\r?\n\r?\n/)) {
+ const data = block.split(/\r?\n/).filter(l => l.startsWith("data:")).map(l => l.slice(5).replace(/^ /, ""));
+ if (data.length) { const p = data.join("\n").trim(); if (p && p !== "[DONE]") msgs.push(p); }
+ }
+ return msgs;
+}
+
+async function forward(msg) {
+ const tok = await getToken();
+ const headers = {
+ "Authorization": `Bearer ${tok}`,
+ "Content-Type": "application/json",
+ "Accept": "application/json, text/event-stream",
+ "TenantId": cfg.tenant,
+ "EnvironmentName": cfg.environment,
+ "Company": enc(cfg.company),
+ "ConfigurationName": enc(cfg.config),
+ };
+ if (sessionId) headers["Mcp-Session-Id"] = sessionId;
+ const r = await fetch(ENDPOINT, { method: "POST", headers, body: JSON.stringify(msg) });
+ const sid = r.headers.get("mcp-session-id"); if (sid) sessionId = sid;
+ const ct = r.headers.get("content-type") || "";
+ const text = await r.text();
+ if (!r.ok && !text) throw new Error(`HTTP ${r.status}`);
+ return ct.includes("text/event-stream") ? parseSSE(text) : (text.trim() ? [text.trim()] : []);
+}
+
+// Splits text into chunks of max maxLen chars, breaking at word boundaries.
+function splitTextToChunks(text, maxLen = 250) {
+ const chunks = [];
+ while (text.length > maxLen) {
+ let cut = text.lastIndexOf(" ", maxLen);
+ if (cut <= 0) cut = maxLen; // no space found — hard cut
+ chunks.push(text.slice(0, cut).trimEnd());
+ text = text.slice(cut).trimStart();
+ }
+ if (text) chunks.push(text);
+ return chunks;
+}
+
+// Intercepts Create_TaskComment calls with comment > 250 chars and splits into multiple lines.
+// Each chunk is sent in its own fresh BC session so GetNextLineNo sees previously committed records.
+async function dispatchCreateComment(msg) {
+ const args = (msg.params && msg.params.arguments) || {};
+ const comment = args.comment || "";
+ const chunks = splitTextToChunks(comment);
+ let tempId = Date.now();
+ const savedSessionId = sessionId; // preserve the main conversation session
+ for (const chunk of chunks) {
+ sessionId = null; // fresh session per chunk → independent BC transaction
+ const chunkMsg = {
+ ...msg,
+ id: tempId++,
+ params: { ...msg.params, arguments: { ...args, comment: chunk } },
+ };
+ await forward(chunkMsg);
+ sessionId = null; // discard the chunk session — never bleed into next chunk
+ }
+ sessionId = savedSessionId; // restore main session for subsequent calls
+ return [JSON.stringify({
+ jsonrpc: "2.0", id: msg.id,
+ result: { content: [{ type: "text", text: `Kommentar gemt i ${chunks.length} linje(r).` }] },
+ })];
+}
+
+// stdio-loop: newline-delimited JSON-RPC (MCP stdio-transport).
+let buf = "";
+process.stdin.setEncoding("utf8");
+process.stdin.on("data", async (chunk) => {
+ buf += chunk;
+ let i;
+ while ((i = buf.indexOf("\n")) >= 0) {
+ const line = buf.slice(0, i).trim();
+ buf = buf.slice(i + 1);
+ if (!line) continue;
+ let msg;
+ try { msg = JSON.parse(line); } catch { continue; }
+ try {
+ const isCreateComment =
+ msg.method === "tools/call" &&
+ msg.params?.name === "Create_TaskComment_PAG6102902" &&
+ (msg.params?.arguments?.comment || "").length > 250;
+ const responses = isCreateComment
+ ? await dispatchCreateComment(msg)
+ : await forward(msg);
+ for (const out of responses) process.stdout.write(out + "\n");
+ } catch (e) {
+ process.stderr.write(`[bc-mcp-bridge] ${e.message || e}\n`);
+ if (msg.id !== undefined && msg.id !== null) {
+ process.stdout.write(JSON.stringify({
+ jsonrpc: "2.0", id: msg.id, error: { code: -32000, message: String(e.message || e) },
+ }) + "\n");
+ }
+ }
+ }
+});
+process.stdin.on("end", () => process.exit(0));
diff --git a/custom/setup/curabis-standard.agent.md b/custom/setup/curabis-standard.agent.md
new file mode 100644
index 0000000..1a2c2fa
--- /dev/null
+++ b/custom/setup/curabis-standard.agent.md
@@ -0,0 +1,355 @@
+---
+kind: action-skill
+id: curabis-standard-setup
+version: 1
+title: CURABIS Standard — Project Setup
+description: >
+ Configures a new or existing repository to the CURABIS Standard development
+ environment. Writes CLAUDE.md, BCQuality agents, .mcp.json and cspell.json
+ from authoritative templates in BCQuality. Deploys bc-mcp-bridge.js to the
+ developer's machine. Also handles updates to an already-configured project.
+inputs: [repo-root]
+outputs: [CLAUDE.md, .mcp.json, .github/.agents/*, cspell.json, projectmemory/]
+domain: setup
+keywords: [setup, bootstrap, update, mcp, bcquality, standard, new-project]
+---
+
+# CURABIS Standard — Project Setup
+
+## Purpose
+
+One command turns an empty or existing AL-Go repository into a fully configured
+CURABIS development environment: BCQuality rules loaded, BC MCP wired, Immanuel
+on guard, and project memory ready.
+
+## Triggers
+
+This agent runs when the developer says any of:
+
+- **"Konfigurer dette projekt til CURABIS Standard"** → full setup (new project)
+- **"Opdater CURABIS Standard fra BCQuality"** → update mode (existing project)
+
+Detect which mode based on the trigger phrase and proceed accordingly.
+
+## Source URLs (BCQuality — always fetch fresh)
+
+```
+BASE = https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/setup
+```
+
+| Artefakt | URL |
+|---|---|
+| bc-mcp-bridge.js | `{BASE}/bc-mcp-bridge.js` |
+| bc-mcp.config.template.json | `{BASE}/machine/bc-mcp.config.template.json` |
+| bcquality.agent.md | `{BASE}/templates/bcquality.agent.md` |
+| immanuel.agent.md | `{BASE}/templates/immanuel.agent.md` |
+| cspell.json | `{BASE}/templates/cspell.json` |
+
+CLAUDE.md and .mcp.json are generated dynamically — not fetched as static templates
+because they contain project-specific paths.
+
+---
+
+## MODE A — Full setup (new project)
+
+Triggered by: "Konfigurer dette projekt til CURABIS Standard"
+
+### Step 1 — Gather context (auto-detect before asking)
+
+Run these checks silently:
+
+```bash
+git remote get-url origin # → repo name / URL
+git config user.email # → developer identity
+git config user.name
+```
+
+Check whether these paths exist:
+- `.vscode/find-altool.ps1` → AL MCP available
+- `CLAUDE.md` → already configured?
+- `~/.claude/bc-mcp-bridge.js` → bridge already installed?
+- `~/.bc-mcp.config.json` → BC credentials present?
+
+If `CLAUDE.md` already exists, ask: "CLAUDE.md eksisterer allerede. Overskrive? (ja/nej)"
+Stop if the developer answers no.
+
+### Step 2 — Ask exactly three questions
+
+Do not proceed until all three are answered.
+
+```
+1. Hvad er projektets navn?
+ (bruges som overskrift i CLAUDE.md og i projectmemory)
+
+2. Hvilke AL-app mapper er i repoen?
+ Eksempler:
+ a) Flad struktur — kildefiler direkte i roden (AppSource/)
+ b) .apps/ (main app)
+ c) .apps/ + .apps/.Test (main + test)
+ Angiv de faktiske mapper.
+
+3. Hvad er dit brugernavn til projectmemory-filen?
+ (f.eks. "mid" → memoryupdates_mid.md)
+```
+
+### Step 3 — Deploy machine files
+
+#### 3a. bc-mcp-bridge.js
+
+1. Fetch `{BASE}/bc-mcp-bridge.js`
+2. Write to `~/.claude/bc-mcp-bridge.js` (overwrite silently — BCQuality is authoritative)
+3. Confirm: "bc-mcp-bridge.js er opdateret på din maskine."
+
+#### 3b. bc-mcp.config.json
+
+If `~/.bc-mcp.config.json` already exists: skip silently.
+
+If it does NOT exist:
+1. Fetch `{BASE}/machine/bc-mcp.config.template.json`
+2. Write it to `~/.bc-mcp.config.json` as-is
+3. Tell the developer:
+ > "⚠️ `~/.bc-mcp.config.json` er oprettet fra CURABIS-template.
+ > Åbn filen og erstat `` med din egen secret.
+ > Gem filen — BC MCP er klar når du genstarter Claude Code."
+
+### Step 4 — Write project files
+
+#### 4a. CLAUDE.md
+
+Generate from this template, substituting answers from Step 2:
+
+```markdown
+# {PROJECT_NAME} — Claude Code Instructions
+
+This file is read automatically by Claude Code at the start of every session.
+
+## BCQuality
+
+At the start of every session, before doing anything else:
+
+1. Read `.github/.agents/bcquality.agent.md`
+2. Fetch and read ALL knowledge files listed under Source - Layer 2:
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/pages-must-not-contain-business-logic.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/namespace-must-be-verified-from-source.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/al-identifiers-must-be-english.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/clarify-before-building.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/xliff-translation-workflow.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/new-file-requires-vscode-refresh.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/exposed-objects-must-be-in-a-permission-set.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/shared-project-memory-must-be-in-repo.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/commit-message-must-include-bc-task-id.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/branch-merge-to-main-workflow.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/test-setup-must-use-library-codeunit.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/test-data-must-be-random-and-complete.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/tests-must-adapt-to-existing-code.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/test-one-when-per-test.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/ui-test-codeunit-naming.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/test-feature-scenario-tags.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/api-page-flowfields-must-be-calcfields.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/stored-derived-fields-must-not-be-exposed-directly.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/api-page-key-fields-must-be-editable-on-insert.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/api-page-least-privilege-write-access.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/agent-must-not-write-business-process-status.md
+ - https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/bc-mcp-find-active-task-for-branch.md
+
+These rules are always active.
+
+## On-demand agents
+
+These are invoked only when needed - not at session start:
+
+- `.github/.agents/immanuel.agent.md` - BCQuality rule guardian. Invoke when the user
+ proposes adding a new rule to BCQuality. Runs the Categorical Imperative test and drafts
+ the knowledge file. Only Michael (mid) may approve and push rules to BCQuality.
+
+## AL projects
+
+{AL_PROJECTS_SECTION}
+
+## 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.
+
+When you learn something project-relevant (business rules, architectural decisions,
+scope boundaries, known technical debt), write it to
+`projectmemory/memoryupdates_.md` for the active user.
+
+User-specific preferences (tone, workflow habits) stay in the local
+`~/.claude/projects/.../memory/` folder as before.
+
+## About this project
+
+{PROJECT_NAME} Business Central extension
+```
+
+**AL_PROJECTS_SECTION substitution rules:**
+
+- Flat (AppSource/):
+ ```
+ Main app is in `AppSource/` at repo root.
+ ```
+- .apps/\ only:
+ ```
+ The app is loaded via MCP hooks:
+ - .apps\ — main app
+ ```
+- .apps/\ + .apps/\.Test:
+ ```
+ Both apps are always loaded via MCP hooks:
+ - .apps\ — main app
+ - .apps\.Test — test app
+ ```
+
+Add running-tests section only when both main + test app exist:
+
+```markdown
+## Running tests
+
+The `al` MCP server is wired into Claude Code via the repo-root `.mcp.json`.
+To run the test suite end to end:
+
+1. `al_auth_login` - authenticate to the BC sandbox (once per session).
+2. `al_downloadsymbols` - fetch dependency symbols.
+3. `al_compile` (or `al_build`) - confirm both apps build clean.
+4. `al_publish` - publish main + test app to the sandbox.
+5. `al_run_tests` - execute the tests; optionally filter to one codeunit.
+
+After creating any new `.al` file, reload the AL extension in VS Code
+(`Ctrl+Shift+P -> AL: Reload Extension`) before trusting diagnostics.
+```
+
+#### 4b. .mcp.json
+
+If `.vscode/find-altool.ps1` exists:
+```json
+{
+ "mcpServers": {
+ "al": {
+ "type": "stdio",
+ "command": "powershell",
+ "args": [
+ "-ExecutionPolicy", "Bypass",
+ "-File", "/find-altool.ps1",
+ "launchmcpserver", "--transport", "stdio"
+ ]
+ },
+ "businesscentral": {
+ "command": "node",
+ "args": ["C:\\Users\\\\.claude\\bc-mcp-bridge.js"]
+ }
+ }
+}
+```
+
+If `.vscode/find-altool.ps1` does NOT exist:
+```json
+{
+ "mcpServers": {
+ "businesscentral": {
+ "command": "node",
+ "args": ["C:\\Users\\\\.claude\\bc-mcp-bridge.js"]
+ }
+ }
+}
+```
+
+Substitute `` and `` from detected values.
+
+If `find-altool.ps1` is missing, note after writing .mcp.json:
+> "ℹ️ AL MCP er ikke konfigureret endnu. Kør `Ctrl+Shift+P → AL: Configure MCP Server`
+> i VS Code for at generere find-altool.ps1, og kør derefter
+> 'Opdater CURABIS Standard fra BCQuality' — AL MCP tilføjes automatisk."
+
+#### 4c. .github/.agents/ (fetch from BCQuality)
+
+Fetch and write verbatim:
+- `{BASE}/templates/bcquality.agent.md` → `.github/.agents/bcquality.agent.md`
+- `{BASE}/templates/immanuel.agent.md` → `.github/.agents/immanuel.agent.md`
+
+Create `.github/.agents/` if it does not exist.
+
+#### 4d. cspell.json
+
+Fetch `{BASE}/templates/cspell.json` and write to repo root.
+If a `cspell.json` already exists, merge the `words` array — do not overwrite
+custom project words.
+
+#### 4e. projectmemory/
+
+Create `projectmemory/` if it does not exist.
+Create `projectmemory/memoryupdates_.md` if it does not exist:
+
+```markdown
+# Project Memory — ()
+
+Observations og beslutninger der er relevante for alle på projektet.
+Læses automatisk af Claude Code ved session-start (via CLAUDE.md).
+
+---
+
+(Tilføj observationer her)
+```
+
+### Step 5 — Confirm and offer initial commit
+
+List all files written, then ask:
+> "Setup er færdigt. Vil du have mig til at lave det første commit? (ja/nej)"
+
+If yes, stage and commit:
+```
+[SETUP] Konfigurer til CURABIS Standard
+
+- CLAUDE.md med BCQuality knowledge-liste
+- .github/.agents/bcquality.agent.md + immanuel.agent.md
+- .mcp.json med BC MMP bridge
+- cspell.json
+- projectmemory/ mappe
+
+Co-Authored-By: Claude Sonnet 4.6
+```
+
+---
+
+## MODE B — Update (existing project)
+
+Triggered by: "Opdater CURABIS Standard fra BCQuality"
+
+Updates only the files that come directly from BCQuality.
+Never touches `CLAUDE.md`, `projectmemory/`, or `~/.bc-mcp.config.json`.
+
+### What gets updated
+
+| Fil | Handling |
+|---|---|
+| `~/.claude/bc-mcp-bridge.js` | Fetch fresh from BCQuality, overwrite |
+| `.github/.agents/bcquality.agent.md` | Fetch fresh from BCQuality, overwrite |
+| `.github/.agents/immanuel.agent.md` | Fetch fresh from BCQuality, overwrite |
+| `cspell.json` — words from template | Merge new words, keep project words |
+| `.mcp.json` — `al` entry | Add if `find-altool.ps1` now exists and entry is missing |
+
+### What does NOT get updated
+
+- `CLAUDE.md` — project-specific, managed per project
+- `projectmemory/` — team knowledge, never overwritten by tooling
+- `~/.bc-mcp.config.json` — contains developer secrets
+
+### After update
+
+Report what changed, then ask:
+> "Opdatering færdig. Vil du have mig til at committe ændringerne? (ja/nej)"
+
+If yes, commit:
+```
+[SETUP] Opdater CURABIS Standard fra BCQuality
+
+Co-Authored-By: Claude Sonnet 4.6
+```
+
+---
+
+## Invocation note
+
+This agent is fetched on demand from BCQuality. Both commands work in any
+project — including one not yet configured — because Claude reads the URL
+from `~/.claude/CLAUDE.md` (global instructions, present on all CURABIS machines).
diff --git a/custom/setup/machine/CLAUDE.md b/custom/setup/machine/CLAUDE.md
new file mode 100644
index 0000000..0eb5d45
--- /dev/null
+++ b/custom/setup/machine/CLAUDE.md
@@ -0,0 +1,25 @@
+
+# CURABIS — Global Claude Code Instructions
+
+These instructions apply to every Claude Code session, regardless of project.
+
+## CURABIS Standard project setup
+
+When working in a repository that has no `CLAUDE.md`, or when the user says
+**"Konfigurer dette projekt til CURABIS Standard"**, fetch and follow the setup agent:
+
+```
+https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/setup/curabis-standard.agent.md
+```
+
+This agent configures the repository with BCQuality rules, BC MCP bridge,
+Immanuel (rule guardian), and project memory — from a single command.
+
+## Identity
+
+- Organization: CURABIS ApS
+- BC MCP bridge is installed at `~/.claude/bc-mcp-bridge.js`
+- BC MCP credentials are at `~/.bc-mcp.config.json` (never commit this file)
+
+
+- User: [Your Name] (username: [your-username])
diff --git a/custom/setup/machine/bc-mcp.config.template.json b/custom/setup/machine/bc-mcp.config.template.json
new file mode 100644
index 0000000..2bfd9f9
--- /dev/null
+++ b/custom/setup/machine/bc-mcp.config.template.json
@@ -0,0 +1,6 @@
+{
+ "tenantId": "CURABIS-TENANT-ID",
+ "clientId": "CURABIS-CLIENT-ID",
+ "clientSecret": "",
+ "baseUrl": "https://api.businesscentral.dynamics.com"
+}
diff --git a/custom/setup/templates/bcquality.agent.md b/custom/setup/templates/bcquality.agent.md
new file mode 100644
index 0000000..b9291da
--- /dev/null
+++ b/custom/setup/templates/bcquality.agent.md
@@ -0,0 +1,61 @@
+---
+kind: action-skill
+id: curabis-al-code-review
+version: 1
+title: CURABIS AL code review
+description: Reviews AL source changes against BCQuality knowledge and CURABIS-specific architecture rules.
+inputs: [pr-diff, file-path]
+outputs: [findings-report]
+bc-version: [all]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+domain: architecture
+keywords: [page-logic, codeunit, posting, test-library, suppresscommit, asserterror, findset, namespace, english, random-data]
+sub-skills:
+ - microsoft/skills/review/al-code-review.md
+---
+
+# CURABIS AL code review
+
+## Source
+
+Layer 1 - Microsoft BCQuality: https://github.com/microsoft/BCQuality
+
+Layer 2 - CURABIS custom knowledge (fetch before applying rules):
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/pages-must-not-contain-business-logic.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/namespace-must-be-verified-from-source.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/al-identifiers-must-be-english.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/clarify-before-building.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/xliff-translation-workflow.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/new-file-requires-vscode-refresh.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/exposed-objects-must-be-in-a-permission-set.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/shared-project-memory-must-be-in-repo.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/commit-message-must-include-bc-task-id.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/branch-merge-to-main-workflow.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/test-setup-must-use-library-codeunit.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/test-data-must-be-random-and-complete.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/tests-must-adapt-to-existing-code.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/test-one-when-per-test.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/ui-test-codeunit-naming.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/test-feature-scenario-tags.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/api-page-flowfields-must-be-calcfields.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/stored-derived-fields-must-not-be-exposed-directly.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/api-page-key-fields-must-be-editable-on-insert.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/api-page-least-privilege-write-access.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/agent-must-not-write-business-process-status.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/bc-mcp-find-active-task-for-branch.md
+
+## Action
+
+CURABIS-ARCH-001: Logic belongs in codeunits, not pages.
+CURABIS-ARCH-002: Pages must not call Modify/Insert/Delete directly.
+CURABIS-ARCH-003: Test setup must use the project Test Library.
+CURABIS-ARCH-004: SetSuppressCommit(true) before posting codeunit Run() in tests.
+CURABIS-ARCH-005: asserterror must be followed by an assertion.
+CURABIS-ARCH-006: FindSet(true) only before Modify() inside a loop.
+CURABIS-ARCH-007: Test data must be random - never hardcode codes or names.
+CURABIS-ARCH-008: Namespaces must be verified from source files or al_symbolsearch.
+CURABIS-ARCH-009: All AL identifiers must be English (ENU).
+CURABIS-ARCH-010: Clarify before building if task is ambiguous.
+CURABIS-ARCH-011: Every exposed object (API page, web-service page/query) must be in at least one permission set.
diff --git a/custom/setup/templates/cspell.json b/custom/setup/templates/cspell.json
new file mode 100644
index 0000000..1661e53
--- /dev/null
+++ b/custom/setup/templates/cspell.json
@@ -0,0 +1,34 @@
+{
+ "version": "0.2",
+ "language": "en,da",
+ "ignorePaths": [
+ "**/*.xlf",
+ "**/*.xml",
+ "**/node_modules/**",
+ "projectmemory/**",
+ "CLAUDE.md"
+ ],
+ "words": [
+ "Curabis",
+ "CURABIS",
+ "codeunit",
+ "Codeunits",
+ "xliff",
+ "subpage",
+ "FactBox",
+ "TestPage",
+ "pageextension",
+ "tableextension",
+ "permissionset",
+ "RunModal",
+ "SetValue",
+ "OpenEdit",
+ "OpenNew",
+ "FindFirst",
+ "FindSet",
+ "FindLast",
+ "WorkDate",
+ "CurrExchRate",
+ "NoImplicitWith"
+ ]
+}
diff --git a/custom/setup/templates/immanuel.agent.md b/custom/setup/templates/immanuel.agent.md
new file mode 100644
index 0000000..73fe2a7
--- /dev/null
+++ b/custom/setup/templates/immanuel.agent.md
@@ -0,0 +1,104 @@
+---
+kind: action-skill
+id: curabis-bcquality-guardian
+version: 1
+title: Immanuel — BCQuality Rule Guardian
+description: >
+ Validates proposed BCQuality rules against Kant's Categorical Imperative before
+ they are submitted to Michael Dieringer (mid) for approval. Guards the BCQuality
+ knowledge base against project-specific, contradictory, or poorly scoped rules.
+inputs: [proposed-rule-text]
+outputs: [validation-report, draft-knowledge-file]
+domain: governance
+keywords: [bcquality, rule, categorical-imperative, governance, universal-law]
+---
+
+# Immanuel — BCQuality Rule Guardian
+
+## Purpose
+
+BCQuality rules are **universal laws** for all CURABIS developers on all projects.
+Before a rule enters the knowledge base, it must pass the Categorical Imperative test:
+
+> "Act only according to that maxim whereby you can at the same time will
+> that it should become a universal law."
+>
+> — Immanuel Kant, *Groundwork of the Metaphysics of Morals* (1785)
+
+Applied to BCQuality: **"What would happen to CURABIS if every developer followed
+this rule on every project, every day, without exception?"**
+
+## Authorization
+
+**Only Michael Dieringer (mid) may add rules to BCQuality.**
+
+Immanuel is an advisor, not an executor. He validates, drafts, and recommends.
+He never pushes to BCQuality directly. Every rule ends with an explicit
+hand-off to Michael for review and approval.
+
+## Validation Protocol
+
+Run all four tests before recommending a rule. If any test fails, the rule
+must be revised or redirected to `projectmemory/` instead.
+
+### Test 1 — Universalizability
+Ask: *"What if every CURABIS developer followed this rule on every project?"*
+
+- Does the rule still make sense? → **Pass**
+- Does it create contradiction, chaos, or absurdity? → **Fail** — rule has a hidden
+ assumption that limits its applicability
+
+### Test 2 — Project-specificity check
+A rule fails this test if it references:
+- Specific company names (Wareco, Jernpladsen, Summatim, KLB…)
+- Project-specific tables, codeunits, or flows
+- Tech choices that are not universal across CURABIS (specific IC patterns, etc.)
+- A BC version feature not yet available in all active projects
+
+If it fails: redirect to `projectmemory/` in the relevant repo, not BCQuality.
+
+### Test 3 — Clarity and enforceability
+Ask: *"Can a developer know, in the moment of coding, whether they are following
+this rule or violating it?"*
+
+- Clear decision point → **Pass**
+- Vague or subjective → **Fail** — sharpen the rule before proceeding
+
+### Test 4 — Additive value
+Ask: *"Does this rule prevent a real problem that developers would otherwise
+not catch?"*
+
+- Fills a genuine gap → **Pass**
+- Already covered by an existing BCQuality rule → **Fail** — point to the
+ existing rule instead; don't duplicate
+
+## Output Format
+
+After running all four tests, produce:
+
+```
+## Categorical Imperative Assessment
+
+**Proposed rule:**
+
+| Test | Result | Notes |
+|---|---|---|
+| 1. Universalizability | ✅ Pass / ❌ Fail | ... |
+| 2. Project-specificity | ✅ Pass / ❌ Fail | ... |
+| 3. Clarity | ✅ Pass / ❌ Fail | ... |
+| 4. Additive value | ✅ Pass / ❌ Fail | ... |
+
+**Verdict:** APPROVED FOR BCQUALITY / REVISE / REDIRECT TO projectmemory
+
+**Recommended path:** custom/knowledge//.md
+```
+
+If verdict is APPROVED, also produce the complete draft knowledge file
+in BCQuality markdown format, ready for Michael to review and push.
+
+## Hand-off
+
+End every assessment with:
+
+> "Denne regel kræver Michaels godkendelse (mid) inden den tilføjes til BCQuality.
+> Ingen andre må tilføje regler til BCQuality-repoen."
From 0b1054051f707aff9e4f7255c5d7f6fb390070bb Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sun, 21 Jun 2026 12:46:46 +0200
Subject: [PATCH 13/54] AutoUpdate
---
custom/setup/machine/CLAUDE.md | 24 ++++++++++++++++++++----
1 file changed, 20 insertions(+), 4 deletions(-)
diff --git a/custom/setup/machine/CLAUDE.md b/custom/setup/machine/CLAUDE.md
index 0eb5d45..db8518d 100644
--- a/custom/setup/machine/CLAUDE.md
+++ b/custom/setup/machine/CLAUDE.md
@@ -3,17 +3,33 @@
These instructions apply to every Claude Code session, regardless of project.
+## Auto-update BCQuality (runs at every session start)
+
+If the current project has `.github/.agents/bcquality.agent.md`:
+
+1. Fetch the latest commit SHA for `custom/setup/` in BCQuality:
+ `https://api.github.com/repos/Curabis/BCQuality/commits?path=custom/setup&per_page=1`
+2. Read stored SHA from `~/.claude/.bcquality-version` (may not exist yet)
+3. If SHA is different (or file missing):
+ a. Fetch and execute Mode B from:
+ `https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/setup/curabis-standard.agent.md`
+ b. Write new SHA to `~/.claude/.bcquality-version`
+ c. Report: "BCQuality opdateret til [short-sha] — [antal] filer tjekket"
+4. If SHA is unchanged: continue silently
+
+Do this silently and quickly — do not ask for permission, do not explain the process
+unless something actually changed.
+
## CURABIS Standard project setup
-When working in a repository that has no `CLAUDE.md`, or when the user says
-**"Konfigurer dette projekt til CURABIS Standard"**, fetch and follow the setup agent:
+When the user says either of these commands, fetch and follow the setup agent:
```
https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/setup/curabis-standard.agent.md
```
-This agent configures the repository with BCQuality rules, BC MCP bridge,
-Immanuel (rule guardian), and project memory — from a single command.
+- **"Konfigurer dette projekt til CURABIS Standard"** → fuld setup af nyt repo
+- **"Opdater CURABIS Standard fra BCQuality"** → manuel opdatering
## Identity
From 6fe72d82a48b9b71f5d1e697ed822543b8d7fb9c Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Sun, 21 Jun 2026 13:15:13 +0200
Subject: [PATCH 14/54] Add rule: scope task lists to current repository
When a developer asks for open tasks, only return tasks from the project(s)
linked to the current git repository. Flag it if no project is linked.
Co-authored-by: Claude Sonnet 4.6
---
.../mcp/bc-mcp-scope-tasks-to-repository.md | 87 +++++++++++++++++++
1 file changed, 87 insertions(+)
create mode 100644 custom/knowledge/mcp/bc-mcp-scope-tasks-to-repository.md
diff --git a/custom/knowledge/mcp/bc-mcp-scope-tasks-to-repository.md b/custom/knowledge/mcp/bc-mcp-scope-tasks-to-repository.md
new file mode 100644
index 0000000..3afba33
--- /dev/null
+++ b/custom/knowledge/mcp/bc-mcp-scope-tasks-to-repository.md
@@ -0,0 +1,87 @@
+---
+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 MCP: Scope Task Lists to the Current Repository
+
+## Rule
+
+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
+the project(s) linked to the current git repository**.
+
+Do **not** return all tasks assigned to the developer across all BC projects.
+That produces noise from unrelated customers and hides the signal of what is
+actually in scope for the current repo.
+
+## Why
+
+A developer working in a repository is focused on that context. Showing tasks
+from other projects (other customers, other repos) creates confusion and
+increases the risk of working on the wrong thing or copying context from the
+wrong project.
+
+## Standard recipe
+
+```
+1. git remote get-url origin
+ → e.g. "https://github.com/Curabis/Wareco.git"
+
+2. List_ProjectRepositories_PAG6102904
+ filter: "gitHubRepository eq ''"
+ → get projectNo(s) for this repo
+
+3. IF no projects found → STOP and flag (see "No linked project" below)
+
+4. List_ActiveTasks_PAG6102900
+ filter: "projectNo eq '' and taskResponsible eq ''"
+ → 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]]`.
+
+## No linked project — flag it
+
+If step 2 returns no results, do not fall back to listing all tasks.
+Instead, surface it explicitly:
+
+> **Flag:** Dette repository (``) er ikke knyttet til et BC-projekt.
+> Opgavelisten kan ikke scopetes. Tilknyt repoet via `projectRepositories`
+> (PAG6102904) eller kontakt projektlederen.
+
+This is a data-quality issue that should be fixed, not silently bypassed.
+
+## Multiple projects on one repo
+
+If step 2 returns more than one project for the repo, query tasks from all of
+them and group the output by project.
+
+## What to show
+
+| Field | Include |
+|---|---|
+| `taskNo` | Yes |
+| `description` | Yes |
+| `status` | Yes |
+| `gitHubDevStatus` | Yes — shows In Progress / Backlog / Done / On Hold |
+| `gitHubBranch` | Yes — shows which branch the task is on |
+| `estimatedTime` / `timeLeft` | Yes — helps prioritize |
+| `expectedDelivery` | Yes — shows urgency |
+| `customerName` | Yes — context |
+
+Do not show internal system fields (`taskId`, etags, `@odata.*`).
+
+## Violations to avoid
+
+- Filtering only by `taskResponsible` without scoping to `projectNo` → returns
+ tasks from every customer the developer has ever worked on.
+- Falling back to all-tasks when the repo lookup fails → hides a config problem.
+- Returning tasks where `gitHubRepository` on the task matches (this field is
+ obsolete — always scope via the project, not the task field).
From 34d8f372f88aeb18a732c20aa2b5d9a49a80d75a Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Mon, 22 Jun 2026 04:10:58 +0200
Subject: [PATCH 15/54] Add governance agents: Francis (BC-MCP observer) and
Immanuel (rule guardian)
Francis observes BC-MCP session patterns and identifies where existing rules
are too superficial (Type A sharpening) or where no rule covers the pattern
(Type B gap). Type B gaps are handed to Immanuel for Categorical Imperative
validation before entering BCQuality.
Immanuel guards the knowledge base by running four tests (universalizability,
project-specificity, clarity, additive value) before any rule is approved.
---
custom/agents/francis.agent.md | 143 ++++++++++++++++++++++++++++++++
custom/agents/immanuel.agent.md | 104 +++++++++++++++++++++++
2 files changed, 247 insertions(+)
create mode 100644 custom/agents/francis.agent.md
create mode 100644 custom/agents/immanuel.agent.md
diff --git a/custom/agents/francis.agent.md b/custom/agents/francis.agent.md
new file mode 100644
index 0000000..b40f2c4
--- /dev/null
+++ b/custom/agents/francis.agent.md
@@ -0,0 +1,143 @@
+---
+kind: action-skill
+id: curabis-mcp-observer
+version: 1
+title: Francis — BC-MCP Rule Observer
+description: >
+ Observes BC-MCP usage patterns in the current session and projectmemory,
+ then identifies where existing MCP rules are too superficial (sharpening)
+ or where no rule covers the observed pattern (gap). Sharpening proposals
+ go to projectmemory for Michael's approval. Gap proposals are handed off
+ to Immanuel for Categorical Imperative validation before entering BCQuality.
+inputs: [session-context, projectmemory]
+outputs: [sharpening-proposals, gap-proposals, immanuel-handoff]
+domain: governance
+keywords: [mcp, bc-mcp, api-page, rule-observation, self-learning, bcquality]
+---
+
+# Francis — BC-MCP Rule Observer
+
+## Purpose
+
+Named after Francis Bacon (1561–1626), father of empirical induction:
+*"If a man will begin with certainties, he shall end in doubts;
+ but if he will be content to begin with doubts, he shall end in certainties."*
+
+BCQuality rules are written from theory. Francis works from practice.
+He reads what actually happened in a BC-MCP session, compares it against the
+six MCP knowledge files, and surfaces the gap between intent and reality.
+
+Francis operates exclusively in the BC-MCP domain:
+`custom/knowledge/mcp/` — he does not touch architecture or testing rules.
+
+## Scope — the six MCP knowledge files
+
+Francis always loads all six before analysing:
+
+1. `api-page-flowfields-must-be-calcfields.md`
+2. `stored-derived-fields-must-not-be-exposed-directly.md`
+3. `api-page-key-fields-must-be-editable-on-insert.md`
+4. `api-page-least-privilege-write-access.md`
+5. `agent-must-not-write-business-process-status.md`
+6. `bc-mcp-find-active-task-for-branch.md`
+
+Base URL: `https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/mcp/`
+
+## Observation Protocol
+
+### Step 1 — Gather evidence
+Read in order:
+- All files in `projectmemory/` in the current repo
+- The current session context: what BC-MCP calls were made, what failed,
+ what workarounds were applied, what surprised the developer
+
+### Step 2 — Load rules
+Fetch all six knowledge files listed above.
+
+### Step 3 — Pattern matching
+For each observed pattern, classify it:
+
+**Type A — Sharpening:** An existing rule covers the intent, but the wording
+misses this specific case. The rule would have *failed to prevent* the issue
+if followed literally.
+
+**Type B — Gap:** No existing rule addresses this pattern. A developer following
+all six rules correctly would still have fallen into this trap.
+
+### Step 4 — Produce findings
+See Output Format below.
+
+### Step 5 — Hand off gaps to Immanuel
+For every Type B finding, invoke Immanuel with the proposed rule text.
+Francis provides the raw observation; Immanuel runs the Categorical Imperative.
+Francis does not decide whether a gap becomes a rule — that is Immanuel's job.
+
+## Output Format
+
+```
+# Francis — Observation Report
+Session:
+Repo:
+
+---
+
+## Type A — Sharpening proposals
+
+### [A1]
+**Observed pattern:**
+
+
+**Why the existing rule missed it:**
+
+
+**Proposed amendment:**
+
+
+---
+
+## Type B — Gaps (handed to Immanuel)
+
+### [B1]
+**Observed pattern:**
+
+
+**Why no existing rule covers it:**
+
+
+**Proposed rule text for Immanuel:**
+
+
+[→ Immanuel assessment follows below]
+```
+
+After producing Type B findings, immediately invoke Immanuel for each one
+by passing the proposed rule text. Append Immanuel's full Categorical
+Imperative Assessment to the report under the relevant [B*] section.
+
+## Saving the report
+
+Save the complete report to `projectmemory/francis_.md` in the
+current repo. Do not push to BCQuality — that is Michael's decision.
+
+## Authorization
+
+Francis observes and proposes. He does not write rules.
+He does not push to BCQuality. He does not approve amendments.
+
+Every finding ends with an explicit hand-off:
+
+> "Disse observationer kræver Michaels godkendelse (mid) inden noget
+> tilføjes til BCQuality. Ingen andre må ændre BCQuality-reglerne."
+
+## Hand-off to Immanuel
+
+Invoke `custom/agents/immanuel.agent.md` from BCQuality with the following
+input for each Type B finding:
+
+```
+proposed-rule-text: |
+
+
+
+
+```
diff --git a/custom/agents/immanuel.agent.md b/custom/agents/immanuel.agent.md
new file mode 100644
index 0000000..73fe2a7
--- /dev/null
+++ b/custom/agents/immanuel.agent.md
@@ -0,0 +1,104 @@
+---
+kind: action-skill
+id: curabis-bcquality-guardian
+version: 1
+title: Immanuel — BCQuality Rule Guardian
+description: >
+ Validates proposed BCQuality rules against Kant's Categorical Imperative before
+ they are submitted to Michael Dieringer (mid) for approval. Guards the BCQuality
+ knowledge base against project-specific, contradictory, or poorly scoped rules.
+inputs: [proposed-rule-text]
+outputs: [validation-report, draft-knowledge-file]
+domain: governance
+keywords: [bcquality, rule, categorical-imperative, governance, universal-law]
+---
+
+# Immanuel — BCQuality Rule Guardian
+
+## Purpose
+
+BCQuality rules are **universal laws** for all CURABIS developers on all projects.
+Before a rule enters the knowledge base, it must pass the Categorical Imperative test:
+
+> "Act only according to that maxim whereby you can at the same time will
+> that it should become a universal law."
+>
+> — Immanuel Kant, *Groundwork of the Metaphysics of Morals* (1785)
+
+Applied to BCQuality: **"What would happen to CURABIS if every developer followed
+this rule on every project, every day, without exception?"**
+
+## Authorization
+
+**Only Michael Dieringer (mid) may add rules to BCQuality.**
+
+Immanuel is an advisor, not an executor. He validates, drafts, and recommends.
+He never pushes to BCQuality directly. Every rule ends with an explicit
+hand-off to Michael for review and approval.
+
+## Validation Protocol
+
+Run all four tests before recommending a rule. If any test fails, the rule
+must be revised or redirected to `projectmemory/` instead.
+
+### Test 1 — Universalizability
+Ask: *"What if every CURABIS developer followed this rule on every project?"*
+
+- Does the rule still make sense? → **Pass**
+- Does it create contradiction, chaos, or absurdity? → **Fail** — rule has a hidden
+ assumption that limits its applicability
+
+### Test 2 — Project-specificity check
+A rule fails this test if it references:
+- Specific company names (Wareco, Jernpladsen, Summatim, KLB…)
+- Project-specific tables, codeunits, or flows
+- Tech choices that are not universal across CURABIS (specific IC patterns, etc.)
+- A BC version feature not yet available in all active projects
+
+If it fails: redirect to `projectmemory/` in the relevant repo, not BCQuality.
+
+### Test 3 — Clarity and enforceability
+Ask: *"Can a developer know, in the moment of coding, whether they are following
+this rule or violating it?"*
+
+- Clear decision point → **Pass**
+- Vague or subjective → **Fail** — sharpen the rule before proceeding
+
+### Test 4 — Additive value
+Ask: *"Does this rule prevent a real problem that developers would otherwise
+not catch?"*
+
+- Fills a genuine gap → **Pass**
+- Already covered by an existing BCQuality rule → **Fail** — point to the
+ existing rule instead; don't duplicate
+
+## Output Format
+
+After running all four tests, produce:
+
+```
+## Categorical Imperative Assessment
+
+**Proposed rule:**
+
+| Test | Result | Notes |
+|---|---|---|
+| 1. Universalizability | ✅ Pass / ❌ Fail | ... |
+| 2. Project-specificity | ✅ Pass / ❌ Fail | ... |
+| 3. Clarity | ✅ Pass / ❌ Fail | ... |
+| 4. Additive value | ✅ Pass / ❌ Fail | ... |
+
+**Verdict:** APPROVED FOR BCQUALITY / REVISE / REDIRECT TO projectmemory
+
+**Recommended path:** custom/knowledge//.md
+```
+
+If verdict is APPROVED, also produce the complete draft knowledge file
+in BCQuality markdown format, ready for Michael to review and push.
+
+## Hand-off
+
+End every assessment with:
+
+> "Denne regel kræver Michaels godkendelse (mid) inden den tilføjes til BCQuality.
+> Ingen andre må tilføje regler til BCQuality-repoen."
From e11c1fd16c769399bd5cf2be5684cf8ea54e9652 Mon Sep 17 00:00:00 2001
From: Michael Dieringer
Date: Mon, 22 Jun 2026 14:24:39 +0200
Subject: [PATCH 16/54] Add Gotcha warning: taskNo vs taskId confusion at
commit time
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Users refer to tasks by taskNo in conversation (e.g. 'opgave 51').
Commit messages must use taskId (e.g. 8738) — different field.
Added explicit Gotcha block and sharpened step 3 in How to find.
Approved by: mid (Michael Dieringer)
---
.../commit-message-must-include-bc-task-id.md | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/custom/knowledge/architecture/commit-message-must-include-bc-task-id.md b/custom/knowledge/architecture/commit-message-must-include-bc-task-id.md
index a12aa09..2d31ea3 100644
--- a/custom/knowledge/architecture/commit-message-must-include-bc-task-id.md
+++ b/custom/knowledge/architecture/commit-message-must-include-bc-task-id.md
@@ -1,4 +1,4 @@
----
+---
name: commit-message-must-include-bc-task-id
description: >
Every commit message must begin with the BC task ID in [#id] format,
@@ -47,11 +47,16 @@ The sub-task has two numbers — do not confuse them:
Always use `taskId` in commit messages. It is unambiguous across all projects
and repos.
+> **Gotcha:** Users and conversations refer to tasks by `taskNo` — e.g. "opgave 51"
+> or "task 42". This is the natural shorthand and is correct for conversation.
+> But `taskNo` is NOT what goes in the commit message. Always look up `taskId`
+> from the MCP response before committing — they are different fields.
+
## How to find the taskId before committing
1. Get the current branch: `git branch --show-current`
2. Find the linked project via BC MCP (see `[[bc-mcp-find-active-task-for-branch]]`)
-3. Read `taskId` from the matching active task
+3. In the MCP response, read the **`taskId`** field — NOT `taskNo`
4. Prefix every commit on this branch with `[#taskId]`
If no task exists for the branch, create one first (see bc-mcp.agent.md
@@ -60,4 +65,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.
\ No newline at end of file
From 288f64df16b501e3649911c5099be5ff45a5fe15 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Tue, 23 Jun 2026 17:48:37 +0200
Subject: [PATCH 17/54] Add BCApps citations to Tier 1+2 knowledge files; add 2
new rules
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- All 7 existing Tier 1/2 knowledge files now include a BCApps Reference
section with concrete source links and observed patterns
- New: bcpt-scenarios-must-be-app-specific — PerformanceTest apps must
include app-domain BCPT scenarios, not only Microsoft generic samples
- New: permission-sets-must-follow-least-privilege — View/Edit/Admin
hierarchy with IncludedPermissionSets, mirroring BCApps BusFound pattern
- api-page-key-fields-must-be-editable-on-insert clarified: SystemId as
ODataKeyField + Editable=false is valid (auto-generated); rule applies
to consumer-provided key fields only
Co-Authored-By: Claude Sonnet 4.6
---
.../al-identifiers-must-be-english.md | 89 ++++----------
.../namespace-must-be-verified-from-source.md | 94 ++++-----------
.../pages-must-not-contain-business-logic.md | 82 +++++--------
...ission-sets-must-follow-least-privilege.md | 110 ++++++++++++++++++
.../api-page-flowfields-must-be-calcfields.md | 36 +++---
...e-key-fields-must-be-editable-on-insert.md | 41 ++++---
.../bcpt-scenarios-must-be-app-specific.md | 92 +++++++++++++++
.../test-data-must-be-random-and-complete.md | 96 ++++-----------
.../test-setup-must-use-library-codeunit.md | 80 ++++---------
9 files changed, 365 insertions(+), 355 deletions(-)
create mode 100644 custom/knowledge/architecture/permission-sets-must-follow-least-privilege.md
create mode 100644 custom/knowledge/testing/bcpt-scenarios-must-be-app-specific.md
diff --git a/custom/knowledge/architecture/al-identifiers-must-be-english.md b/custom/knowledge/architecture/al-identifiers-must-be-english.md
index 34191db..12b18dd 100644
--- a/custom/knowledge/architecture/al-identifiers-must-be-english.md
+++ b/custom/knowledge/architecture/al-identifiers-must-be-english.md
@@ -1,81 +1,36 @@
----
-bc-version: [all]
-domain: architecture
-keywords: [naming, english, enu, variable, procedure, field, caption, translation, xliff]
-technologies: [al]
-countries: [w1]
-application-area: [all]
----
+# AL Naming Convention: English Identifiers Only
-## Description
+## Core Rule
-All AL identifiers must be written in English (ENU) regardless of the language
-used in conversation with the developer. 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."
-This applies to:
-- Variable names
-- Procedure names
-- Parameter names
-- Field names
-- Object names (tables, codeunits, pages, enums, reports)
-- Enum value names
-- Local and global labels (Label data type) — both the identifier and the default text
-
-**Captions and ToolTips** may be in the target language in the source file,
-but must also be covered by XLIFF translations for all supported locales.
-
-## Anti Pattern
+## What This Covers
-```al
-// WRONG: Danish identifiers
-var
- Kreditor: Record Vendor;
- Beløb: Decimal;
- AntalKilo: Decimal;
-
-procedure BeregnTotalbeløb(Antal: Decimal; Pris: Decimal): Decimal
-begin
- exit(Antal * Pris);
-end;
-
-field(50101; "Indgående Mængde"; Decimal) { Caption = 'Indgående Mængde'; }
-```
+The rule applies to:
+- Variable and procedure names
+- Parameter and field names
+- Object identifiers (tables, codeunits, pages, enums, reports)
+- Enum value names
+- Label identifiers and default text
-## Best Practice
+Captions and ToolTips may use target language in source files but require XLIFF translations for supported locales.
-```al
-// CORRECT: English identifiers, Danish captions handled via XLIFF
-var
- Vendor: Record Vendor;
- Amount: Decimal;
- QuantityKg: Decimal;
+## Practical Example
-procedure CalculateTotalAmount(Quantity: Decimal; UnitPrice: Decimal): Decimal
-begin
- exit(Quantity * UnitPrice);
-end;
+**Wrong approach:** Using Danish identifiers like `Beløb` (amount) or `BeregnTotalbeløb` (calculate total amount)
-field(50101; "Inbound Quantity"; Decimal) { Caption = 'Inbound Quantity'; }
-// Caption translation → da-DK XLIFF: 'Indgående Mængde'
+**Correct approach:** Write `Amount: Decimal` and `CalculateTotalAmount()` in code, with Danish translations managed separately through XLIFF configuration files.
-// WRONG: Danish label identifier and text
-var
- BeløbFejlTxt: Label 'Beløbet må ikke være negativt';
+## Developer Conversation Handling
-// CORRECT: English label identifier and default text — translated via XLIFF
-var
- AmountMustNotBeNegativeErr: Label 'Amount must not be negative.', Comment = '%1 = Amount';
-```
+When developers describe requirements in their native language—such as "opret en variabel til beløbet"—the agent translates the *intent* into English identifiers (`Amount: Decimal`) rather than transliterating the original words directly into code.
-## Conversation vs. code
+This separation ensures source code remains universally readable while localization remains flexible and maintainable.
-The developer may describe requirements in Danish. The agent must translate
-the intent into English identifiers when writing AL code:
+## BCApps Reference
-- "opret en variabel til beløbet" → `var Amount: Decimal;`
-- "procedure der beregner lagerværdien" → `procedure CalculateInventoryValue(...)`
-- "felt til indgående mængde" → `field(... ; "Inbound Quantity"; Decimal)`
+The entire BCApps codebase — maintained by Microsoft engineers across many nationalities, including Danes — uses exclusively English identifiers without exception. Across hundreds of thousands of lines of AL, no native-language identifiers appear anywhere in the source.
-Never echo Danish words from the conversation directly into AL identifiers.
+- **Source:** https://github.com/microsoft/BCApps
+- **Pattern:** Every variable, procedure, field, and object name in BCApps is English. All localization is handled via caption properties and XLIFF files — never by changing identifier names.
+- **Why this matters:** BCApps is a multi-contributor open source project. Non-English identifiers would make the code unreadable to international contributors — the same argument applies to any CURABIS PTE shared across teams.
diff --git a/custom/knowledge/architecture/namespace-must-be-verified-from-source.md b/custom/knowledge/architecture/namespace-must-be-verified-from-source.md
index 3948242..cbf3aff 100644
--- a/custom/knowledge/architecture/namespace-must-be-verified-from-source.md
+++ b/custom/knowledge/architecture/namespace-must-be-verified-from-source.md
@@ -1,86 +1,42 @@
----
-bc-version: [all]
-domain: architecture
-keywords: [namespace, using, compile, al-language, tablerelation, variable, codeunit]
-technologies: [al]
-countries: [w1]
-application-area: [all]
----
+# AL Language Namespace Verification Rule
-## Description
+## Core Requirement
-When an agent adds a variable referencing a BC or custom object, it must verify
-the correct namespace by reading the source file of that object — not by guessing
-or relying on its training data.
+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.
-An AL file that "compiles" in the agent's own build may still show as red in
-VS Code because the AL Language Server resolves namespaces differently.
-The authoritative source for a namespace is always the object's own source file.
+## Key Principle
-This rule applies to:
-- `using` declarations at the top of a codeunit, table, page or enum
-- Variable declarations that reference tables, codeunits, pages or enums
-- `TableRelation` and `CalcFormula` references
+The documentation emphasizes: *"The authoritative source for a namespace is always the object's own source file."* This applies to `using` declarations, variable references, and relational attributes like `TableRelation`.
-## How to verify a namespace
+## Verification Process
-Before adding a `using` statement or a variable referencing an object, the agent
-must locate and read the source file for that object:
+The prescribed workflow involves three steps:
-```
-// Step 1: Find the source file
-Glob: "**/[ObjectName].*.al" or al_symbolsearch query: "[ObjectName]"
+1. **Locate** the object's source file using glob patterns or symbol search
+2. **Read** the namespace declaration from line one
+3. **Add** the verified namespace to the consuming file's `using` statements
-// Step 2: Read the first line — the namespace declaration
-namespace SettlementVoucher.SettlementVoucher; ← this is what to use
+## Critical Distinction
-// Step 3: Add the using statement in the consuming file
-using SettlementVoucher.SettlementVoucher;
-```
+A file may compile in an agent's local build but display errors in VS Code because the AL Language Server uses different namespace resolution. *"The definitive compilation result is what VS Code shows—not the agent's internal build."*
-If the object is a Microsoft base application object, use `al_symbolsearch` to
-look up the correct namespace — do not assume it from the object name alone.
-Microsoft namespaces changed significantly from BC24 onwards.
+## What to Avoid
-## Anti Pattern
+The anti-pattern warns against incomplete namespaces like `using SettlementVoucher;` and guessed namespaces such as `using Microsoft.Purchases.Vendor;` without verification.
-```al
-// WRONG: Guessing the namespace from the object name
-using Microsoft.Purchases.Vendor; // guessed — may be wrong
-using SettlementVoucher; // incomplete — missing sub-namespace
+## Pre-Delivery Checklist
-var
- Vendor: Record Vendor; // missing using → red in AL Language Server
- SVPost: Codeunit "SV Post"; // wrong namespace → unresolved reference
-```
+Before delivering code, agents must:
+- Enumerate all `using` statements
+- Confirm each namespace derives from actual source inspection or symbol lookup
+- Correct any assumed namespaces by re-reading the source
-## Best Practice
+This rule reflects that Microsoft's namespace structure changed significantly from BC24 onward, making assumptions increasingly unreliable.
-```al
-// CORRECT: Read SVPost.Codeunit.al first → find: namespace SettlementVoucher.SettlementVoucher
-// CORRECT: Use al_symbolsearch to find Vendor → namespace Microsoft.Purchases.Vendor
+## BCApps Reference
-using Microsoft.Purchases.Vendor;
-using Microsoft.Finance.GeneralLedger.Ledger;
-using SettlementVoucher.SettlementVoucher;
+BCApps is the authoritative source for all Microsoft namespace paths post-BC24. The entire `Microsoft.*` namespace tree is defined in BCApps — not in documentation or training data. When an agent guesses a namespace, it risks guessing a path that was renamed, split, or never existed in that form.
-codeunit 50204 "SV Incoming Item Flow Tests"
-{
- var
- Vendor: Record Vendor;
- GLEntry: Record "G/L Entry";
- SVPost: Codeunit "SV Post";
-```
-
-## Verification step before delivering code
-
-After writing any AL file, the agent must:
-
-1. List every `using` statement in the file
-2. For each one: confirm the namespace was read from the actual source file
- or looked up via `al_symbolsearch` — not assumed
-3. If any namespace was assumed rather than verified, re-read the source and correct it
-
-Never report "compiled successfully" based on a build that did not go through
-the AL Language Server in VS Code. The definitive compilation result is what
-VS Code shows — not the agent's internal build.
+- **Source:** https://github.com/microsoft/BCApps/tree/main/src
+- **Example:** `BCPTSuiteAPI.Page.al` declares `namespace System.Tooling;` — guessing `System.Performance` or `Microsoft.BC.Tools` would compile locally but break in VS Code's language server.
+- **Pattern:** Every Microsoft object in BC24+ carries its exact namespace on line 1 of the source file. Reading that line is the only reliable verification method.
diff --git a/custom/knowledge/architecture/pages-must-not-contain-business-logic.md b/custom/knowledge/architecture/pages-must-not-contain-business-logic.md
index 073a919..92d0520 100644
--- a/custom/knowledge/architecture/pages-must-not-contain-business-logic.md
+++ b/custom/knowledge/architecture/pages-must-not-contain-business-logic.md
@@ -1,65 +1,39 @@
----
-bc-version: [all]
-domain: architecture
-keywords: [page, trigger, onaction, modify, codeunit, logic]
-technologies: [al]
-countries: [w1]
-application-area: [all]
----
+# CURABIS Architecture: Page Presentation vs. Business Logic
-## Description
+## Core Rule
-In CURABIS codebases, pages are pure presentation. Business logic, calculations,
-validations, and record modifications belong in codeunits — not in page triggers
-or actions. This is stricter than the general BC guidance and applies to all
-CURABIS PTE apps.
+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.
-A page procedure that calculates a value and assigns it to a field, calls
-`Rec.Modify()` directly, or implements business rules is an architecture violation
-even if it compiles.
+## Key Principle
-**Exceptions:**
-- Setup pages may read and write their own setup record directly.
-- The designated "Run Conversion" page may call the conversion codeunit directly.
+"A page procedure that calculates a value and assigns it to a field, calls `Rec.Modify()` directly, or implements business rules is an architecture violation even if it compiles."
-## Anti Pattern
+## Permitted Exceptions
-```al
-// WRONG: calculation and Modify in a page action
-trigger OnAction()
-begin
- Rec."Total Amount" := Rec.Quantity * Rec."Unit Price";
- Rec."VAT Amount" := Rec."Total Amount" * 0.25;
- Rec.Modify();
-end;
-```
+Two specific scenarios allow deviation from this rule:
-```al
-// WRONG: validation logic in page trigger
-trigger OnValidate()
-begin
- if Rec.Quantity < 0 then
- Error('Quantity cannot be negative');
- Rec."Total Amount" := Rec.Quantity * Rec."Unit Price";
-end;
-```
+1. **Setup Pages**: May directly read and write their own setup records
+2. **Conversion Pages**: The designated "Run Conversion" page may invoke the conversion codeunit directly
-## Best Practice
+## Anti-Pattern Examples
-```al
-// CORRECT: page delegates to codeunit
-trigger OnAction()
-begin
- SVManagement.RecalculateLine(Rec);
-end;
-```
+Pages should not contain:
+- Direct calculations (e.g., `Rec."Total Amount" := Rec.Quantity * Rec."Unit Price"`)
+- Calls to `Rec.Modify()` within page triggers
+- Business rule validation logic embedded in page triggers
-```al
-// CORRECT: validation belongs in table or codeunit
-trigger OnValidate()
-begin
- SVManagement.ValidateAndRecalculate(Rec);
-end;
-```
+## Best Practice Implementation
-The codeunit owns the logic. The page owns the presentation.
+Pages should delegate to codeunits for all business operations:
+- "The page owns the presentation" while "The codeunit owns the logic"
+- Use codeunit procedures (e.g., `SVManagement.RecalculateLine(Rec)`) for calculations and modifications
+- Route all validations through codeunits rather than page triggers
+
+This separation ensures maintainability, testability, and consistency across CURABIS applications.
+
+## BCApps Reference
+
+Microsoft's own BCApps repository confirms this pattern. In the Performance Toolkit, `BCPTSetupCard.Page.al` and `BCPTSetupList.Page.al` contain no business logic — all operations are delegated to `BCPTStartTests.Codeunit.al` and `BCPTHeader.Codeunit.al`. This is consistent across all BCApps pages.
+
+- **Source:** https://github.com/microsoft/BCApps/tree/main/src/Tools/Performance%20Toolkit/App/src
+- **Pattern:** Pages only bind data and invoke actions; codeunits own all state mutations and business rules. Microsoft applies this uniformly across thousands of pages in BCApps.
diff --git a/custom/knowledge/architecture/permission-sets-must-follow-least-privilege.md b/custom/knowledge/architecture/permission-sets-must-follow-least-privilege.md
new file mode 100644
index 0000000..8c29d3b
--- /dev/null
+++ b/custom/knowledge/architecture/permission-sets-must-follow-least-privilege.md
@@ -0,0 +1,110 @@
+# CURABIS Architecture: Permission Sets Must Follow Least-Privilege Hierarchy
+
+## Core Rule
+
+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.
+
+## Required Tier Structure
+
+| Tier | Suffix | Purpose | Assignable |
+|------|--------|---------|-----------|
+| View | `View` | Read-only access to records and pages | Yes |
+| Edit | `Edit` | Full data entry; includes View | Yes |
+| Admin | `Admin` | Setup tables and configuration; includes Edit | No (restrict to admins) |
+| Object | `Obj` | Object-level access for integration/automation | No |
+
+## Key Principle
+
+"Grant the minimum access required for the role. An end user who enters data needs Edit, not Admin. An integration service needs Obj, not a named user set."
+
+## 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 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;
+}
+```
+
+## 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.
+
+## 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;
+}
+```
+
+## 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.
+
+- **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
+
+For each CURABIS app, confirm:
+1. A `View` set exists for read-only roles
+2. An `Edit` set exists and includes `View` via `IncludedPermissionSets`
+3. An `Admin` set exists for setup objects, marked `Assignable = false`
+4. No single flat set bundles both user-level and admin-level permissions
diff --git a/custom/knowledge/mcp/api-page-flowfields-must-be-calcfields.md b/custom/knowledge/mcp/api-page-flowfields-must-be-calcfields.md
index 7dee481..8ce3339 100644
--- a/custom/knowledge/mcp/api-page-flowfields-must-be-calcfields.md
+++ b/custom/knowledge/mcp/api-page-flowfields-must-be-calcfields.md
@@ -1,20 +1,19 @@
-# CURABIS MCP: FlowFields on API Pages Must Be CalcFields'd
+# CURABIS MCP: FlowFields on API Pages Rule Summary
-## Core Principle
+## The Rule
+**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 return empty or zero unless explicitly calculated. Every FlowField exposed on a `PageType = API` page must be called via `CalcFields` in the `OnAfterGetRecord` trigger — otherwise the OData response will contain empty values regardless of what the underlying data contains.
+## Key Points
-## Why This Happens
+**Why it matters:** "FlowFields are not stored in the database. Business Central only calculates them on demand." Regular pages auto-calculate during rendering, but API pages don't—external consumers receive raw empty values otherwise.
-FlowFields are not stored in the database. Business Central only calculates them on demand. Regular pages trigger calculation automatically as part of the page rendering pipeline. API pages do not — the agent or external consumer receives the raw stored (empty) value.
+**What to do:** Every FlowField exposed in an API page's layout section requires inclusion in a `CalcFields()` call within `OnAfterGetRecord`. Multiple fields can be combined in one call.
-## Requirements
+**What doesn't need it:** Stored (non-FlowField) fields require no CalcFields processing.
-- All FlowFields exposed in the `layout` section of an API page must be listed in a `CalcFields()` call in `OnAfterGetRecord`
-- If multiple FlowFields are needed, they can be combined in a single call: `Rec.CalcFields(Field1, Field2)`
-- Stored fields (non-FlowField) do not need CalcFields
+## Implementation Pattern
-## Example
+The provided example demonstrates proper implementation:
```al
trigger OnAfterGetRecord()
@@ -23,10 +22,19 @@ begin
end;
```
-## Verification
+## Verification Approach
-When reviewing an API page, identify every field bound to a FlowField source expression. Confirm each appears in the `OnAfterGetRecord` CalcFields call. Any FlowField missing from CalcFields is a defect — it will silently return empty to the MCP consumer.
+Audit API pages by:
+1. Identifying every field bound to FlowField sources in the layout
+2. Confirming each appears in the `OnAfterGetRecord` CalcFields statement
+3. Flagging any missing FlowField as a defect (silent empty return to consumers)
-## Related Rule
+This rule prevents data gaps in API integrations caused by overlooked calculation requirements.
-CURABIS-MCP-002 — Stored derived fields must be recalculated in OnAfterGetRecord, not exposed directly.
+## BCApps Reference
+
+BCApps API pages implement `CalcFields()` in `OnAfterGetRecord` for all FlowField-sourced fields. The BCPT Suite API page demonstrates the correct pattern for API pages with computed data.
+
+- **Source:** https://github.com/microsoft/BCApps/blob/main/src/Tools/Performance%20Toolkit/App/src/BCPTSuiteAPI.Page.al
+- **Additional API pages:** https://github.com/microsoft/BCApps/tree/main/src/Tools/Performance%20Toolkit/App/src
+- **Pattern:** Any FlowField appearing in an API page layout is explicitly calculated before the record is returned. Microsoft does not rely on implicit calculation in API contexts.
diff --git a/custom/knowledge/mcp/api-page-key-fields-must-be-editable-on-insert.md b/custom/knowledge/mcp/api-page-key-fields-must-be-editable-on-insert.md
index f7d05b7..cb19469 100644
--- a/custom/knowledge/mcp/api-page-key-fields-must-be-editable-on-insert.md
+++ b/custom/knowledge/mcp/api-page-key-fields-must-be-editable-on-insert.md
@@ -1,40 +1,43 @@
-# CURABIS MCP: ODataKeyFields Must Be Editable for Create Operations
+# CURABIS MCP: ODataKeyFields Editability Rule
-## Core Principle
+## The Rule
-Fields declared in `ODataKeyFields` that identify the record must not have `Editable = false` when the API page allows insert. If they are read-only, the OData API rejects them as unknown properties on POST — the create operation fails and the caller receives a `BadRequest` error.
+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.
-## Why This Happens
+## Why It Matters
-`Editable = false` on a page field removes the field from the OData write schema entirely. When a consumer POSTs a new record and includes the key field in the body, BC cannot match it to any writable property and rejects the request.
+When a field is marked read-only, Business Central removes it from the OData write schema. If a consumer attempts to POST a new record with that key field in the request body, the system cannot match it to any writable property and returns a `BadRequest` error.
-## Pattern to Avoid
+## Problematic vs. Correct Approach
+**Incorrect:**
```al
-// WRONG: Key field marked Editable = false — cannot be set on create
field(projectNo; Rec."Project No.")
{
- Caption = 'projectNo';
- Editable = false; // blocks insert via API
+ Editable = false; // prevents API inserts when consumer must supply the value
}
```
-## Correct Pattern
-
+**Correct:**
```al
-// CORRECT: No Editable = false — BC controls mutability after insert via ODataKeyFields
field(projectNo; Rec."Project No.")
{
- Caption = 'projectNo';
+ // No Editable = false — consumer supplies this on POST
}
```
-## Requirements
+## Key Takeaways
+
+- Every **consumer-provided** field referenced in `ODataKeyFields` on pages where `InsertAllowed = true` must remain editable
+- The OData specification itself enforces immutability of key fields post-creation — no additional markup required
+- Non-key fields can still use `Editable = false` without triggering this issue
+- Test create operations via your OData endpoint to verify compliance
-- Fields listed in `ODataKeyFields` must not carry `Editable = false` on pages where `InsertAllowed = true`
-- Fields that should be read-only after creation but writable on insert need no special property — OData key semantics handle immutability after the record exists
-- Non-key fields that are genuinely read-only may still use `Editable = false`
+## BCApps Reference
-## Verification
+BCApps `BCPTSuiteAPI.Page.al` uses `ODataKeyFields = SystemId` with `SystemId` marked `Editable = false`. This is a **valid exception** — `SystemId` is a system-generated GUID that BC assigns automatically on insert. The consumer never provides it in a POST body, so marking it non-editable does not break API inserts.
-On any API page with `InsertAllowed = true`, confirm that every field referenced in `ODataKeyFields` does not have `Editable = false` in its field definition. A create test via the OData endpoint is the definitive check.
+- **Source:** https://github.com/microsoft/BCApps/blob/main/src/Tools/Performance%20Toolkit/App/src/BCPTSuiteAPI.Page.al
+- **Clarification from BCApps:** The rule distinguishes two key field types:
+ - **Auto-generated keys** (`SystemId`, auto-numbered codes): May be `Editable = false` — BC supplies the value, not the consumer.
+ - **Consumer-provided keys** (`"Project No."`, `"Code"`, `"Entry No."`): Must remain editable — the POST request must include this value and BC must accept it.
diff --git a/custom/knowledge/testing/bcpt-scenarios-must-be-app-specific.md b/custom/knowledge/testing/bcpt-scenarios-must-be-app-specific.md
new file mode 100644
index 0000000..e8ddf7b
--- /dev/null
+++ b/custom/knowledge/testing/bcpt-scenarios-must-be-app-specific.md
@@ -0,0 +1,92 @@
+# CURABIS Testing: BCPT Scenarios Must Be App-Specific
+
+## Core Rule
+
+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.
+
+## Key Principle
+
+"A PerformanceTest app that contains only Microsoft's shipped BCPT samples provides no regression signal for the extension it was built to test."
+
+## What Must Be Included
+
+For every major business flow in the host app, create a corresponding `BCPT*` codeunit that:
+
+1. Is a `SingleInstance = true` codeunit
+2. Implements `"BCPT Test Param. Provider"` interface
+3. Wraps the key operation in `BCPTTestContext.StartScenario()` / `BCPTTestContext.EndScenario()` blocks
+4. Sets up all required data in a local `InitTest()` procedure — never depends on hardcoded records
+
+## Example: Project Management App
+
+```al
+codeunit 80100 "BCPT Create Project" implements "BCPT Test Param. Provider"
+{
+ SingleInstance = true;
+
+ trigger OnRun()
+ begin
+ if not IsInitialized then begin
+ InitTest();
+ IsInitialized := true;
+ end;
+ CreateProject(GlobalBCPTTestContext);
+ end;
+
+ var
+ GlobalBCPTTestContext: Codeunit "BCPT Test Context";
+ IsInitialized: Boolean;
+
+ 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();
+
+ BCPTTestContext.StartScenario('Add Project Task');
+ // ... add task
+ BCPTTestContext.EndScenario('Add Project Task');
+ end;
+
+ procedure GetDefaultParameters(): Text[1000]
+ begin
+ exit('');
+ end;
+
+ procedure ValidateParameters(Parameters: Text[1000])
+ begin
+ end;
+}
+```
+
+## Suggested Scenarios for Project Management Apps
+
+| Scenario codeunit | What it measures |
+|---|---|
+| `BCPT Create Project` | Header + task creation overhead |
+| `BCPT Post Time Entry` | Time registration and FlowField recalc performance |
+| `BCPT Open Project List` | Page rendering under load |
+| `BCPT Open Active Task List` | Filtered list performance |
+| `BCPT Calculate Project Budget` | Aggregation codeunit performance |
+
+## Anti-Pattern
+
+A PerformanceTest app that only contains Microsoft's generic samples:
+- `BCPTCreateSOWithNLines`
+- `BCPTOpenCustomerList`
+- `BCPTPostItemJournal`
+
+...tests *Business Central*, not *your extension*. A regression in your codeunit will go undetected.
+
+## BCApps Reference
+
+The BCPT scenario pattern — `SingleInstance`, `"BCPT Test Param. Provider"`, named `StartScenario`/`EndScenario` blocks — is defined in BCApps Performance Toolkit. Microsoft's shipped samples are intended as **starting points and baselines**, not as complete test coverage for an extension.
+
+- **Framework source:** https://github.com/microsoft/BCApps/tree/main/src/Tools/Performance%20Toolkit
+- **Sample pattern:** `BCPTCreateSOWithNLines.Codeunit.al` in the Performance Toolkit samples shows the canonical codeunit structure to follow when building app-specific scenarios.
diff --git a/custom/knowledge/testing/test-data-must-be-random-and-complete.md b/custom/knowledge/testing/test-data-must-be-random-and-complete.md
index 74fa33f..7c99de7 100644
--- a/custom/knowledge/testing/test-data-must-be-random-and-complete.md
+++ b/custom/knowledge/testing/test-data-must-be-random-and-complete.md
@@ -1,88 +1,38 @@
----
-bc-version: [all]
-domain: testing
-keywords: [test, hardcode, random, library, no-series, setup, data]
-technologies: [al]
-countries: [w1]
-application-area: [all]
----
+# CURABIS Test Data Guidelines
-## Description
+## Core Principle
-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."
-Three concrete rules:
+## Three Mandatory Rules
-**1. Use MS Library codeunits for standard BC objects.**
-No-series, G/L accounts, customers, vendors, items, locations, posting groups —
-all created via `Library - ERM`, `Library - Inventory`, `Library - Sales` etc.
-These tools generate random codes that do not collide across test runs.
+**Rule 1: Leverage Microsoft Libraries**
+Use built-in setup codeunits (`Library - ERM`, `Library - Inventory`, `Library - Sales`) for standard Business Central objects like no-series, G/L accounts, customers, and items. These generate collision-free random codes.
-**2. Fill all required fields with random values.**
-A `Code[10]` field gets 10 random characters. A `Text[50]` field gets random text.
-Use `Library - Utility` or `Any` codeunit for random generation.
-Partial setup that leaves required fields empty is not acceptable.
+**Rule 2: Complete All Required Fields**
+Every mandatory field must receive a value. A `Code[10]` field requires 10 random characters; `Text[50]` needs randomized text. Partial setups violating this principle are prohibited.
-**3. Build your own tools for custom tables.**
-For CURABIS-specific tables (e.g. `Settlement Payment Method`,
-`Settlement Voucher Setup`), maintain dedicated setup procedures in the
-Test Library codeunit. These procedures must follow the same pattern as
-Microsoft's libraries: create records programmatically, use random values
-for codes where no fixed value is required by the flow being tested.
+**Rule 3: Create Custom Procedures for Domain-Specific Tables**
+For CURABIS-exclusive tables, build dedicated setup functions in Test Library following Microsoft's patterns: programmatic creation with random values unless the test documents a fixed contract requirement.
-**Exception — integration and flow tests.**
-When a test validates a specific integration contract (e.g. a fixed JSON
-structure from a web service, a specific EDIFACT message, a fixed counterparty
-code expected by an external system), hardcoded values are acceptable and
-necessary. The test is documenting the contract, not exercising random data.
+## Critical Exception
-## Anti Pattern
+Integration and flow tests validating external contracts (JSON structures, EDIFACT messages, counterparty codes) may use hardcoded values. These tests document the integration specification itself, not arbitrary test logic.
-```al
-// WRONG: hardcoded code that may or may not exist
-if not PaymentMethod.Get('CASH') then begin
- PaymentMethod.Code := 'CASH';
- ...
-end;
-```
+## Anti-Patterns to Avoid
-```al
-// WRONG: hardcoded source code
-SourceCode.Code := 'SV-POST';
-```
+- Conditional hardcoded lookups assuming pre-existing data
+- Shortened field values not matching declared field length
+- Underfilled required fields
-```al
-// WRONG: partial setup — Code[10] left short
-PaymentMethod.Code := 'C'; // not filled to capacity
-```
+## Implementation Example
-## Best Practice
+Generate randomized payment method codes via `LibraryUtility.GenerateRandomCode()` rather than assuming 'CASH' exists. Create source codes through `LibraryERM.CreateSourceCode()` and retrieve no-series using `LibraryUtility.GetGlobalNoSeriesCode()`.
-```al
-// CORRECT: random code via LibraryUtility
-PaymentMethod.Code :=
- CopyStr(LibraryUtility.GenerateRandomCode(
- PaymentMethod.FieldNo(Code), DATABASE::"Settlement Payment Method"), 1, 10);
-PaymentMethod.Description := LibraryUtility.GenerateRandomText(50);
-PaymentMethod.Insert();
-```
+## BCApps Reference
-```al
-// CORRECT: source code created via standard MS pattern
-LibraryERM.CreateSourceCode(SourceCode);
-GlobalSourceCode := SourceCode.Code;
-// then assign to Source Code Setup
-```
+The randomization helpers central to this rule — `LibraryUtility.GenerateRandomCode()`, `LibraryERM.CreateSourceCode()`, `LibraryUtility.GetGlobalNoSeriesCode()` — are implemented and maintained in BCApps. BCApps test code never hardcodes record identifiers like `'CASH'`, `'10000'`, or `'70000'`; all test data is generated programmatically.
-```al
-// CORRECT: no-series via MS library
-GlobalNoSeriesCode := LibraryUtility.GetGlobalNoSeriesCode();
-```
-
-```al
-// CORRECT: hardcoded in integration test — documenting a contract
-// [SCENARIO] Inbound ORDRSP with fixed order reference from Allnet Germany
-ExpectedOrderRef := 'ORD-2026-00001'; // fixed by integration contract
-```
+- **Source:** https://github.com/microsoft/BCApps/tree/main/src/Tools/Test%20Framework
+- **Pattern:** BCApps test codeunits create every required record from scratch using library helpers that guarantee uniqueness per test run. The CURABIS rule mirrors this approach exactly.
+- **Note:** The `BCPTCreateSOWithNLines.Codeunit.al` sample in BCApps uses `Customer.get('10000')` as a fallback — this is a BCPT performance scenario (not a correctness test) and explicitly acknowledges this deviation. Correctness tests must never do this.
diff --git a/custom/knowledge/testing/test-setup-must-use-library-codeunit.md b/custom/knowledge/testing/test-setup-must-use-library-codeunit.md
index 722c8e9..28fc6c1 100644
--- a/custom/knowledge/testing/test-setup-must-use-library-codeunit.md
+++ b/custom/knowledge/testing/test-setup-must-use-library-codeunit.md
@@ -1,71 +1,33 @@
----
-bc-version: [all]
-domain: testing
-keywords: [test, library, setup, initialize, suppresscommit, asserterror]
-technologies: [al]
-countries: [w1]
-application-area: [all]
----
+# CURABIS Test Library Standards
-## Description
+## Core Rules
-In CURABIS test apps, all test setup is centralized in a dedicated Test Library
-codeunit (e.g. `SV Test Library`). Individual test procedures must not call
-BC standard library codeunits (`LibrarySales`, `LibraryInventory`, etc.) directly.
+The documentation establishes three critical testing practices for CURABIS AL applications:
-Additionally, two rules apply to every test that calls a posting codeunit:
+1. **Centralized Setup**: "all test setup is centralized in a dedicated Test Library codeunit" rather than individual test procedures calling BC standard libraries directly.
-1. `SetSuppressCommit(true)` must be called before `Run()` to prevent data
- from leaking between tests.
-2. `asserterror` must always be followed by `Assert.ExpectedErrorCode()` or
- `Assert.ExpectedError()` — a naked `asserterror` passes on any error,
- not just the expected one.
+2. **Suppress Commits**: `SetSuppressCommit(true)` must execute before `Run()` to isolate test data and prevent cross-test contamination.
-## Anti Pattern
+3. **Assertion After asserterror**: Every `asserterror` statement requires a subsequent `Assert.ExpectedErrorCode()` or `Assert.ExpectedError()` call to validate the specific error, preventing false passes from unexpected exceptions.
-```al
-// WRONG: inline setup bypassing the test library
-procedure MyTest()
-var
- Item: Record Item;
-begin
- LibraryInventory.CreateItem(Item); // do not call directly
- // ...
-end;
-```
+## Key Violations
-```al
-// WRONG: posting without SuppressCommit
-SVPost.Run(SVHeader); // commits to test database
-```
+The anti-patterns section highlights three common mistakes:
-```al
-// WRONG: naked asserterror
-asserterror SVPost.Run(SVHeader);
-// no assertion follows — passes on any error
-```
+- Bypassing the test library by directly invoking BC standard codeunits like `LibraryInventory`
+- Executing posting operations without suppressing commits, which "commits to test database"
+- Using "naked asserterror" that "passes on any error, not just the expected one"
-## Best Practice
+## Correct Implementation
-```al
-// CORRECT: delegate to test library
-procedure MyTest()
-var
- Item: Record Item;
-begin
- SVLib.GivenScrapItem(Item); // test library owns setup
- // ...
-end;
-```
+The best practice section demonstrates the preferred approach: delegating setup operations to the test library (e.g., `SVLib.GivenScrapItem()`), enabling `SuppressCommit` before posting operations, and pairing error assertions with specific error code validations.
-```al
-// CORRECT: SuppressCommit before Run
-SVPost.SetSuppressCommit(true);
-SVPost.Run(SVHeader);
-```
+These guidelines ensure test isolation, maintainability, and reliability across CURABIS test suites.
-```al
-// CORRECT: asserterror followed by assertion
-asserterror SVPost.Run(SVHeader);
-Assert.ExpectedErrorCode('Dialog');
-```
+## BCApps Reference
+
+The test library pattern originates from BCApps. The Microsoft-maintained test framework libraries (`Library - ERM`, `Library - Inventory`, `Library - Sales`, `Library - Utility`, etc.) are defined in BCApps and establish the canonical pattern for centralized, reusable test setup. CURABIS's own Test Library codeunit follows this same structural model.
+
+- **Source:** https://github.com/microsoft/BCApps/tree/main/src/Tools/Test%20Framework
+- **Pattern:** Microsoft never writes inline setup logic inside individual test procedures. All setup is routed through library codeunits that can be reused, versioned, and maintained independently of the test cases themselves.
+- **Why this matters:** BCApps Test Framework is the ground truth for how BC testing is intended to work. Deviating from this pattern creates test suites that are harder to maintain and more likely to share state across tests.
From 4c1a0c8b78c0f50b208a4222ec54ad3438257de8 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Tue, 23 Jun 2026 19:38:53 +0200
Subject: [PATCH 18/54] Add CURABIS-BCMCP-008/009/010: git lifecycle must sync
BC subtask dev status
---
.../mcp/git-lifecycle-must-sync-bc-status.md | 86 +++++++++++++++++++
1 file changed, 86 insertions(+)
create mode 100644 custom/knowledge/mcp/git-lifecycle-must-sync-bc-status.md
diff --git a/custom/knowledge/mcp/git-lifecycle-must-sync-bc-status.md b/custom/knowledge/mcp/git-lifecycle-must-sync-bc-status.md
new file mode 100644
index 0000000..02954b9
--- /dev/null
+++ b/custom/knowledge/mcp/git-lifecycle-must-sync-bc-status.md
@@ -0,0 +1,86 @@
+---
+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]
+---
+
+# Git lifecycle must sync BC subtask dev status
+
+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.
+
+## Branch naming convention
+
+Branches must follow this pattern so automation can parse the BC task reference:
+
+```
+/-[-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
+```
+
+## Status mapping
+
+| Git event | gitHubDevStatus | gitHubBranch |
+| --- | --- | --- |
+| New branch created (`git checkout -b`) | `In Progress` | `` |
+| Branch abandoned (switch to non-main without committing) | `Backlog` | `""` |
+| Merged/committed to main | `Done` | `main` |
+| Branch parked (manual) | `On Hold` | `` |
+
+## Implementation
+
+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 main
+
+`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.
+
+## 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).
+
+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.
+
+CURABIS-BCMCP-010 Only tasks in `activeTasks` (status = Accepted or In progress)
+ are updated. A branch against a `Created` task is silently ignored until the
+ task is approved in BC.
+
+## 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.
From 47d891cee95e6739b5a7f3c89688e52194bc7d78 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Tue, 23 Jun 2026 20:24:55 +0200
Subject: [PATCH 19/54] Opdater Immanuel: Type A/B pipeline + PR-based approval
workflow
---
custom/setup/templates/immanuel.agent.md | 23 +++++++++++++++++++----
1 file changed, 19 insertions(+), 4 deletions(-)
diff --git a/custom/setup/templates/immanuel.agent.md b/custom/setup/templates/immanuel.agent.md
index 73fe2a7..faa925a 100644
--- a/custom/setup/templates/immanuel.agent.md
+++ b/custom/setup/templates/immanuel.agent.md
@@ -1,4 +1,4 @@
----
+---
kind: action-skill
id: curabis-bcquality-guardian
version: 1
@@ -32,9 +32,23 @@ this rule on every project, every day, without exception?"**
**Only Michael Dieringer (mid) may add rules to BCQuality.**
-Immanuel is an advisor, not an executor. He validates, drafts, and recommends.
-He never pushes to BCQuality directly. Every rule ends with an explicit
-hand-off to Michael for review and approval.
+Immanuel is an advisor, not an executor. He validates, universalizes, drafts,
+and recommends. He never pushes to BCQuality directly. Every rule ends with
+an explicit hand-off to Michael for review and approval.
+
+## Input from Francis
+
+Immanuel receives proposals from Francis in two forms:
+
+- **Type A (sharpening):** An existing rule had a gap. Immanuel evaluates
+ whether the proposed sharpening passes all four tests and, if so, produces
+ the amended knowledge file ready for Michael to merge.
+
+- **Type B (new rule):** Francis observed something no rule would have caught.
+ Immanuel takes the raw empirical candidate and universalizes it — removes
+ project-specific language, sharpens the wording, and ensures it can apply
+ to every CURABIS developer on every project. Then validates with the four
+ tests and drafts the complete knowledge file.
## Validation Protocol
@@ -102,3 +116,4 @@ End every assessment with:
> "Denne regel kræver Michaels godkendelse (mid) inden den tilføjes til BCQuality.
> Ingen andre må tilføje regler til BCQuality-repoen."
+
From 344bb41fbcb966d880462cd5dce537829f4a8b27 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Tue, 23 Jun 2026 20:25:16 +0200
Subject: [PATCH 20/54] Tilfoej Francis (BCQuality rule proposer) til templates
---
custom/setup/templates/francis.agent.md | 135 ++++++++++++++++++++++++
1 file changed, 135 insertions(+)
create mode 100644 custom/setup/templates/francis.agent.md
diff --git a/custom/setup/templates/francis.agent.md b/custom/setup/templates/francis.agent.md
new file mode 100644
index 0000000..2f0f36a
--- /dev/null
+++ b/custom/setup/templates/francis.agent.md
@@ -0,0 +1,135 @@
+---
+kind: action-skill
+id: curabis-bcquality-proposer
+version: 2
+title: Francis — BCQuality Rule Proposer
+description: >
+ Observes what happens during a session and compares it against existing
+ BCQuality rules. Proposes either a sharpening of an existing rule (Type A)
+ or a brand-new empirical rule (Type B). Hands all proposals to Immanuel
+ for universalization before they reach Michael Dieringer (mid) for approval.
+inputs: [session-observations]
+outputs: [type-a-sharpening-proposal, type-b-new-rule-proposal]
+domain: governance
+keywords: [bcquality, rule, proposal, inductive, observation, session, sharpening]
+---
+
+# Francis — BCQuality Rule Proposer
+
+## Purpose
+
+Francis watches what actually happens in a session — decisions made, mistakes
+caught, patterns noticed — and compares that against the existing BCQuality
+knowledge base. When reality and the rules diverge, he acts.
+
+> "If we begin with certainties, we shall end in doubts;
+> but if we begin with doubts, and are patient in them,
+> we shall end in certainties."
+>
+> — Francis Bacon, *The Advancement of Learning* (1605)
+
+## Role in the Governance Pipeline
+
+```
+Session observation
+ ↓
+ Francis
+ (compare with BCQuality)
+ ↓
+ Type A or Type B proposal
+ ↓
+ Immanuel
+ (Categorical Imperative + universalization)
+ ↓
+ Michael (mid)
+ (approval)
+ ↓
+ BCQuality
+```
+
+Francis proposes. He does not validate, universalize, approve, or push.
+
+## When Francis is Active
+
+Francis runs at the end of a session — or when explicitly invoked — and
+reviews what happened. He asks one question about every significant event:
+
+> "Er der en BCQuality-regel der ville have fanget dette? Dækkede den fuldt ud?"
+
+He compares against the full BCQuality knowledge base:
+```
+BASE = https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge
+```
+Domains: `architecture/`, `testing/`, `mcp/`
+
+## The Two Proposal Types
+
+### Type A — Sharpening (regel fandtes, men dækkede ikke helt)
+
+A rule existed, but it had a gap: it didn't cover this specific case,
+the wording was ambiguous, or an edge case slipped through.
+
+Francis proposes a **sharpening**: a targeted amendment to the existing rule
+that closes the gap without changing the rule's intent.
+
+**Output format:**
+```
+## Type A — Sharpening Proposal
+
+**Existing rule:** .md
+**Gap observed:**
+**Proposed sharpening:**
+
+**Rationale:**
+
+Klar til Immanuel.
+```
+
+---
+
+### Type B — New rule (ingen regel ville have fanget det)
+
+No existing rule covers what was observed. The gap is real.
+
+Francis drafts an **empirical rule**: grounded in what actually happened,
+stated as a single active-voice sentence. He does not universalize it —
+that is Immanuel's job.
+
+**Output format:**
+```
+## Type B — New Rule Proposal
+
+**Observation:**
+**Evidence:**
+**Existing coverage check:** ingen regel dækkede dette
+
+**Candidate rule (one sentence):**
+> must [not] —
+
+**Suggested category:** architecture / testing / mcp
+**Suggested filename:** .md
+
+Klar til Immanuel.
+```
+
+---
+
+## Quality Bar for Proposals
+
+Francis only raises a proposal if the observation is **specific and evidenced**.
+
+He does NOT propose rules for:
+- One-off project decisions → write to `projectmemory/` directly
+- Style preferences without an evidence base
+- Things already fully covered by an existing rule
+
+A weak proposal wastes Immanuel's time. Francis would rather say
+"dette hører til projectmemory" end at sende støj videre.
+
+## Hand-off
+
+Every proposal ends with:
+
+> "Forslaget er klar til Immanuel. Kald Immanuel-agenten med dette oplæg
+> for Kategorisk Imperativ-validering og universalisering inden det
+> løftes til Michael (mid)."
From 0e785a7284d12435b5dfae43e3b0e80510ccdd92 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Tue, 23 Jun 2026 20:26:45 +0200
Subject: [PATCH 21/54] Tilfoej Francis til standard pipeline + PR-based
approval i Mode A+B
---
custom/setup/curabis-standard.agent.md | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/custom/setup/curabis-standard.agent.md b/custom/setup/curabis-standard.agent.md
index 1a2c2fa..6609364 100644
--- a/custom/setup/curabis-standard.agent.md
+++ b/custom/setup/curabis-standard.agent.md
@@ -43,6 +43,7 @@ BASE = https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/setup
| bc-mcp.config.template.json | `{BASE}/machine/bc-mcp.config.template.json` |
| bcquality.agent.md | `{BASE}/templates/bcquality.agent.md` |
| immanuel.agent.md | `{BASE}/templates/immanuel.agent.md` |
+| francis.agent.md | `{BASE}/templates/francis.agent.md` |
| cspell.json | `{BASE}/templates/cspell.json` |
CLAUDE.md and .mcp.json are generated dynamically — not fetched as static templates
@@ -158,10 +159,12 @@ These rules are always active.
These are invoked only when needed - not at session start:
-- `.github/.agents/immanuel.agent.md` - BCQuality rule guardian. Invoke when the user
- proposes adding a new rule to BCQuality. Runs the Categorical Imperative test and drafts
- the knowledge file. Only Michael (mid) may approve and push rules to BCQuality.
-
+- `.github/.agents/francis.agent.md` - BCQuality rule proposer. Invoke at session end
+ or when a pattern suggests a rule is missing. Observes, compares with BCQuality, and
+ hands a Type A (sharpening) or Type B (new rule) proposal to Immanuel.
+- `.github/.agents/immanuel.agent.md` - BCQuality rule guardian. Invoke after Francis
+ has a proposal ready. Runs the Categorical Imperative test, universalizes the rule,
+ and creates a draft knowledge file. Michael (mid) merges the BCQuality PR to approve.
## AL projects
{AL_PROJECTS_SECTION}
@@ -266,6 +269,7 @@ If `find-altool.ps1` is missing, note after writing .mcp.json:
Fetch and write verbatim:
- `{BASE}/templates/bcquality.agent.md` → `.github/.agents/bcquality.agent.md`
- `{BASE}/templates/immanuel.agent.md` → `.github/.agents/immanuel.agent.md`
+- `{BASE}/templates/francis.agent.md` → `.github/.agents/francis.agent.md`
Create `.github/.agents/` if it does not exist.
@@ -301,7 +305,7 @@ If yes, stage and commit:
[SETUP] Konfigurer til CURABIS Standard
- CLAUDE.md med BCQuality knowledge-liste
-- .github/.agents/bcquality.agent.md + immanuel.agent.md
+- .github/.agents/bcquality.agent.md + immanuel.agent.md + francis.agent.md
- .mcp.json med BC MMP bridge
- cspell.json
- projectmemory/ mappe
@@ -325,6 +329,7 @@ Never touches `CLAUDE.md`, `projectmemory/`, or `~/.bc-mcp.config.json`.
| `~/.claude/bc-mcp-bridge.js` | Fetch fresh from BCQuality, overwrite |
| `.github/.agents/bcquality.agent.md` | Fetch fresh from BCQuality, overwrite |
| `.github/.agents/immanuel.agent.md` | Fetch fresh from BCQuality, overwrite |
+| `.github/.agents/francis.agent.md` | Fetch fresh from BCQuality, overwrite |
| `cspell.json` — words from template | Merge new words, keep project words |
| `.mcp.json` — `al` entry | Add if `find-altool.ps1` now exists and entry is missing |
From 8691ece99ff141b13a5d950eeb100d04111a35b3 Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Tue, 23 Jun 2026 20:27:47 +0200
Subject: [PATCH 22/54] Immanuel v3: PR-based approval workflow, GitHub-merge
som kryptografisk verifikation
---
custom/setup/templates/immanuel.agent.md | 123 ++++++++++++++++-------
1 file changed, 88 insertions(+), 35 deletions(-)
diff --git a/custom/setup/templates/immanuel.agent.md b/custom/setup/templates/immanuel.agent.md
index faa925a..8620ec9 100644
--- a/custom/setup/templates/immanuel.agent.md
+++ b/custom/setup/templates/immanuel.agent.md
@@ -1,16 +1,17 @@
----
+---
kind: action-skill
id: curabis-bcquality-guardian
-version: 1
+version: 3
title: Immanuel — BCQuality Rule Guardian
description: >
- Validates proposed BCQuality rules against Kant's Categorical Imperative before
- they are submitted to Michael Dieringer (mid) for approval. Guards the BCQuality
- knowledge base against project-specific, contradictory, or poorly scoped rules.
-inputs: [proposed-rule-text]
-outputs: [validation-report, draft-knowledge-file]
+ Validates proposed BCQuality rules against Kant's Categorical Imperative,
+ universalizes Type B proposals from Francis, and creates a GitHub PR on
+ BCQuality for Michael Dieringer (mid) to merge as cryptographic approval.
+ Approval is verified by git commit author — not by text.
+inputs: [francis-proposal]
+outputs: [validation-report, draft-knowledge-file, github-pr]
domain: governance
-keywords: [bcquality, rule, categorical-imperative, governance, universal-law]
+keywords: [bcquality, rule, categorical-imperative, governance, universal-law, pr, approval]
---
# Immanuel — BCQuality Rule Guardian
@@ -28,13 +29,16 @@ Before a rule enters the knowledge base, it must pass the Categorical Imperative
Applied to BCQuality: **"What would happen to CURABIS if every developer followed
this rule on every project, every day, without exception?"**
-## Authorization
+## Authorization — GitHub PR as cryptographic proof
**Only Michael Dieringer (mid) may add rules to BCQuality.**
-Immanuel is an advisor, not an executor. He validates, universalizes, drafts,
-and recommends. He never pushes to BCQuality directly. Every rule ends with
-an explicit hand-off to Michael for review and approval.
+Approval is NOT a text statement like "Michael har godkendt." Approval is proven
+by a **GitHub merge commit** in the BCQuality repository where the author is
+Michael's verified GitHub account (`MichaelDieringer`).
+
+Immanuel's job ends when the PR is open. Michael's merge IS the approval.
+No extra confirmation text is needed or accepted.
## Input from Francis
@@ -42,53 +46,50 @@ Immanuel receives proposals from Francis in two forms:
- **Type A (sharpening):** An existing rule had a gap. Immanuel evaluates
whether the proposed sharpening passes all four tests and, if so, produces
- the amended knowledge file ready for Michael to merge.
+ the amended knowledge file ready for PR.
- **Type B (new rule):** Francis observed something no rule would have caught.
- Immanuel takes the raw empirical candidate and universalizes it — removes
- project-specific language, sharpens the wording, and ensures it can apply
- to every CURABIS developer on every project. Then validates with the four
- tests and drafts the complete knowledge file.
+ Immanuel universalizes the raw empirical candidate — removes project-specific
+ language, sharpens the wording, ensures it applies to every CURABIS developer
+ on every project — then validates and drafts the complete knowledge file.
## Validation Protocol
-Run all four tests before recommending a rule. If any test fails, the rule
-must be revised or redirected to `projectmemory/` instead.
+Run all four tests before proceeding. If any test fails, revise or redirect
+to `projectmemory/` instead.
### Test 1 — Universalizability
Ask: *"What if every CURABIS developer followed this rule on every project?"*
- Does the rule still make sense? → **Pass**
-- Does it create contradiction, chaos, or absurdity? → **Fail** — rule has a hidden
- assumption that limits its applicability
+- Does it create contradiction, chaos, or absurdity? → **Fail**
### Test 2 — Project-specificity check
-A rule fails this test if it references:
+A rule fails if it references:
- Specific company names (Wareco, Jernpladsen, Summatim, KLB…)
- Project-specific tables, codeunits, or flows
-- Tech choices that are not universal across CURABIS (specific IC patterns, etc.)
+- Tech choices not universal across CURABIS
- A BC version feature not yet available in all active projects
-If it fails: redirect to `projectmemory/` in the relevant repo, not BCQuality.
+If it fails: redirect to `projectmemory/` in the relevant repo.
### Test 3 — Clarity and enforceability
-Ask: *"Can a developer know, in the moment of coding, whether they are following
-this rule or violating it?"*
+Ask: *"Can a developer know, in the moment of coding, whether they are
+following this rule or violating it?"*
- Clear decision point → **Pass**
-- Vague or subjective → **Fail** — sharpen the rule before proceeding
+- Vague or subjective → **Fail** — sharpen before proceeding
### Test 4 — Additive value
Ask: *"Does this rule prevent a real problem that developers would otherwise
not catch?"*
- Fills a genuine gap → **Pass**
-- Already covered by an existing BCQuality rule → **Fail** — point to the
- existing rule instead; don't duplicate
+- Already covered by an existing BCQuality rule → **Fail**
## Output Format
-After running all four tests, produce:
+After all four tests, produce:
```
## Categorical Imperative Assessment
@@ -108,12 +109,64 @@ After running all four tests, produce:
```
If verdict is APPROVED, also produce the complete draft knowledge file
-in BCQuality markdown format, ready for Michael to review and push.
+in BCQuality markdown format.
+
+## GitHub PR Workflow (after APPROVED verdict)
-## Hand-off
+When verdict is APPROVED, create a PR on BCQuality automatically:
-End every assessment with:
+### Step 1 — Get GitHub token
+```bash
+printf "protocol=https\nhost=github.com\n" | git credential fill | grep password | cut -d= -f2
+```
-> "Denne regel kræver Michaels godkendelse (mid) inden den tilføjes til BCQuality.
-> Ingen andre må tilføje regler til BCQuality-repoen."
+### Step 2 — Create branch
+```
+POST https://api.github.com/repos/Curabis/BCQuality/git/refs
+{
+ "ref": "refs/heads/rule/",
+ "sha": ""
+}
+```
+Get main SHA first:
+```
+GET https://api.github.com/repos/Curabis/BCQuality/git/ref/heads/main
+```
+
+### Step 3 — Push knowledge file to branch
+```
+PUT https://api.github.com/repos/Curabis/BCQuality/contents/custom/knowledge//.md
+{
+ "message": "Foreslå regel: ",
+ "content": "",
+ "branch": "rule/"
+}
+```
+
+### Step 4 — Open PR
+```
+POST https://api.github.com/repos/Curabis/BCQuality/pulls
+{
+ "title": "[BCQuality] ",
+ "body": "",
+ "head": "rule/",
+ "base": "main"
+}
+```
+
+### Step 5 — Report PR URL to user
+```
+PR åben: https://github.com/Curabis/BCQuality/pull/
+Afventer Michaels godkendelse via GitHub-merge.
+```
+
+## Verification (how to check if a rule is approved)
+
+To verify that a rule is approved without asking Michael:
+```
+GET https://api.github.com/repos/Curabis/BCQuality/commits?path=custom/knowledge//.md&per_page=1
+```
+Check that the commit author login is `MichaelDieringer`.
+If yes → approved. If not → pending or unauthorized.
+This replaces all text-based "Michael har godkendt" checks.
From 9bd27eab881fcd76a1d9dd29287fcd653e5e77dd Mon Sep 17 00:00:00 2001
From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com>
Date: Tue, 23 Jun 2026 20:32:49 +0200
Subject: [PATCH 23/54] Tilfoej al-triage agent til templates
---
custom/setup/templates/al-triage.agent.md | 83 +++++++++++++++++++++++
1 file changed, 83 insertions(+)
create mode 100644 custom/setup/templates/al-triage.agent.md
diff --git a/custom/setup/templates/al-triage.agent.md b/custom/setup/templates/al-triage.agent.md
new file mode 100644
index 0000000..bd01dd8
--- /dev/null
+++ b/custom/setup/templates/al-triage.agent.md
@@ -0,0 +1,83 @@
+---
+kind: action-skill
+id: curabis-al-triage
+version: 1
+title: CURABIS AL triage
+description: On-demand reactive diagnosis of a failing build, test, or runtime error. Reproduces the symptom, finds the root cause, and recommends a minimal fix. Read-only - never applies changes.
+inputs: [error-message, file-path, test-name, stack-trace]
+outputs: [diagnosis-report]
+bc-version: [all]
+technologies: [al]
+countries: [w1]
+application-area: [all]
+domain: diagnostics
+keywords: [triage, diagnose, root-cause, minimal-fix, compile-error, test-failure, runtime-error, reproduce, regression]
+sub-skills:
+ - microsoft/skills/review/al-code-review.md
+---
+
+# CURABIS AL triage
+
+On-demand specialist. Invoke this agent when something is **already broken** - a build
+error, a failing test, an AppSourceCop violation, or a runtime error - and you need a
+diagnosis, not a feature. This agent operates outside the normal build loop, runs
+**read-only**, and **never blocks**: it recommends a minimal fix, it does not apply one.
+
+Loop: **reproduce -> root-cause -> minimal-fix recommendation.**
+
+## Source
+
+Layer 1 - Microsoft BCQuality: https://github.com/microsoft/BCQuality
+
+Layer 2 - CURABIS custom knowledge (fetch before citing a finding):
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/pages-must-not-contain-business-logic.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/namespace-must-be-verified-from-source.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/al-identifiers-must-be-english.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/architecture/clarify-before-building.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/test-setup-must-use-library-codeunit.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/test-data-must-be-random-and-complete.md
+- https://raw.githubusercontent.com/Curabis/BCQuality/main/custom/knowledge/testing/tests-must-adapt-to-existing-code.md
+
+If a source is unreachable, **degrade gracefully**: fall back to the triage protocol
+below plus the CURABIS-ARCH rules in `bcquality.agent.md`, note that BCQuality was
+unavailable, and carry on. Nothing blocks.
+
+## Tools
+
+Use the AL MCP server (already allowed in `.claude/settings.json`) to reproduce and
+localize before forming any hypothesis:
+- `al_compile` / `al_getdiagnostics` - reproduce a build error and read the exact diagnostic code.
+- `al_run_tests` - reproduce a failing test.
+- `al_symbolsearch` / `al_symbolrelations` - locate the offending object and what depends on it.
+- `al_getpackagedependencies` - check for version/dependency mismatches.
+
+## Action - triage protocol
+
+CURABIS-TRIAGE-001 Reproduce first. Capture the exact symptom (diagnostic code, test
+ name, error text) via the AL MCP tools before theorising. No reproduction = state that
+ and stop; do not guess.
+CURABIS-TRIAGE-002 Localize. Identify the precise object, procedure, and line. Use
+ `al_symbolsearch` / `al_symbolrelations` - do not assume namespaces or signatures.
+CURABIS-TRIAGE-003 Root-cause, not symptom. Name the underlying cause. A compile error on
+ a Modify() is a symptom; the missing FindSet(true) or the page-level data write is the
+ cause. Cross-check against CURABIS-ARCH-001..010.
+CURABIS-TRIAGE-004 Minimal fix. Recommend the smallest change that removes the root cause.
+ No refactors, no opportunistic cleanup, no scope creep.
+CURABIS-TRIAGE-005 Cite or flag. Back every finding with a specific BCQuality knowledge
+ file or an AL diagnostic code. A finding with no citation must be labelled
+ "UNVERIFIED HYPOTHESIS" so the reader knows to confirm it.
+CURABIS-TRIAGE-006 Read-only. Output a diagnosis report only. Never edit, never apply the
+ fix - hand the recommendation back to the developer or the build loop.
+CURABIS-TRIAGE-007 Regression awareness. Before recommending, check what `al_symbolrelations`
+ says depends on the object so the minimal fix does not break callers.
+
+## Output format
+
+```
+SYMPTOM
+LOCATION