Add CMFRT naming conventions, object ID ranges, and patterns documentation

- Introduced guidelines for using the "CMFRT" prefix in object names, fields, and procedures to avoid naming collisions and ensure clarity.
- Established rules for object ID ranges to prevent conflicts with other extensions and maintain historical integrity.
- Documented best practices and anti-patterns for various coding patterns, including case statements, interface injection, label usage, and validation methods.
- Implemented a standards review skill to evaluate AL source changes against CMFRT company standards, ensuring compliance with naming, permissions, and architectural patterns.
This commit is contained in:
BeytullahCengiz88 2026-07-14 14:09:04 +02:00
parent d6ac005173
commit afb1fa2883
53 changed files with 1288 additions and 0 deletions

16
.altestrunner/config.json Normal file
View file

@ -0,0 +1,16 @@
{
"containerResultPath": "",
"launchConfigName": "",
"securePassword": "",
"userName": "",
"companyName": "",
"testSuiteName": "",
"vmUserName": "",
"vmSecurePassword": "",
"remoteContainerName": "",
"dockerHost": "",
"newPSSessionOptions": "",
"testRunnerServiceUrl": "",
"codeCoveragePath": ".altestrunner/codecoverage.json",
"culture": "en-US"
}

View file

@ -0,0 +1,15 @@
page 55035 "CMFRT AQ FS JJL API"
{
PageType = API;
SourceTable = "Job Journal Line"; // real table exposed directly to the API
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
begin
// Manual insert/exit boilerplate duplicates the framework insert
// and couples the HTTP request to the real-table insert: no retry,
// no error queue, one validation error fails the whole request.
Rec.Insert(true);
Rec.CMFRTAQFSLogIncomingRequest();
exit(false);
end;
}

View file

@ -0,0 +1,33 @@
page 55035 "CMFRT AQ FS JJL API"
{
PageType = API;
SourceTable = "CMFRT AQ FS JJL Buffer"; // buffer table, not Job Journal Line
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
begin
// Side effects only; framework performs the default insert.
Rec.CMFRTAQFSLogIncomingRequest();
end;
}
codeunit 55038 "CMFRT AQ FS JJL Proc"
{
procedure CMFRTAQProcessPendingEntries()
var
JJLBuffer: Record "CMFRT AQ FS JJL Buffer";
JJLCreator: Codeunit "CMFRT AQ FS JJL Creator";
begin
JJLBuffer.SetRange("CMFRT AQ Status", "CMFRT AQ Buffer Status"::"CMFRT AQ Pending");
if JJLBuffer.FindSet(true) then
repeat
// Proc owns the error boundary: one bad row does not abort the batch.
if Codeunit.Run(Codeunit::"CMFRT AQ FS JJL Creator", JJLBuffer) then
JJLBuffer."CMFRT AQ Status" := "CMFRT AQ Buffer Status"::"CMFRT AQ Processed"
else begin
JJLBuffer."CMFRT AQ Status" := "CMFRT AQ Buffer Status"::"CMFRT AQ Error";
JJLBuffer."CMFRT AQ Error Message" := CopyStr(GetLastErrorText(), 1, MaxStrLen(JJLBuffer."CMFRT AQ Error Message"));
end;
JJLBuffer.Modify(true);
until JJLBuffer.Next() = 0;
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: architecture
keywords: [api, buffer, staging, oninsertrecord, api-page, inbound, creator, proc, codeunit-run]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT inbound API pages write to buffer tables
## Description
Inbound CMFRT API pages never source from real application tables. Each API page sources from a dedicated buffer (staging) table owned by the extension. The buffer row records the raw inbound payload plus a status field (Pending/Processing/Processed/Error) and an operation enum where applicable. A Proc codeunit picks up pending buffer rows and owns the `Codeunit.Run` error boundary; a Creator codeunit transfers buffer values into the real table via `Validate` calls. The API page's `OnInsertRecord` trigger contains no manual insert plumbing — the framework performs the default insert; the trigger only performs side effects such as request logging.
## Best Practice
For each inbound API: one buffer table (with status and error-message fields), one API page sourced from the buffer, one Proc codeunit that iterates pending rows and calls the Creator inside a `Codeunit.Run` boundary so one failing row does not abort the batch, and one Creator codeunit that fills and inserts the real record. `OnInsertRecord` bodies contain only logging or metadata capture and no `exit` statement, so the framework insert proceeds.
See sample: `cmfrt-buffer-table-api-pattern.good.al`.
## Anti Pattern
An API page sourced directly from a real table (Sales Header, Job, Ship-to Address), or an `OnInsertRecord` trigger that calls `Rec.Insert(true)` followed by `exit(false)` to suppress the framework insert. Direct-to-real-table APIs make inbound failures atomic with the HTTP request (no retry, no error queue), and the manual insert/exit boilerplate duplicates framework behaviour while hiding the insert from other trigger logic.
See sample: `cmfrt-buffer-table-api-pattern.bad.al`.

View file

@ -0,0 +1,14 @@
// Subscriber calls the implementation codeunit DIRECTLY bypasses the base
// table's entry point and all OnBefore/OnAfter integration events.
codeunit 2045720 "CMFRT BA Sales Subscribers"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterPostSalesDoc', '', false, false)]
local procedure CMFRTBAOnAfterPostSalesDoc(var SalesHeader: Record "Sales Header")
var
SyncImpl: Codeunit "CMFRT BA SyncPostedDoc Impl";
begin
// Direct call to implementation codeunit OnBefore/OnAfter events on the
// base table never fire, dependent extensions cannot intercept this operation.
SyncImpl.CMFRTBASyncPostedSalesDoc(SalesHeader."No.");
end;
}

View file

@ -0,0 +1,37 @@
// Subscriber codeunit calls the BASE TABLE procedure not the implementation codeunit.
codeunit 2045720 "CMFRT BA Sales Subscribers"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterPostSalesDoc', '', false, false)]
local procedure CMFRTBAOnAfterPostSalesDoc(var SalesHeader: Record "Sales Header")
var
CMFRTBAItem: Record "CMFRT BA Item";
begin
// Route through the base table integration events fire normally.
CMFRTBAItem.CMFRTBASyncPostedSalesDoc(SalesHeader."No.");
end;
}
// Base table owns all entry points to implementation codeunits.
table 2045095 "CMFRT BA Item"
{
procedure CMFRTBASyncPostedSalesDoc(SalesDocNo: Code[20])
var
SyncImpl: Codeunit "CMFRT BA SyncPostedDoc Impl";
Handled: Boolean;
begin
OnBeforeCMFRTBASyncPostedSalesDoc(SalesDocNo, Handled);
if Handled then
exit;
CMFRTBASyncPostedSalesDoc(SyncImpl);
end;
procedure CMFRTBASyncPostedSalesDoc(SyncImpl: Interface "CMFRT BA ISyncPostedDoc")
begin
SyncImpl.CMFRTBASyncPostedSalesDoc(Rec);
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeCMFRTBASyncPostedSalesDoc(SalesDocNo: Code[20]; var Handled: Boolean)
begin
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: architecture
keywords: [codeunit, base-table, entry-point, coupling, architecture, cross-codeunit, subscriber]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT calls to implementation codeunits from base table only
## Description
In the CMFRT extension architecture, all calls to implementation codeunits must originate from the base-table object. No AL object other than the base table may call a CMFRT implementation codeunit directly. Event subscriber codeunits handle platform or application events and call the base table's entry-point procedures — not the implementation codeunit directly. Pages and reports call base-table procedures. This keeps the base table as the single integration point for all business-logic calls and ensures that `OnBefore`/`OnAfter` integration events fire consistently regardless of where a workflow begins.
## Best Practice
When a subscriber codeunit handles an event and needs to trigger a CMFRT operation, it calls the relevant base-table procedure. When a page action initiates business logic, it calls the base-table procedure. The implementation codeunit is an internal detail of the base table and should never appear in `using` clauses or variable declarations of pages, reports, or subscriber codeunits.
See sample: `cmfrt-calls-from-base-table-only.good.al`.
## Anti Pattern
Calling an implementation codeunit directly from a page, report, subscriber codeunit, or any object other than the base table. Direct calls bypass the base table's integration events, making the operation invisible to dependent extensions that subscribed to those events. This also creates hidden coupling between the caller and the implementation, which breaks when the implementation codeunit is renamed or replaced under the interface.
See sample: `cmfrt-calls-from-base-table-only.bad.al`.

View file

@ -0,0 +1,13 @@
// Two global procedures in one codeunit mixed concerns, impossible to
// apply interface injection independently to each operation.
codeunit 2045710 "CMFRT BA Price Utilities"
{
procedure CMFRTBACreateSalesPrice(ItemNo: Code[20]; UnitPrice: Decimal)
begin
end;
// Second global entry point belongs in its own codeunit with its own interface.
procedure CMFRTBADeleteExpiredPrices(ItemNo: Code[20]; CutoffDate: Date)
begin
end;
}

View file

@ -0,0 +1,22 @@
// One codeunit one global entry point one interface.
interface "CMFRT BA ICreateSalesPrice"
{
procedure CMFRTBACreateSalesPrice(ItemNo: Code[20]; UnitPrice: Decimal);
}
codeunit 2045710 "CMFRT BA CreateSalesPrice Impl" implements "CMFRT BA ICreateSalesPrice"
{
procedure CMFRTBACreateSalesPrice(ItemNo: Code[20]; UnitPrice: Decimal)
begin
CMFRTBAValidateItem(ItemNo);
CMFRTBAWriteSalesPrice(ItemNo, UnitPrice);
end;
local procedure CMFRTBAValidateItem(ItemNo: Code[20])
begin
end;
local procedure CMFRTBAWriteSalesPrice(ItemNo: Code[20]; UnitPrice: Decimal)
begin
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: architecture
keywords: [codeunit, single-responsibility, entry-point, interface, architecture, coupling]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT one codeunit one global function
## Description
In the CMFRT extension architecture, each implementation codeunit exposes exactly one global procedure. That procedure is the codeunit's entry point and corresponds directly to the single interface the codeunit implements. Helper logic is placed in local procedures within the same codeunit and is never exposed as additional global procedures. This rule enforces single responsibility at the AL codeunit boundary: one codeunit, one concern, one interface, one entry point.
## Best Practice
Create one implementation codeunit per functional concern. If a codeunit accumulates a second global procedure that has a distinct concern, extract that procedure into its own codeunit with its own interface. Keep local procedures `local` so callers outside the codeunit cannot bypass the interface contract.
See sample: `cmfrt-one-codeunit-one-function.good.al`.
## Anti Pattern
Placing multiple global procedures in a single implementation codeunit. A multi-entry-point codeunit mixes concerns, prevents the interface injection pattern from being applied independently to each concern, and grows into a difficult-to-test utility class. Callers that skip the base-table entry point and call implementation procedures directly bypass integration events and violate the architecture.
See sample: `cmfrt-one-codeunit-one-function.bad.al`.

View file

@ -0,0 +1,9 @@
codeunit 2045700 "CMFRT TST Calculator"
{
// Original signature modified in place every caller must update simultaneously.
// Dependent extensions that were not recompiled fail at runtime.
procedure CMFRTTSTCalculateSum(NumberOne: Decimal; NumberTwo: Decimal; Decimals: Integer): Decimal
begin
exit(Round(NumberOne + NumberTwo, Power(10, -Decimals)));
end;
}

View file

@ -0,0 +1,15 @@
codeunit 2045700 "CMFRT TST Calculator"
{
// Original signature kept unchanged and marked obsolete once callers migrate.
[Obsolete('Use CMFRTTSTCalculateSumWithRounding instead.', 'Task-23859')]
procedure CMFRTTSTCalculateSum(NumberOne: Decimal; NumberTwo: Decimal): Decimal
begin
exit(CMFRTTSTCalculateSumWithRounding(NumberOne, NumberTwo, 2));
end;
// New overload with extra parameter callers can adopt at their own pace.
procedure CMFRTTSTCalculateSumWithRounding(NumberOne: Decimal; NumberTwo: Decimal; Decimals: Integer): Decimal
begin
exit(Round(NumberOne + NumberTwo, Power(10, -Decimals)));
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: breaking-changes
keywords: [overload, parameter, signature, procedure, breaking-change, backward-compatibility, arity]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT add parameter via overload
## Description
When a CMFRT extension procedure requires an additional parameter, the original procedure signature must not be changed. A second procedure with the same name and an extended parameter list is added alongside the original. AL resolves same-name procedures by parameter count at the call site, so both coexist without conflict. The original signature is retained indefinitely and is only marked obsolete after all known callers have migrated to the new overload.
## Best Practice
Keep the existing procedure unchanged and add a new procedure of the same name with the extra parameter appended. The original procedure may delegate to the new overload with a sensible default value for the added parameter, or it may keep its own implementation when the semantics differ. Both forms are valid. The `ObsoleteState = Pending` marker on the original should only be added once all callers have been confirmed to use the new overload.
See sample: `cmfrt-add-parameter-via-overload.good.al`.
## Anti Pattern
Adding a parameter to an existing procedure's parameter list in place, even when the intention is to add a trailing parameter that callers can ignore. AL does not support optional parameters or default values for procedure arguments. Any in-place signature change is a breaking change: every caller must be updated simultaneously and any dependent extension that was not recompiled fails at runtime.
See sample: `cmfrt-add-parameter-via-overload.bad.al`.

View file

@ -0,0 +1,15 @@
// Field deleted outright any upgrade codeunit or dependent extension
// that referenced "CMFRT GD Error Path" will fail to compile.
table 2045085 "CMFRT GD Setup"
{
fields
{
field(1; "Primary Key"; Code[10]) { DataClassification = SystemMetadata; }
field(2; "CMFRT GD ErrorPath"; Text[500])
{
Caption = 'Error Path';
DataClassification = CustomerContent;
}
// "CMFRT GD Error Path" (field 2045325) was deleted here breaking change.
}
}

View file

@ -0,0 +1,23 @@
// Obsoleted field kept at the end of the table never deleted.
table 2045085 "CMFRT GD Setup"
{
fields
{
field(1; "Primary Key"; Code[10]) { DataClassification = SystemMetadata; }
field(2; "CMFRT GD ErrorPath"; Text[500])
{
Caption = 'Error Path';
DataClassification = CustomerContent;
}
// Obsolete section at the end original field retained with state = Removed.
field(2045325; "CMFRT GD Error Path"; Text[200])
{
Caption = 'Error Path (Obsolete)';
DataClassification = CustomerContent;
ObsoleteState = Removed;
ObsoleteReason = 'Replaced by field "CMFRT GD ErrorPath" with extended length.';
ObsoleteTag = 'Task-29100';
}
}
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: breaking-changes
keywords: [obsolete, delete, remove, breaking-change, backward-compatibility, obsolete-state, obsolete-reason]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT never delete — always obsolete
## Description
In a CMFRT extension the following AL members must never be physically deleted: global procedures, table fields, page fields, enum values, and entire objects. Removing any of these breaks dependent extensions and upgrade paths without a compiler warning. The required approach is to retain the member, mark it with `ObsoleteState = Pending` when deprecation begins, and promote to `ObsoleteState = Removed` in a subsequent release after dependents have migrated. All obsoleted members are placed at the end of their containing object so that active code is never mixed with retired code.
## Best Practice
When a member is no longer needed, keep it in place, add `ObsoleteState = Pending`, `ObsoleteReason = '<explanation>'`, and `ObsoleteTag = '<task-id>'`. In a later release, promote to `ObsoleteState = Removed`. For fields, add the replacement field first, then obsolete the original. For procedures, add the replacement first, then obsolete the original. Provide an upgrade codeunit procedure whenever a field rename or type change requires data migration.
See sample: `cmfrt-never-delete-always-obsolete.good.al`.
## Anti Pattern
Deleting a global procedure, table field, page field, enum value, or entire AL object from the extension source. Physical deletion produces compiler errors in every dependent extension that referenced the removed member, and for table fields it causes data loss and upgrade failures in existing customer databases.
See sample: `cmfrt-never-delete-always-obsolete.bad.al`.

View file

@ -0,0 +1,34 @@
codeunit 55043 "CMFRT AQ FS ProForma Meth"
{
procedure CMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line")
var
Item: Record Item;
GeneralPostingSetup: Record "General Posting Setup";
IsHandled: Boolean;
begin
OnBeforeCMFRTAQFSResolveItemToGLAcc(SalesLine, IsHandled);
if IsHandled then
exit;
// Business logic inline in the shell: this early exit
// silently skips the OnAfter event below.
if SalesLine."CMFRT AQ FS Item No." = '' then
exit;
if not Item.Get(SalesLine."CMFRT AQ FS Item No.") then
exit;
// ... more inline logic ...
OnAfterCMFRTAQFSResolveItemToGLAcc(SalesLine);
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeCMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line"; var IsHandled: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnAfterCMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line")
begin
end;
}

View file

@ -0,0 +1,38 @@
codeunit 55043 "CMFRT AQ FS ProForma Meth"
{
procedure CMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line")
var
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeCMFRTAQFSResolveItemToGLAcc(SalesLine, IsHandled);
if IsHandled then
exit;
DoCMFRTAQFSResolveItemToGLAcc(SalesLine);
OnAfterCMFRTAQFSResolveItemToGLAcc(SalesLine);
end;
local procedure DoCMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line")
var
Item: Record Item;
GeneralPostingSetup: Record "General Posting Setup";
ItemNotFoundErr: Label 'Item %1 was not found.', Comment = '%1 = Item No.';
begin
Item.SetLoadFields("No.", "Gen. Prod. Posting Group", Description);
if not Item.Get(SalesLine."CMFRT AQ FS Item No.") then
Error(ItemNotFoundErr, SalesLine."CMFRT AQ FS Item No.");
// ... business logic only; early exits here cannot skip OnAfter ...
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeCMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line"; var IsHandled: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnAfterCMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line")
begin
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: events
keywords: [on-before, on-after, do-procedure, thin-shell, meth, is-handled, extraction, entry-point]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT OnBefore → Do → OnAfter procedure shape
## Description
Global procedures in CMFRT Meth codeunits are thin shells: the body consists of firing `OnBefore<Name>` with `var IsHandled: Boolean`, exiting if handled, calling a local `Do<Name>` procedure that holds all business logic, and firing `OnAfter<Name>`. Local variables that only serve the business logic (record buffers, labels, working values) live in the `Do` procedure, not in the shell. This keeps the event bracket structurally impossible to bypass — the `OnAfter` event cannot be skipped by an early `exit` inside business logic, because business logic lives one level down.
## Best Practice
Write every global Meth procedure as: reset `IsHandled`, fire `OnBefore`, `if IsHandled then exit;`, call `Do<Name>(...)`, fire `OnAfter`. When review finds a global procedure whose body mixes event calls with business logic, extract the logic into `Do<Name>` and move its private variables and labels along with it.
See sample: `cmfrt-onbefore-do-onafter.good.al`.
## Anti Pattern
A global procedure whose business logic sits inline between the `OnBefore` and `OnAfter` calls. Inline bodies grow early `exit` paths that silently skip the `OnAfter` event, and their local variables and labels accumulate at the shell level where every branch can touch them. Equally wrong: declaring the event pair but never calling the events from the procedure (dead events), which advertises an extension point that never fires.
See sample: `cmfrt-onbefore-do-onafter.bad.al`.

View file

@ -0,0 +1,15 @@
// No integration events dependent extensions cannot intercept or react
// to the operation without using an AL override, which is a breaking pattern.
codeunit 2045700 "CMFRT BA Item Price Impl"
{
procedure CMFRTBAUpdateItemPrice(ItemNo: Code[20]; UnitPrice: Decimal)
var
Item: Record Item;
begin
Item.SetLoadFields("Unit Price");
if Item.Get(ItemNo) then begin
Item.Validate("Unit Price", UnitPrice);
Item.Modify();
end;
end;
}

View file

@ -0,0 +1,30 @@
codeunit 2045700 "CMFRT BA Item Price Impl"
{
procedure CMFRTBAUpdateItemPrice(ItemNo: Code[20]; UnitPrice: Decimal)
var
Item: Record Item;
Handled: Boolean;
begin
OnBeforeCMFRTBAUpdateItemPrice(ItemNo, UnitPrice, Handled);
if Handled then
exit;
Item.SetLoadFields("Unit Price");
if Item.Get(ItemNo) then begin
Item.Validate("Unit Price", UnitPrice);
Item.Modify();
end;
OnAfterCMFRTBAUpdateItemPrice(ItemNo, UnitPrice);
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeCMFRTBAUpdateItemPrice(ItemNo: Code[20]; UnitPrice: Decimal; var Handled: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnAfterCMFRTBAUpdateItemPrice(ItemNo: Code[20]; UnitPrice: Decimal)
begin
end;
}

View file

@ -0,0 +1,28 @@
---
bc-version: [all]
domain: events
keywords: [integration-event, on-before, on-after, extensibility, event-publisher, global-procedure, handled]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT OnBefore and OnAfter for every global procedure
## Description
Every global procedure in a CMFRT extension must be bracketed by a paired `OnBefore<ProcedureName>` and `OnAfter<ProcedureName>` integration event declared in the same object. Both events are `[IntegrationEvent(false, false)]` local procedures. The `OnBefore` event passes the procedure's key input variables and a `var Handled: Boolean` parameter. The entry procedure checks `Handled` on return from `OnBefore` and exits without executing its body if a subscriber has already handled the operation. The `OnAfter` event passes the key output variables so subscribers can react to the completed result.
## Best Practice
Declare both events at the time the global procedure is written, not as a later addition. Place the `OnBefore` call at the top of the procedure body before any logic, and the `OnAfter` call at the bottom after the last statement. Follow the naming convention `OnBefore<ProcedureName>` and `OnAfter<ProcedureName>` exactly so event consumers can locate publishers by convention.
See sample: `cmfrt-onbefore-onafter-all-globals.good.al`.
## Anti Pattern
Publishing a global procedure without both `OnBefore` and `OnAfter` integration events, or adding events only after a downstream extension explicitly requests an extension point. A global procedure with no event bracket is a black box: dependent extensions cannot inject logic around it without an AL override, which is a breaking pattern. Adding events later is itself a non-breaking change but causes unnecessary churn and review cycles.
The inverse is equally a violation: event declarations that nothing raises. An `[IntegrationEvent]` declared but never called from any procedure, or a wrapper procedure (for example a `[TryFunction]` insert wrapper) that no caller invokes, is dead code that advertises an extension point which never fires. Wire the event into the owning procedure or delete the declaration and its plumbing.
See sample: `cmfrt-onbefore-onafter-all-globals.bad.al`.

View file

@ -0,0 +1,33 @@
table 55008 "CMFRT AQ Setup"
{
fields
{
field(55011; "CMFRT AQ FS Jnl. Template Name"; Code[10])
{
Caption = 'FS Journal Template Name'; // missing CMFRT AQ prefix
DataClassification = CustomerContent;
}
}
}
page 55020 "CMFRT AQ Setup"
{
layout
{
area(Content)
{
field("CMFRT AQ FS Jnl. Template Name"; Rec."CMFRT AQ FS Jnl. Template Name")
{
ApplicationArea = All;
// Page-level overrides duplicate the table definition and drift apart.
Caption = 'FS Journal Template Name';
ToolTip = 'Specifies the Job Journal Template used for material lines created by the Field Service API.';
}
}
}
}
enum 55040 "CMFRT AQ Buffer Status"
{
value(55000; "CMFRT AQ Pending") { Caption = 'Pending'; } // missing prefix
}

View file

@ -0,0 +1,32 @@
table 55008 "CMFRT AQ Setup"
{
fields
{
field(55011; "CMFRT AQ FS Jnl. Template Name"; Code[10])
{
Caption = 'CMFRT AQ FS Journal Template Name';
ToolTip = 'Specifies the Job Journal Template used for material lines created by the Field Service API.';
DataClassification = CustomerContent;
}
}
}
page 55020 "CMFRT AQ Setup"
{
layout
{
area(Content)
{
field("CMFRT AQ FS Jnl. Template Name"; Rec."CMFRT AQ FS Jnl. Template Name")
{
ApplicationArea = All; // Caption and ToolTip inherited from the table field
}
}
}
}
enum 55040 "CMFRT AQ Buffer Status"
{
value(55000; "CMFRT AQ Pending") { Caption = 'CMFRT AQ Pending'; }
value(55001; "CMFRT AQ Processed") { Caption = 'CMFRT AQ Processed'; }
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: naming
keywords: [caption, prefix, enum-value, field-caption, tooltip, table-level, page-override, translation]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT caption prefix and table-level captions
## Description
Captions in a CMFRT extension follow the same prefix rule as names: every field caption and every enum value caption carries the `CMFRT <ABBR>` prefix — for example `Caption = 'CMFRT AQ FS Journal Template Name'` on a field and `Caption = 'CMFRT AQ Pending'` on an enum value. Captions and tooltips are defined once, on the table field (or enum value), never overridden on pages. Pages inherit the table-level `Caption` and `ToolTip`, so the text is maintained in one place and every page showing the field stays consistent.
## Best Practice
Set `Caption` with the full `CMFRT <ABBR>` prefix and `ToolTip` on the table field definition. On pages, declare only `ApplicationArea` for the field — no `Caption`, no `ToolTip`. Give every enum value a prefixed caption matching its prefixed value name.
See sample: `cmfrt-caption-prefix.good.al`.
## Anti Pattern
An unprefixed caption (`Caption = 'Pending'`, `Caption = 'FS Journal Template Name'`), or a page field that repeats or overrides the table-level `Caption`/`ToolTip`. Unprefixed captions are indistinguishable from base-application text for users and translators, and duplicated page-level text drifts from the table definition the first time either copy is edited.
See sample: `cmfrt-caption-prefix.bad.al`.

View file

@ -0,0 +1,29 @@
// Object names without the CMFRT prefix collide with other extensions.
pageextension 2045661 "Job Card Extension" extends "Job Card"
{
}
// Unprefixed fields are indistinguishable from base application fields.
tableextension 2045660 "Job Extension" extends Job
{
fields
{
field(2045081; "POI ID"; Guid)
{
DataClassification = CustomerContent;
Caption = 'POI ID';
}
}
}
// Unprefixed procedures have no ownership signal for reviewers.
codeunit 2045700 "Job Management"
{
procedure LinkJobToPOI(JobNo: Code[20]; POIId: Guid)
begin
end;
local procedure ValidatePOIExists(POIId: Guid): Boolean
begin
end;
}

View file

@ -0,0 +1,29 @@
// Objects use "CMFRT <ABBR> <Name>" with spaces.
pageextension 2045661 "CMFRT GD Job" extends "Job Card"
{
}
// Fields use "CMFRT <ABBR> <FieldName>" with spaces.
tableextension 2045660 "CMFRT GD Job Ext" extends Job
{
fields
{
field(2045081; "CMFRT GD POI ID"; Guid)
{
DataClassification = CustomerContent;
Caption = 'POI ID';
}
}
}
// Procedures use CMFRT<ABBR><ProcedureName> concatenated, no spaces.
codeunit 2045700 "CMFRT GD Job Mgmt"
{
procedure CMFRTGDLinkJobToPOI(JobNo: Code[20]; POIId: Guid)
begin
end;
local procedure CMFRTGDValidatePOIExists(POIId: Guid): Boolean
begin
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: naming
keywords: [naming, prefix, cmfrt, object-name, procedure-name, field-name, abbreviation]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT naming prefix
## Description
Every object, field, and procedure in a CMFRT extension must carry the `CMFRT XXX` prefix, where `XXX` is the product or customer abbreviation (for example `GD` for Geodynamics, `BA` for Batch Automation, `JO` for Jobs). The prefix applies to tables, table extensions, pages, page extensions, codeunits, interfaces, reports, enum values, and to all global and local procedures. Object and field names use the three-part form `CMFRT <ABBR> <Descriptive Name>` with spaces. Procedure names use the concatenated form `CMFRT<ABBR><ProcedureName>` with no spaces.
## Best Practice
Name every object `"CMFRT <ABBR> <Name>"` — for example `"CMFRT GD Job"` or `"CMFRT GD POI"`. Name every field `"CMFRT <ABBR> <FieldName>"` — for example `"CMFRT GD POI ID"`. Name every procedure `CMFRT<ABBR><ProcedureName>` — for example `CMFRTGDCalculatePOIDistance`. The procedure name must be self-describing: a reader must understand what the procedure does without reading its body. Local procedures follow the same rule.
See sample: `cmfrt-naming-prefix.good.al`.
## Anti Pattern
Naming objects, fields, or procedures without the CMFRT prefix — for example `procedure CalculateDiscount()` or `field(50000; "Amount"; Decimal)`. Unprefixed members collide with base application fields, break the reviewer's ability to identify extension-owned members, and violate the astena naming convention enforced across all CMFRT extensions.
See sample: `cmfrt-naming-prefix.bad.al`.

View file

@ -0,0 +1,25 @@
// ID 50000 is outside both defined CMFRT ranges and will conflict with
// other extensions that follow the standard AppSource free range.
table 50000 "CMFRT GD POI"
{
Caption = 'POI';
DataClassification = CustomerContent;
fields
{
field(1; "Code"; Code[20]) { DataClassification = CustomerContent; }
}
}
// ID 2045081 was previously assigned to a removed object.
// Reusing it causes silent conflicts with historical telemetry and upgrade codeunits.
table 2045081 "CMFRT GD New Feature"
{
Caption = 'New Feature';
DataClassification = CustomerContent;
fields
{
field(1; "Code"; Code[20]) { DataClassification = CustomerContent; }
}
}

View file

@ -0,0 +1,24 @@
// Product extension IDs are within the product range 2045081..2046580.
table 2045081 "CMFRT GD POI"
{
Caption = 'POI';
DataClassification = CustomerContent;
fields
{
field(2045081; "Code"; Code[20]) { DataClassification = CustomerContent; }
field(2045082; "Description"; Text[100]) { DataClassification = CustomerContent; }
}
}
// Customer extension IDs are within the customer range 55000..55999.
table 55000 "CMFRT JO Customer Site"
{
Caption = 'Customer Site';
DataClassification = CustomerContent;
fields
{
field(55000; "Code"; Code[20]) { DataClassification = CustomerContent; }
}
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: naming
keywords: [object-id, id-range, numbering, product-extension, customer-extension, range, field-id, enum-value-id]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT object ID ranges
## Description
CMFRT object IDs are partitioned into two non-overlapping numeric ranges by deployment scope. Product extensions — features delivered to all customers under the CMFRT product — use IDs from `2045081` to `2046580`. Customer-specific extensions — functionality tailored to a single customer deployment — use IDs from `55000` to `55999`. The range applies to member IDs as well as object IDs: table fields and enum values declared by the extension take IDs from the same licensed range (for example `field(55011; ...)`, `value(55000; ...)`), even on tables the extension owns. Always pick the next free ID in the correct range. A removed object's ID must never be recycled; the platform retains historical references to deleted object IDs and recycling causes silent conflicts with upgrade and telemetry systems.
## Best Practice
Before adding any AL object, identify whether the feature is product-wide or customer-specific, look up the highest currently allocated ID in the correct range across the extension's source, and assign the next sequential ID. Record ID allocations in the pull request description so reviewers can confirm the range and sequence without scanning all object files.
See sample: `cmfrt-object-id-ranges.good.al`.
## Anti Pattern
Assigning an ID outside both ranges, choosing a round-number ID that has no relation to the next free slot, reusing an ID from a previously removed object, or numbering table fields or enum values outside the licensed range (for example `field(10; ...)` on a customer-range table). ID conflicts between extensions produce runtime application errors that are difficult to reproduce and trace, because the conflict may only manifest when both extensions are installed in the same environment.
See sample: `cmfrt-object-id-ranges.bad.al`.

View file

@ -0,0 +1,16 @@
codeunit 2045760 "CMFRT GD Status Processor"
{
procedure CMFRTGDProcessByStatus(Status: Enum "CMFRT GD POI Status")
begin
// No else clause a future enum value silently falls through
// with no error and no action, producing a silent no-op.
case Status of
Status::Active:
CMFRTGDActivatePOI();
Status::Inactive:
CMFRTGDDeactivatePOI();
Status::Pending:
CMFRTGDQueuePOIForReview();
end;
end;
}

View file

@ -0,0 +1,18 @@
codeunit 2045760 "CMFRT GD Status Processor"
{
procedure CMFRTGDProcessByStatus(Status: Enum "CMFRT GD POI Status")
begin
case Status of
Status::Active:
CMFRTGDActivatePOI();
Status::Inactive:
CMFRTGDDeactivatePOI();
Status::Pending:
CMFRTGDQueuePOIForReview();
else
// New enum values added in the future are caught here
// instead of silently doing nothing.
Error('Unhandled POI status: %1', Status);
end;
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: patterns
keywords: [case, else, defensive-coding, unhandled-case, enum, option, silent-failure]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT CASE statement requires ELSE clause
## Description
Every `case` statement in CMFRT AL code must include an `else` clause. The rule is unconditional: even when every currently known value of the matched expression is enumerated, the `else` clause guards against future values being added to an enum, option field, or integer range. Without `else`, a new value passes through the `case` block silently — no error, no action — and the resulting silent no-op or data corruption is difficult to trace because the `case` appears correct at the time it was written.
## Best Practice
Always add an `else` clause to every `case` statement. When no meaningful action applies to unexpected values, the `else` clause should raise an error that identifies the unexpected value, log it, or call a dedicated handler procedure. The key outcome is that the unexpected case is detected at runtime rather than silently ignored.
See sample: `cmfrt-case-requires-else.good.al`.
## Anti Pattern
Writing a `case` statement that enumerates all currently known values and omits `else`. The code appears complete but becomes a silent failure mode the moment a new enum value is added by a future developer or by a base application update, because the added value simply falls through the entire `case` block.
See sample: `cmfrt-case-requires-else.bad.al`.

View file

@ -0,0 +1,12 @@
// Monolithic entry-point: the implementation is embedded, there is no interface,
// and there is no OnBefore event. Dependent extensions cannot override the
// calculation without an AL override hack.
table 2045090 "CMFRT MS Measure State"
{
procedure CalcTotals()
begin
// All calculation logic is hard-coded here.
Rec."Total Amount" := Rec."Line Amount" + Rec."Tax Amount";
Rec.Modify();
end;
}

View file

@ -0,0 +1,31 @@
// Step 1: Interface with a single procedure.
interface "CMFRT MS ICalcTotals"
{
procedure CMFRTMSCalcTotals(var MeasureState: Record "CMFRT MS Measure State");
}
// Step 2 & 3: Base-table entry point and interface-accepting overload.
table 2045090 "CMFRT MS Measure State"
{
procedure CalcTotals()
var
DefaultImpl: Codeunit "CMFRT MS CalcTotals Impl";
Handled: Boolean;
begin
OnBeforeDefaultImplCalcTotals(Rec, Handled);
if Handled then
exit;
CalcTotals(DefaultImpl);
end;
procedure CalcTotals(CalcImpl: Interface "CMFRT MS ICalcTotals")
begin
CalcImpl.CMFRTMSCalcTotals(Rec);
end;
// Step 4: OnBefore event with Handled pattern.
[IntegrationEvent(false, false)]
local procedure OnBeforeDefaultImplCalcTotals(var Ms: Record "CMFRT MS Measure State"; var Handled: Boolean)
begin
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: patterns
keywords: [interface, injection, extensibility, implementation, handled, on-before, pluggable, override]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT interface injection pattern
## Description
The CMFRT interface injection pattern makes any table-level operation pluggable by dependent extensions using a four-step structure. Step 1: define an AL `interface` with a single procedure matching the operation's signature. Step 2: add a parameterless entry-point procedure on the base table that instantiates the default implementation codeunit, fires an `OnBefore` integration event with `var Handled: Boolean`, exits early if `Handled` is true, and otherwise calls the overloaded form in Step 3. Step 3: add an overloaded procedure on the same table that accepts the interface as a parameter and delegates to it. Step 4: declare the `[IntegrationEvent(false, false)]` `OnBefore` event as a local procedure passing `var Handled: Boolean`. Dependent extensions subscribe to `OnBefore`, set `Handled := true`, and call the overloaded procedure with their own implementation codeunit.
## Best Practice
Apply this pattern whenever a calculation or operation might need different behaviour in different customer deployments. The separation between the parameterless entry-point and the interface-accepting overload means a subscriber can inject an alternative implementation without modifying the base table. The default implementation remains the fallback for all extensions that do not subscribe.
See sample: `cmfrt-interface-injection.good.al`.
## Anti Pattern
Placing the entire implementation directly inside the entry-point procedure with no interface and no `OnBefore` event. Monolithic entry points cannot be overridden by a subscriber without an AL override pattern, which is a breaking change for the overriding extension every time the base procedure is updated.
See sample: `cmfrt-interface-injection.bad.al`.

View file

@ -0,0 +1,18 @@
codeunit 55026 "CMFRT AQ ServiceWarehouseMeth"
{
var
// Global labels: shared between unrelated procedures, outlive their
// callers as dead text, and %1/%2 are undocumented for translators.
CreateTransferOrdersQst: Label 'Create 2 transfer orders for job %1?';
TransferOrdersCreatedMsg: Label 'Transfer orders %1 and %2 created.';
local procedure DoCMFRTAQMakeTransferOrders(var Job: Record Job)
var
ConfirmMgt: Codeunit "Confirm Management";
begin
if not ConfirmMgt.GetResponseOrDefault(StrSubstNo(CreateTransferOrdersQst, Job."No."), true) then
exit;
// ... create orders ...
Message(TransferOrdersCreatedMsg, 'T-001', 'T-002');
end;
}

View file

@ -0,0 +1,14 @@
codeunit 55026 "CMFRT AQ ServiceWarehouseMeth"
{
local procedure DoCMFRTAQMakeTransferOrders(var Job: Record Job)
var
ConfirmMgt: Codeunit "Confirm Management";
CreateTransferOrdersQst: Label 'Create 2 transfer orders for job %1?', Comment = '%1 = Job No.';
TransferOrdersCreatedMsg: Label 'Transfer orders %1 and %2 created.', Comment = '%1 = Transfer Order No. 1, %2 = Transfer Order No. 2';
begin
if not ConfirmMgt.GetResponseOrDefault(StrSubstNo(CreateTransferOrdersQst, Job."No."), true) then
exit;
// ... create orders ...
Message(TransferOrdersCreatedMsg, 'T-001', 'T-002');
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: patterns
keywords: [label, local-scope, global-var, comment-attribute, placeholder, translation, strsubstno]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT Labels are local with documented placeholders
## Description
`Label` variables in CMFRT code are declared in the `var` section of the one procedure that uses them, not in the codeunit's global `var` section. Every label whose text contains `%1`-style placeholders carries a `Comment` attribute documenting each placeholder (`Comment = '%1 = Job No.'`). Global label sections accumulate text that no procedure references anymore, and undocumented placeholders leave translators guessing what will be substituted.
## Best Practice
Declare each label local to the procedure (usually the `Do<Name>` procedure) that passes it to `Error`, `Message`, `Confirm Management`, or `StrSubstNo`. Add `Comment` naming every placeholder. When review moves logic into a `Do` procedure, move its labels with it.
See sample: `cmfrt-labels-local-scope.good.al`.
## Anti Pattern
Labels declared in the codeunit-level `var` section, or placeholder labels without a `Comment` attribute. Global labels outlive their callers as dead text, are shared between unrelated procedures, and their missing placeholder documentation produces mistranslations that only surface in localized builds.
See sample: `cmfrt-labels-local-scope.bad.al`.

View file

@ -0,0 +1,9 @@
codeunit 55028 "CMFRT AQ FS Item Attr Push"
{
local procedure DoCMFRTAQInitAPILogEntry(var CMFRTAQAPILog: Record "CMFRT AQ API Log"; ErrorText: Text)
begin
// Literal lengths diverge from the field definition on the first schema change.
CMFRTAQAPILog."CMFRT AQ User ID" := CopyStr(UserId(), 1, 50);
CMFRTAQAPILog."CMFRT AQ Error Message" := CopyStr(ErrorText, 1, 250);
end;
}

View file

@ -0,0 +1,8 @@
codeunit 55028 "CMFRT AQ FS Item Attr Push"
{
local procedure DoCMFRTAQInitAPILogEntry(var CMFRTAQAPILog: Record "CMFRT AQ API Log"; ErrorText: Text)
begin
CMFRTAQAPILog."CMFRT AQ User ID" := CopyStr(UserId(), 1, MaxStrLen(CMFRTAQAPILog."CMFRT AQ User ID"));
CMFRTAQAPILog."CMFRT AQ Error Message" := CopyStr(ErrorText, 1, MaxStrLen(CMFRTAQAPILog."CMFRT AQ Error Message"));
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: patterns
keywords: [maxstrlen, copystr, truncation, magic-number, field-length, overflow, string]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT use MaxStrLen in CopyStr, never a literal length
## Description
When CMFRT code truncates a text value to fit a field, the length argument of `CopyStr` must be `MaxStrLen(TargetRecord.TargetField)`, never a numeric literal. The field is the single source of truth for its own length; a hardcoded number silently diverges the moment the field is widened or the code is copied to a field of a different size, producing either needless truncation or a runtime overflow error.
## Best Practice
Write `Rec."CMFRT AQ User ID" := CopyStr(UserId(), 1, MaxStrLen(Rec."CMFRT AQ User ID"));`. The expression stays correct through any future field-length change and documents intent: truncate to whatever fits the target.
See sample: `cmfrt-maxstrlen-copystr.good.al`.
## Anti Pattern
`CopyStr(UserId(), 1, 50)` — the literal encodes the field length at the time of writing. Widening the field leaves data truncated at the stale length; narrowing it turns the assignment into a runtime "length exceeds" error that only fires on long values in production.
See sample: `cmfrt-maxstrlen-copystr.bad.al`.

View file

@ -0,0 +1,14 @@
codeunit 2045750 "CMFRT GD Deletion Handler"
{
procedure CMFRTGDDeletePOI(POICode: Code[20])
var
CMFRTGDPOI: Record "CMFRT GD POI";
begin
// Confirm built-in blocks test automation automated tests hang here.
if not Confirm('Delete POI %1?', false, POICode) then
exit;
if CMFRTGDPOI.Get(POICode) then
CMFRTGDPOI.Delete(true);
end;
}

View file

@ -0,0 +1,15 @@
codeunit 2045750 "CMFRT GD Deletion Handler"
{
procedure CMFRTGDDeletePOI(POICode: Code[20])
var
CMFRTGDPOI: Record "CMFRT GD POI";
ConfirmManagement: Codeunit "Confirm Management";
DeleteConfirmQst: Label 'Delete POI %1?', Comment = '%1 = POI Code';
begin
if not ConfirmManagement.GetResponseOrDefault(StrSubstNo(DeleteConfirmQst, POICode), false) then
exit;
if CMFRTGDPOI.Get(POICode) then
CMFRTGDPOI.Delete(true);
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: patterns
keywords: [confirm, confirmmanagement, dialog, user-confirmation, test, automation, testability]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT use ConfirmManagement instead of Confirm
## Description
CMFRT AL code must use the `"Confirm Management"` codeunit to prompt the user for confirmation instead of calling the `Confirm` built-in function directly. `"Confirm Management"` wraps `Confirm` and resolves confirmation dialogs without displaying UI during automated test runs, making test behaviour deterministic. The `Confirm` built-in always renders a modal dialog regardless of execution context, which causes automated tests to hang waiting for user input.
## Best Practice
Declare `ConfirmManagement: Codeunit "Confirm Management";` as a local variable and call `ConfirmManagement.GetResponseOrDefault(QuestionLbl, true)`. The codeunit suppresses the dialog automatically in test context. Use a `Label` for the question text so it is translatable.
See sample: `cmfrt-use-confirmmanagement.good.al`.
## Anti Pattern
Calling `if Confirm(QuestionText, true) then` directly. Direct `Confirm` calls are not interceptable by the test framework: the modal dialog appears during automated test runs, causing the test to stall indefinitely or fail with a UI-interaction error. Tests that hit a `Confirm` call cannot be run in a CI pipeline without manual intervention.
See sample: `cmfrt-use-confirmmanagement.bad.al`.

View file

@ -0,0 +1,14 @@
codeunit 55029 "CMFRT AQ FS Job Creator"
{
local procedure CMFRTAQFillJobFromBuffer(var WorkOrderBuffer: Record "CMFRT AQ FS WO Buffer"; var Job: Record Job)
begin
Job.Init();
Job."No." := WorkOrderBuffer."CMFRT AQ No.";
// Direct assignment skips OnValidate: dependent fields stay empty,
// posting-group checks and field subscribers never run.
Job.Description := WorkOrderBuffer."CMFRT AQ Description";
Job."Sell-to Customer No." := WorkOrderBuffer."CMFRT AQ Sell-to Customer No.";
Job."Ship-to Address" := WorkOrderBuffer."CMFRT AQ Ship-to Address";
Job."Location Code" := WorkOrderBuffer."CMFRT AQ Location Code";
end;
}

View file

@ -0,0 +1,14 @@
codeunit 55029 "CMFRT AQ FS Job Creator"
{
local procedure CMFRTAQFillJobFromBuffer(var WorkOrderBuffer: Record "CMFRT AQ FS WO Buffer"; var Job: Record Job)
begin
Job.Init();
// Primary key: direct assignment, never Validate.
Job."No." := WorkOrderBuffer."CMFRT AQ No.";
// Business fields: Validate so OnValidate logic runs.
Job.Validate(Description, WorkOrderBuffer."CMFRT AQ Description");
Job.Validate("Sell-to Customer No.", WorkOrderBuffer."CMFRT AQ Sell-to Customer No.");
Job.Validate("Ship-to Address", WorkOrderBuffer."CMFRT AQ Ship-to Address");
Job.Validate("Location Code", WorkOrderBuffer."CMFRT AQ Location Code");
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: patterns
keywords: [validate, assignment, onvalidate, field-population, buffer, creator, direct-assignment]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT populate real tables via Validate, not direct assignment
## Description
When CMFRT code populates fields on a real (non-buffer) table — Job, Sales Header, Sales Line, Job Planning Line, Ship-to Address, Transfer Header and the like — each field must be set with `Record.Validate(Field, Value)` so the field's `OnValidate` trigger logic runs. Direct assignment (`Record.Field := Value`) silently skips validation, dependent-field copying, posting-group checks, and any subscriber logic attached to the field. Two exceptions apply: primary-key and document-number fields that are set as part of record identity are assigned directly (validating them can renumber or re-key the record), and buffer/staging tables are always filled by direct assignment because they carry no business logic.
## Best Practice
In creator and method codeunits that transfer buffer values into real tables, call `Validate` for every business field: `Job.Validate(Description, Buffer."CMFRT AQ Description");`. Keep primary-key fields (`Job."No." := ...`, `SalesLine."Document No." := ...`) as direct assignments, set immediately after `Init()`. Fill buffer tables by direct assignment.
See sample: `cmfrt-validate-not-assign.good.al`.
## Anti Pattern
Filling a real table field-by-field with `:=`. The record is inserted with unvalidated data: posting-group checks never run, dependent fields (ship-to copies, cost fields, status transitions) stay empty or stale, and downstream extensions subscribed to `OnValidate` never fire. The defect surfaces later as inconsistent data that is hard to trace back to the skipped trigger.
See sample: `cmfrt-validate-not-assign.bad.al`.

View file

@ -0,0 +1,16 @@
// Only one flat permission set no composition, no read-only role.
// Adding a new table means updating this one set and hoping nothing was missed.
permissionset 2045222 "CMFRT GD Geodynamics"
{
Assignable = true;
Caption = 'CMFRT GD Geodynamics';
Permissions =
table "CMFRT GD POI" = X,
tabledata "CMFRT GD POI" = RIMD,
table "CMFRT GD Setup" = X,
tabledata "CMFRT GD Setup" = RIMD,
page "CMFRT GD POI" = X,
page "CMFRT GD Setup" = X,
codeunit "CMFRT GD IGeodynamics" = X,
codeunit "CMFRT GD IGeodynamics Impl" = X;
}

View file

@ -0,0 +1,35 @@
// Step 1: Objects set owns all AL objects with execute access.
permissionset 2045222 "CMFRT GD Objects"
{
Assignable = true;
Caption = 'CMFRT GD Geodynamics - Objects';
Permissions =
table "CMFRT GD POI" = X,
table "CMFRT GD Setup" = X,
page "CMFRT GD POI" = X,
page "CMFRT GD Setup" = X,
codeunit "CMFRT GD IGeodynamics" = X,
codeunit "CMFRT GD IGeodynamics Impl" = X;
}
// Step 2: Read set inherits object access, adds tabledata read.
permissionset 2045221 "CMFRT GD Read"
{
Assignable = true;
Caption = 'CMFRT GD Geodynamics - Read';
IncludedPermissionSets = "CMFRT GD Objects";
Permissions =
tabledata "CMFRT GD POI" = R,
tabledata "CMFRT GD Setup" = R;
}
// Step 3: Edit set inherits Read, adds insert/modify/delete.
permissionset 2045226 "CMFRT GD Edit"
{
Assignable = true;
Caption = 'CMFRT GD Geodynamics - Edit';
IncludedPermissionSets = "CMFRT GD Read";
Permissions =
tabledata "CMFRT GD POI" = IMD,
tabledata "CMFRT GD Setup" = IMD;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: security
keywords: [permissionset, permission-set, objects, read, edit, assignable, included-permission-sets, authorization]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT three-permission-set pattern
## Description
Every CMFRT functional module must ship exactly three permission set objects: an Objects set, a Read set, and an Edit set. The Objects set lists every AL object the module owns — tables, pages, codeunits, reports, interfaces — with `X` (execute) access. The Read set includes the Objects set via `IncludedPermissionSets` and grants `tabledata = R` on each table. The Edit set includes the Read set and grants `tabledata = IMD` on each writable table. All three sets have `Assignable = true`. Names follow the pattern `"CMFRT <ABBR> Objects"`, `"CMFRT <ABBR> Read"`, and `"CMFRT <ABBR> Edit"`.
## Best Practice
Define the sets in the strict composition chain Objects ← Read ← Edit so that object access is declared once and inherited. When a new table is added to the module, update the Objects set and the tabledata entries in Read and Edit — there is no risk of the object access drifting between roles because the chain is the single source of truth for it.
See sample: `cmfrt-three-permissionset-pattern.good.al`.
## Anti Pattern
Shipping fewer than three permission sets, omitting `IncludedPermissionSets` and enumerating the same object list in each set by hand, or granting `RIMD` access in a flat set that cannot be composed. Flat role sets that enumerate objects independently drift apart when tables are added or removed, and the resulting authorization gap is invisible until a user reports an access error in production.
See sample: `cmfrt-three-permissionset-pattern.bad.al`.

View file

@ -0,0 +1,109 @@
---
kind: action-skill
id: cmfrt-standards-review
version: 1
title: CMFRT AL standards review
description: Reviews AL source changes for CMFRT company standards — naming, object IDs, permission sets, breaking-changes, events, patterns, and architecture.
inputs: [pr-diff, file-path]
outputs: [findings-report]
bc-version: [all]
technologies: [al]
countries: [w1]
application-area: [all]
---
# CMFRT AL standards review
Reviews AL source changes against the CMFRT company standards defined in `custom/knowledge/`. This is a leaf action skill: it invokes no sub-skills. It is dispatched alongside `microsoft/skills/review/al-code-review.md` for every CMFRT project review.
An orchestrator invokes this skill with either a `pr-diff` (the standard PR-review entry point) or a `file-path` (single-file review). The skill produces a single JSON document conforming to the DO output contract.
## Source
Read the BCQuality knowledge index once (`knowledge-index.json` at the checkout root). Take every entry whose `layer` is `custom` as the candidate set — this covers all domains under `custom/knowledge/`: `naming`, `security`, `breaking-changes`, `events`, `patterns`, and `architecture`. Do not open individual article files at this step.
## Relevance
Apply frontmatter matching rules defined in READ against the task context:
- `bc-version` — from the target branch's `app.json` or orchestrator context; `unknown` if absent.
- `technologies``[al]`.
- `countries` — from `app.json` or orchestrator context; default `unknown`.
- `application-area` — union of application areas declared by changed objects; `unknown` if not determinable.
Discard candidates whose filter dimensions explicitly do not match. Retain conditionally applicable candidates (any dimension `unknown`); findings from those files MUST have `confidence` no higher than `medium` and the `message` MUST name the unknown dimension.
## Worklist
Narrow the relevant candidates to those that apply to the changes under review. Compute overlap against:
- Changed AL object names, types, and IDs — weighted toward tables, table extensions, codeunits, interfaces, permission sets, and any object whose ID, name, or prefix is being set or changed. Match against the `naming` and `architecture` domain candidates.
- Changed procedure signatures and parameter lists — especially global procedures being added, renamed, or extended. Match against `breaking-changes` and `events` candidates.
- Changed permission set objects and their `IncludedPermissionSets` chains. Match against `security` candidates.
- Tokens extracted from the diff: `ObsoleteState`, `ObsoleteReason`, `ObsoleteTag`, `Confirm`, `case`, `else`, `interface`, `implements`, `IntegrationEvent`, `OnBefore`, `OnAfter`, `Handled`, `IsHandled`, `IncludedPermissionSets`, `Assignable`, `procedure`, `local procedure`, `Validate`, `Init`, `Insert`, `OnInsertRecord`, `Buffer`, `Caption`, `ToolTip`, `Label`, `Comment`, `CopyStr`, `MaxStrLen`, `TryFunction`, `Codeunit.Run`, `field(`, `value(`.
- Field-assignment shape: contiguous runs of `Record.Field := Value` statements on a non-buffer record variable inside a creator/method codeunit. Match against the `patterns` candidate `cmfrt-validate-not-assign`.
- API page triggers: `PageType = API` combined with `SourceTable` on a base application table, or an `OnInsertRecord` body containing `Rec.Insert` or `exit(false)`. Match against the `architecture` candidate `cmfrt-buffer-table-api-pattern`.
A candidate enters the worklist when its `keywords` intersect the extracted tokens, or when its topic (from the index `path`, `title`, and `description`) matches the type of change in the diff. Read an article's full body only after it enters the worklist.
Resolve layer-precedence conflicts per READ. Since all candidates are in the `custom` layer, no cross-layer conflict arises unless a `microsoft` or `community` file shares a concern; in that case the `custom` file takes precedence and the lower-precedence file is recorded in `suppressed` with `reason: "layer-precedence"`.
When no custom knowledge survives filtering, emit `outcome: "no-knowledge"`. When the worklist is empty because no candidates matched the diff, emit `outcome: "completed"` with an empty `findings` array.
## Action
For each worklist entry, evaluate the diff against its `## Best Practice` and `## Anti Pattern` sections. Emit findings as follows:
- Clear Anti Pattern match: severity `major` or `blocker`. Use `blocker` only when the knowledge file explicitly states the pattern violates a platform-level guarantee. Otherwise `major`.
- Code that contradicts a Best Practice without being a full anti-pattern: severity `minor`.
- File is clearly applicable but no violation is detectable: severity `info` citing the file.
Set `confidence` to `high` for unambiguous token or structural matches, `medium` for heuristic matches or when any frontmatter dimension was `unknown`, and `low` for advisory-only applicability.
Emit `findings[].suggested-code` whenever the fix is small, local, and mechanical — for example: adding an `else` clause to a bare `case`, replacing a `Confirm(` call with `ConfirmManagement.GetResponseOrDefault(`, adding `ObsoleteState = Pending` to a deleted member, correcting a naming or caption prefix, rewriting `Record.Field := Value` as `Record.Validate(Field, Value)` (except primary-key/document-no. fields and buffer tables), or replacing a literal `CopyStr` length with `MaxStrLen(Target.Field)`. The payload must be a literal replacement for the lines in `location` with no diff markers or commentary. If the fix is mechanical-looking but spans non-contiguous lines or requires context not visible in the diff, set `suggested-code-omission-reason` instead.
After evaluating all worklist entries, consider whether the diff exhibits a CMFRT standards violation the agent recognises from general AL knowledge that no custom knowledge file covers. Hold such candidates to the precision bar in `skills/do.md` (*Agent findings*): emit only concrete, material violations a CMFRT reviewer would agree are wrong. Encode as agent findings with `references: []`, `id` prefixed `agent:`, `confidence` capped at `medium`, `severity` capped at `minor`.
Outcome selection:
- `completed` — every worklist item evaluated, including when `findings` is empty.
- `no-knowledge` — no custom knowledge survived Source, Relevance, and configuration filtering.
- `not-applicable` — no AL changes in the diff or `technologies` filter rejected the task.
- `partial` — time or token budget exhausted before worklist completion; set `outcome-reason`.
- `failed` — unrecoverable error; set `outcome-reason`.
## Output
Output conforms to the DO output contract. Example — naming and event violations found:
```json
{
"skill": { "id": "cmfrt-standards-review", "version": 1 },
"outcome": "completed",
"summary": {
"counts": { "blocker": 0, "major": 1, "minor": 1, "info": 0 },
"coverage": { "worklist-size": 3, "items-evaluated": 3 }
},
"findings": [
{
"id": "custom/knowledge/naming/cmfrt-naming-prefix.md",
"severity": "major",
"message": "Procedure 'CalculateDiscount' on codeunit 2045710 has no CMFRT prefix. Rename to 'CMFRTBACalculateDiscount' to comply with the CMFRT naming convention.",
"location": { "file": "src/Sales/Discount.Codeunit.al", "line": 8 },
"references": [{ "path": "custom/knowledge/naming/cmfrt-naming-prefix.md" }],
"confidence": "high",
"suggested-code": " procedure CMFRTBACalculateDiscount(ItemNo: Code[20]; Qty: Decimal): Decimal"
},
{
"id": "custom/knowledge/events/cmfrt-onbefore-onafter-all-globals.md",
"severity": "minor",
"message": "Global procedure 'CMFRTBACalculateDiscount' has no OnBefore/OnAfter integration events. Add both events to allow dependent extensions to intercept or react to this operation.",
"location": { "file": "src/Sales/Discount.Codeunit.al", "line": 8 },
"references": [{ "path": "custom/knowledge/events/cmfrt-onbefore-onafter-all-globals.md" }],
"confidence": "high",
"suggested-code-omission-reason": "Event declarations span multiple non-contiguous locations in the file."
}
],
"suppressed": []
}
```