)` when the flow must not show UI. No path should issue the request without approval.
-See sample: `privacy-notice-consent-for-external-data-transfer.good.al`.
+See sample: [`privacy-notice-consent-for-external-data-transfer.good.al`](privacy-notice-consent-for-external-data-transfer.good.al).
## Anti Pattern
A custom integration that posts data without checking its own notice, or that gates the call with a built-in ID such as the Exchange privacy notice ID. Consent for one service does not authorize another.
-See sample: `privacy-notice-consent-for-external-data-transfer.bad.al`.
+See sample: [`privacy-notice-consent-for-external-data-transfer.bad.al`](privacy-notice-consent-for-external-data-transfer.bad.al).
diff --git a/microsoft/knowledge/privacy/register-integration-in-privacy-notice-registrations.md b/microsoft/knowledge/privacy/register-integration-in-privacy-notice-registrations.md
index 4e76779..a9f12ff 100644
--- a/microsoft/knowledge/privacy/register-integration-in-privacy-notice-registrations.md
+++ b/microsoft/knowledge/privacy/register-integration-in-privacy-notice-registrations.md
@@ -17,7 +17,7 @@ The current extension point is `Codeunit "Privacy Notice"`. Extensions can subsc
Choose a stable ID owned by the extension. Register it through `OnRegisterPrivacyNotices`, or call `PrivacyNotice.CreatePrivacyNotice` during an intentional setup or upgrade path. Use that same ID for consent checks described in `privacy-notice-consent-for-external-data-transfer.md`.
-See sample: `register-integration-in-privacy-notice-registrations.good.al`.
+See sample: [`register-integration-in-privacy-notice-registrations.good.al`](register-integration-in-privacy-notice-registrations.good.al).
## Anti Pattern
diff --git a/microsoft/knowledge/privacy/session-logmessage-requires-dataclassification.md b/microsoft/knowledge/privacy/session-logmessage-requires-dataclassification.md
index 67381f7..2febcae 100644
--- a/microsoft/knowledge/privacy/session-logmessage-requires-dataclassification.md
+++ b/microsoft/knowledge/privacy/session-logmessage-requires-dataclassification.md
@@ -17,10 +17,10 @@ application-area: [all]
Use the overload that takes `Verbosity`, `DataClassification`, and `TelemetryScope`. For payload-free operational telemetry that does not embed customer data, `DataClassification::SystemMetadata` is the right value. Choose `TelemetryScope::ExtensionPublisher` for telemetry meant for the publishing partner only; `TelemetryScope::All` also forwards to the customer's tenant telemetry.
-See sample: `session-logmessage-requires-dataclassification.good.al`.
+See sample: [`session-logmessage-requires-dataclassification.good.al`](session-logmessage-requires-dataclassification.good.al).
## Anti Pattern
Calling `Session.LogMessage('0003', 'Operation completed', Verbosity::Normal)` โ the overload omits `DataClassification` and leaves the platform without the information needed to classify the entry. Detection signal: a `Session.LogMessage` call whose argument list ends at `Verbosity`.
-See sample: `session-logmessage-requires-dataclassification.bad.al`.
+See sample: [`session-logmessage-requires-dataclassification.bad.al`](session-logmessage-requires-dataclassification.bad.al).
diff --git a/microsoft/knowledge/privacy/table-level-data-classification-cascades.md b/microsoft/knowledge/privacy/table-level-data-classification-cascades.md
index 253d2cc..f41dc11 100644
--- a/microsoft/knowledge/privacy/table-level-data-classification-cascades.md
+++ b/microsoft/knowledge/privacy/table-level-data-classification-cascades.md
@@ -17,7 +17,7 @@ A valid table-level `DataClassification` is the effective default for the Normal
Use a table-level classification when it accurately describes the table's fields, and add a field-level classification only where a field stores a different kind of data. Do not flag a Normal field solely because it omits an explicit property when its own table supplies a valid default; verify whether the inherited value matches the field's data instead. A `tableextension` has no default to inherit, so require an explicit `DataClassification` on every Normal field it adds.
-See sample: `table-level-data-classification-cascades.good.al`.
+See sample: [`table-level-data-classification-cascades.good.al`](table-level-data-classification-cascades.good.al).
## Anti Pattern
diff --git a/microsoft/knowledge/query/reopening-query-resets-cursor-but-keeps-filters.md b/microsoft/knowledge/query/reopening-query-resets-cursor-but-keeps-filters.md
index 4bc8816..0d7ca47 100644
--- a/microsoft/knowledge/query/reopening-query-resets-cursor-but-keeps-filters.md
+++ b/microsoft/knowledge/query/reopening-query-resets-cursor-but-keeps-filters.md
@@ -17,10 +17,10 @@ Calling `Open()` on an already open query first closes the current dataset and o
Open once for one read pass. Close after the pass, and call `Clear(QueryVariable)` before reusing the variable for a logically independent query whose filters must start empty. Set the next pass's filters explicitly before reopening.
-See sample: `reopening-query-resets-cursor-but-keeps-filters.good.al`.
+See sample: [`reopening-query-resets-cursor-but-keeps-filters.good.al`](reopening-query-resets-cursor-but-keeps-filters.good.al).
## Anti Pattern
Calling `Open()` inside or between reads to "advance" or "start fresh", or reusing the same query variable for a new operation while assuming `Open()` cleared old filters. The code compiles but can repeatedly process the first row or silently omit rows behind a retained filter.
-See sample: `reopening-query-resets-cursor-but-keeps-filters.bad.al`.
+See sample: [`reopening-query-resets-cursor-but-keeps-filters.bad.al`](reopening-query-resets-cursor-but-keeps-filters.bad.al).
diff --git a/microsoft/knowledge/query/set-query-filters-before-open.md b/microsoft/knowledge/query/set-query-filters-before-open.md
index f999456..bb82e9d 100644
--- a/microsoft/knowledge/query/set-query-filters-before-open.md
+++ b/microsoft/knowledge/query/set-query-filters-before-open.md
@@ -17,10 +17,10 @@ application-area: [all]
Apply every filter before `Open()`, then read the dataset to completion and call `Close()`. When a later branch needs different filters, close or clear the query, set the new filters, and open a new dataset deliberately.
-See sample: `set-query-filters-before-open.good.al`.
+See sample: [`set-query-filters-before-open.good.al`](set-query-filters-before-open.good.al).
## Anti Pattern
`Query.Open()` followed by `SetFilter` or `SetRange` and then `Read()` under the assumption that the filter updates the open cursor. Refiltering after `Open()` is valid only when the code intentionally opens a fresh dataset afterward.
-See sample: `set-query-filters-before-open.bad.al`.
+See sample: [`set-query-filters-before-open.bad.al`](set-query-filters-before-open.bad.al).
diff --git a/microsoft/knowledge/security/al-has-no-built-in-htmlencode.md b/microsoft/knowledge/security/al-has-no-built-in-htmlencode.md
index 7441712..649c384 100644
--- a/microsoft/knowledge/security/al-has-no-built-in-htmlencode.md
+++ b/microsoft/knowledge/security/al-has-no-built-in-htmlencode.md
@@ -15,8 +15,8 @@ AL does not ship a built-in `HtmlEncode` (or equivalent) function. Code that bui
## Best Practice
-Replace the four characters by hand before concatenating user content into HTML: `&` โ `&` first, then `<` โ `<`, `>` โ `>`, `"` โ `"`. Centralize the substitution in one helper so every HTML producer in the extension uses the same encoder. Better still, do not build raw HTML at all โ use a structured format (JSON for an API payload, a report layout for a printed document) and let the renderer do the encoding. See sample: `al-has-no-built-in-htmlencode.good.al`.
+Replace the four characters by hand before concatenating user content into HTML: `&` โ `&` first, then `<` โ `<`, `>` โ `>`, `"` โ `"`. Centralize the substitution in one helper so every HTML producer in the extension uses the same encoder. Better still, do not build raw HTML at all โ use a structured format (JSON for an API payload, a report layout for a printed document) and let the renderer do the encoding. See sample: [`al-has-no-built-in-htmlencode.good.al`](al-has-no-built-in-htmlencode.good.al).
## Anti Pattern
-`HtmlContent := 'Welcome ' + UserName + '!
'` โ any record-field value or user input concatenated directly into an HTML string. Reviewers should flag any string concatenation whose right-hand operand is a field, a parameter, or any non-literal value, and whose surrounding context contains HTML tags (`<`, ``, `
'` โ any record-field value or user input concatenated directly into an HTML string. Reviewers should flag any string concatenation whose right-hand operand is a field, a parameter, or any non-literal value, and whose surrounding context contains HTML tags (`<`, ``, `
` column headers, so a captionless field that is mean
Reserve `ShowCaption = false` in a layout-table grid for non-editable, free-standing content cells. If a field's role is to label or annotate another field in the same grid, restructure the grid to meet the data-table conditions (see `grid-data-table-heuristic.md`) instead of hiding the caption.
-See sample: `standalone-content-in-layout-table.good.al`.
+See sample: [`standalone-content-in-layout-table.good.al`](standalone-content-in-layout-table.good.al).
diff --git a/microsoft/knowledge/ui/style-expr-text-vs-boolean.md b/microsoft/knowledge/ui/style-expr-text-vs-boolean.md
index 5040099..720423b 100644
--- a/microsoft/knowledge/ui/style-expr-text-vs-boolean.md
+++ b/microsoft/knowledge/ui/style-expr-text-vs-boolean.md
@@ -22,4 +22,4 @@ When `StyleExpr` is Text, you must trace the variable's assignments โ typicall
Inspect the declared type of the symbol referenced by `StyleExpr` before drawing conclusions. If it is Boolean, evaluate the `Style` property. If it is Text, follow every assignment to the variable and check the full set of possible style values against `cosmetic-styles-need-no-textual-context.md` and `semantic-styles-need-independent-textual-meaning.md`.
-See sample: `style-expr-text-vs-boolean.good.al`.
+See sample: [`style-expr-text-vs-boolean.good.al`](style-expr-text-vs-boolean.good.al).
diff --git a/microsoft/knowledge/ui/tabular-intent-requires-data-table-conditions.md b/microsoft/knowledge/ui/tabular-intent-requires-data-table-conditions.md
index b56aadb..c02bbd9 100644
--- a/microsoft/knowledge/ui/tabular-intent-requires-data-table-conditions.md
+++ b/microsoft/knowledge/ui/tabular-intent-requires-data-table-conditions.md
@@ -24,4 +24,4 @@ Both manifestations have the same root cause: tabular semantics were intended bu
A single field that keeps its visible caption is enough to demote an entire would-be data-table grid into a layout table โ and silently strip the labels off its sibling captionless fields. Either restructure to meet all three conditions, or restore captions on every editable field.
-See sample: `tabular-intent-requires-data-table-conditions.bad.al`.
+See sample: [`tabular-intent-requires-data-table-conditions.bad.al`](tabular-intent-requires-data-table-conditions.bad.al).
diff --git a/microsoft/knowledge/ui/validate-request-page-input-in-onqueryclosepage.md b/microsoft/knowledge/ui/validate-request-page-input-in-onqueryclosepage.md
index 75c8a09..d796827 100644
--- a/microsoft/knowledge/ui/validate-request-page-input-in-onqueryclosepage.md
+++ b/microsoft/knowledge/ui/validate-request-page-input-in-onqueryclosepage.md
@@ -17,13 +17,13 @@ application-area: [all]
## Best Practice
-Put the validation in one local procedure and call it from both places: from the request page's `OnQueryClosePage`, so an interactive user can correct the input where they entered it, and from `OnPreReport` (or the relevant `OnPreDataItem`), so a run without a request page is still refused. Guard the interactive call on the close action โ validate only when the user confirmed the run, for example `if CloseAction = Action::OK then`. The base application uses this shape; report 292, `Copy Sales Document`, validates its request-page input in `OnQueryClosePage` behind a close-action check. Mark the control with `ShowMandatory` as well, so the requirement is visible before the user submits โ see `showmandatory-on-code-required-page-fields.md`. See sample: `validate-request-page-input-in-onqueryclosepage.good.al`.
+Put the validation in one local procedure and call it from both places: from the request page's `OnQueryClosePage`, so an interactive user can correct the input where they entered it, and from `OnPreReport` (or the relevant `OnPreDataItem`), so a run without a request page is still refused. Guard the interactive call on the close action โ validate only when the user confirmed the run, for example `if CloseAction = Action::OK then`. The base application uses this shape; report 292, `Copy Sales Document`, validates its request-page input in `OnQueryClosePage` behind a close-action check. Mark the control with `ShowMandatory` as well, so the requirement is visible before the user submits โ see `showmandatory-on-code-required-page-fields.md`. See sample: [`validate-request-page-input-in-onqueryclosepage.good.al`](validate-request-page-input-in-onqueryclosepage.good.al).
## Anti Pattern
Validating mandatory request-page input only in `OnPreReport`. The check is correct and the report is never run with bad input, but every interactive mistake costs the user the whole request page: the error arrives after the page is gone, and filters, dates, and options all have to be entered again. Reviewer signal: a `TestField`, `Error`, or blank/zero-value check in `OnPreReport` or `OnPreDataItem` against a variable that is bound to a request-page control, in a report whose request page declares no `OnQueryClosePage`.
-The mirror defect is an `OnQueryClosePage` that validates without inspecting `CloseAction`: because an error prevents the page from closing, a user who presses Cancel or Esc to abandon the report is trapped in a request page that errors on every attempt to leave it. Validating only in `OnQueryClosePage` is the third variant โ the interactive path behaves well, and a job queue entry runs the report with unchecked input. See sample: `validate-request-page-input-in-onqueryclosepage.bad.al`.
+The mirror defect is an `OnQueryClosePage` that validates without inspecting `CloseAction`: because an error prevents the page from closing, a user who presses Cancel or Esc to abandon the report is trapped in a request page that errors on every attempt to leave it. Validating only in `OnQueryClosePage` is the third variant โ the interactive path behaves well, and a job queue entry runs the report with unchecked input. See sample: [`validate-request-page-input-in-onqueryclosepage.bad.al`](validate-request-page-input-in-onqueryclosepage.bad.al).
## See also
diff --git a/microsoft/knowledge/upgrade/breaking-changes-only-on-tables-without-data.md b/microsoft/knowledge/upgrade/breaking-changes-only-on-tables-without-data.md
index 9ba7e8a..5efc751 100644
--- a/microsoft/knowledge/upgrade/breaking-changes-only-on-tables-without-data.md
+++ b/microsoft/knowledge/upgrade/breaking-changes-only-on-tables-without-data.md
@@ -17,10 +17,10 @@ Primary-key changes and field-type changes (for example widening `Integer` to `B
Treat primary-key and field-type changes as restricted to tables introduced in the same change. For changes on tables with existing data, design and ship the corresponding upgrade procedure (typically backed by `DataTransfer` and an upgrade tag) that guarantees the new layout is achievable for every row, and verify with concrete evidence that the existing values fit the new constraint (no PK collisions, no value-range overflow).
-See sample: `breaking-changes-only-on-tables-without-data.good.al`.
+See sample: [`breaking-changes-only-on-tables-without-data.good.al`](breaking-changes-only-on-tables-without-data.good.al).
## Anti Pattern
Changing the primary key on a base-app table, or widening / narrowing a field type on a table that has been shipping for releases, with no accompanying upgrade plan. The change compiles cleanly and may even deploy on an empty-ish tenant, then fails on customers who actually have data.
-See sample: `breaking-changes-only-on-tables-without-data.bad.al`.
+See sample: [`breaking-changes-only-on-tables-without-data.bad.al`](breaking-changes-only-on-tables-without-data.bad.al).
diff --git a/microsoft/knowledge/upgrade/check-only-triggers-do-not-migrate-data.md b/microsoft/knowledge/upgrade/check-only-triggers-do-not-migrate-data.md
index 30f6ca7..8770f93 100644
--- a/microsoft/knowledge/upgrade/check-only-triggers-do-not-migrate-data.md
+++ b/microsoft/knowledge/upgrade/check-only-triggers-do-not-migrate-data.md
@@ -17,10 +17,10 @@ application-area: [all]
Have check triggers call query-only helpers that raise an error when an invariant fails. Put every `Insert`, `Modify`, `Delete`, `Rename`, `DataTransfer`, and other migration write behind helpers called from the matching `OnUpgrade...` trigger.
-See sample: `check-only-triggers-do-not-migrate-data.good.al`.
+See sample: [`check-only-triggers-do-not-migrate-data.good.al`](check-only-triggers-do-not-migrate-data.good.al).
## Anti Pattern
Repairing data in `OnCheckPreconditions...` or finishing migration in `OnValidateUpgrade...`. Those writes blur the phase contract and make a check alter the state it is supposed to assess.
-See sample: `check-only-triggers-do-not-migrate-data.bad.al`.
+See sample: [`check-only-triggers-do-not-migrate-data.bad.al`](check-only-triggers-do-not-migrate-data.bad.al).
diff --git a/microsoft/knowledge/upgrade/datatransfer-for-bulk-init.md b/microsoft/knowledge/upgrade/datatransfer-for-bulk-init.md
index 988dfa4..830dbdb 100644
--- a/microsoft/knowledge/upgrade/datatransfer-for-bulk-init.md
+++ b/microsoft/knowledge/upgrade/datatransfer-for-bulk-init.md
@@ -17,13 +17,13 @@ Tables that can contain more than 300,000 records, and any newly added field on
For a bulk update use a `DataTransfer` variable: call `SetTables(Database::"...", Database::"...")` (source and destination may be the same table), add filters with `AddSourceFilter`, set the target value with `AddConstantValue` (or copy a source field with `AddFieldValue`), and execute with `CopyFields()`. To express multiple distinct updates against the same table, `Clear` the `DataTransfer` between executions and configure the next one.
-See sample: `datatransfer-for-bulk-init.good.al`.
+See sample: [`datatransfer-for-bulk-init.good.al`](datatransfer-for-bulk-init.good.al).
## Anti Pattern
Iterating with `FindSet(true) ... repeat ... Modify() ... until Next() = 0` to set a single field across an entire large table. On 300k+ rows this is the canonical slow-upgrade footgun.
-See sample: `datatransfer-for-bulk-init.bad.al`.
+See sample: [`datatransfer-for-bulk-init.bad.al`](datatransfer-for-bulk-init.bad.al).
## See also
diff --git a/microsoft/knowledge/upgrade/datatransfer-skips-triggers-and-subscribers.md b/microsoft/knowledge/upgrade/datatransfer-skips-triggers-and-subscribers.md
index 34a406b..33173da 100644
--- a/microsoft/knowledge/upgrade/datatransfer-skips-triggers-and-subscribers.md
+++ b/microsoft/knowledge/upgrade/datatransfer-skips-triggers-and-subscribers.md
@@ -19,10 +19,10 @@ For *new fields and tables added in the same change* this is fine: nothing yet d
Use `DataTransfer` when set-based transfer is safe and row-level business logic is intentionally unnecessary โ initial population of a new field is the canonical case. When an existing field's validation must run, loop through records and call `Validate(Field, Value)`; if the table's modify trigger must also run, follow with `Modify(true)`. If performance requires `DataTransfer`, document exactly which field-validation and row-modification triggers or subscribers are intentionally bypassed and verify that derived data remains correct.
-See sample: `datatransfer-skips-triggers-and-subscribers.good.al`.
+See sample: [`datatransfer-skips-triggers-and-subscribers.good.al`](datatransfer-skips-triggers-and-subscribers.good.al).
## Anti Pattern
Reaching for `DataTransfer` to update an existing field with non-trivial `OnValidate` or `OnModify` logic, without confirming that both validation and row-modification subscribers can be skipped. Replacing it with only `Modify(true)` is also incomplete when field validation is required; call `Validate` for that field first.
-See sample: `datatransfer-skips-triggers-and-subscribers.bad.al`.
+See sample: [`datatransfer-skips-triggers-and-subscribers.bad.al`](datatransfer-skips-triggers-and-subscribers.bad.al).
diff --git a/microsoft/knowledge/upgrade/do-not-block-upgrade-on-data-errors.md b/microsoft/knowledge/upgrade/do-not-block-upgrade-on-data-errors.md
index cf585eb..868b61e 100644
--- a/microsoft/knowledge/upgrade/do-not-block-upgrade-on-data-errors.md
+++ b/microsoft/knowledge/upgrade/do-not-block-upgrade-on-data-errors.md
@@ -17,10 +17,10 @@ When upgrade code encounters unexpected data โ a record it expected to find, a
When an upgrade procedure detects something missing, call `Session.LogMessage` with a stable event ID, classify the message verbosity (typically `Warning`), and `exit` the procedure so the rest of the upgrade can proceed. The platform telemetry then surfaces the situation to the partner without breaking the customer.
-See sample: `do-not-block-upgrade-on-data-errors.good.al`.
+See sample: [`do-not-block-upgrade-on-data-errors.good.al`](do-not-block-upgrade-on-data-errors.good.al).
## Anti Pattern
Calling `Record.Get(Key)` (or any other erroring API) and letting the error propagate out of the upgrade trigger. The first tenant with imperfect data fails to upgrade, and the failure surfaces as a hard upgrade error rather than as a telemetry signal.
-See sample: `do-not-block-upgrade-on-data-errors.bad.al`.
+See sample: [`do-not-block-upgrade-on-data-errors.bad.al`](do-not-block-upgrade-on-data-errors.bad.al).
diff --git a/microsoft/knowledge/upgrade/enum-values-additive-at-end.md b/microsoft/knowledge/upgrade/enum-values-additive-at-end.md
index 4de929b..332efa6 100644
--- a/microsoft/knowledge/upgrade/enum-values-additive-at-end.md
+++ b/microsoft/knowledge/upgrade/enum-values-additive-at-end.md
@@ -17,13 +17,13 @@ An AL `enum` is a fixed list of ordinal-named values. Persisted rows reference e
When adding an enum value, place it after the last existing `value(N; ...)` entry, with an ordinal strictly greater than every existing one. Never renumber existing entries. To retire a value, do not delete it: mark it `ObsoleteState = Pending` (and later `Removed`) with `ObsoleteReason` and `ObsoleteTag` so the ordinal remains taken.
-See sample: `enum-values-additive-at-end.good.al`.
+See sample: [`enum-values-additive-at-end.good.al`](enum-values-additive-at-end.good.al).
## Anti Pattern
Inserting a value between existing entries ("just put `NewMiddleValue` between `First` and `Second`"), or removing a value from the enum without first going through `ObsoleteState = Pending` โ `Removed`. Every row whose persisted ordinal matched the removed or shifted value now reads as a different member.
-See sample: `enum-values-additive-at-end.bad.al`.
+See sample: [`enum-values-additive-at-end.bad.al`](enum-values-additive-at-end.bad.al).
## See also
diff --git a/microsoft/knowledge/upgrade/first-install-dataversion-zero-check.md b/microsoft/knowledge/upgrade/first-install-dataversion-zero-check.md
index 6324836..5cbdec8 100644
--- a/microsoft/knowledge/upgrade/first-install-dataversion-zero-check.md
+++ b/microsoft/knowledge/upgrade/first-install-dataversion-zero-check.md
@@ -17,13 +17,13 @@ On the first install of an extension on a tenant the platform records a zero dat
In `OnInstallAppPerCompany`, fetch the current `ModuleInfo` via `NavApp.GetCurrentModuleInfo`, compare `AppInfo.DataVersion()` to `Version.Create('0.0.0.0')`, and run first-install seed logic only when they match. On a non-zero data version, follow the reinstall path or exit.
-See sample: `first-install-dataversion-zero-check.good.al`.
+See sample: [`first-install-dataversion-zero-check.good.al`](first-install-dataversion-zero-check.good.al).
## Anti Pattern
Treating `OnInstallAppPerCompany` as if it always implies "fresh tenant". The trigger also fires when reinstalling over an existing data set; without the `0.0.0.0` guard, first-install seed code can run again and duplicate rows.
-See sample: `first-install-dataversion-zero-check.bad.al`.
+See sample: [`first-install-dataversion-zero-check.bad.al`](first-install-dataversion-zero-check.bad.al).
## See also
diff --git a/microsoft/knowledge/upgrade/guard-database-reads.md b/microsoft/knowledge/upgrade/guard-database-reads.md
index c5bc206..09a99e3 100644
--- a/microsoft/knowledge/upgrade/guard-database-reads.md
+++ b/microsoft/knowledge/upgrade/guard-database-reads.md
@@ -17,10 +17,10 @@ Inside an upgrade codeunit (or any procedure transitively invoked from `OnUpgrad
Wrap every read in an `if`. `if Item.Get(No) then ...`, `if Customer.FindSet() then;`, `if not Vendor.FindLast() then exit;`. The empty-then form `if Customer.FindSet() then;` is the idiomatic way to attempt a read whose only purpose is to position a record, while swallowing the "not found" case.
-See sample: `guard-database-reads.good.al`.
+See sample: [`guard-database-reads.good.al`](guard-database-reads.good.al).
## Anti Pattern
Calling `Item.Get()`, `Customer.FindSet()`, or `Vendor.FindLast()` bare in upgrade code. The first tenant whose data does not match the upgrade's assumptions will fail to upgrade.
-See sample: `guard-database-reads.bad.al`.
+See sample: [`guard-database-reads.bad.al`](guard-database-reads.bad.al).
diff --git a/microsoft/knowledge/upgrade/initvalue-does-not-update-existing-rows.md b/microsoft/knowledge/upgrade/initvalue-does-not-update-existing-rows.md
index 4733ef2..bfa0f03 100644
--- a/microsoft/knowledge/upgrade/initvalue-does-not-update-existing-rows.md
+++ b/microsoft/knowledge/upgrade/initvalue-does-not-update-existing-rows.md
@@ -23,10 +23,10 @@ Several legitimate cases do NOT need upgrade code:
When a new field on an existing table has an `InitValue` that matters, ship an upgrade procedure that walks the existing rows and sets the field to the same value โ typically via `DataTransfer.AddConstantValue` for performance โ guarded by an upgrade tag.
-See sample: `initvalue-does-not-update-existing-rows.good.al`.
+See sample: [`initvalue-does-not-update-existing-rows.good.al`](initvalue-does-not-update-existing-rows.good.al).
## Anti Pattern
Adding a field with `InitValue = true;` (or any non-default `InitValue`) and shipping no upgrade code. Existing rows silently carry the datatype default, leaving the table in two states: rows created before the upgrade with the wrong value, and rows created after with the right one.
-See sample: `initvalue-does-not-update-existing-rows.bad.al`.
+See sample: [`initvalue-does-not-update-existing-rows.bad.al`](initvalue-does-not-update-existing-rows.bad.al).
diff --git a/microsoft/knowledge/upgrade/install-code-does-not-run-on-version-upgrade.md b/microsoft/knowledge/upgrade/install-code-does-not-run-on-version-upgrade.md
index 12f432f..52c3989 100644
--- a/microsoft/knowledge/upgrade/install-code-does-not-run-on-version-upgrade.md
+++ b/microsoft/knowledge/upgrade/install-code-does-not-run-on-version-upgrade.md
@@ -17,10 +17,10 @@ An install codeunit runs when an extension is installed for the first time or an
Use `Subtype = Install` for first-install and reinstall initialization. Put version migration in a separate `Subtype = Upgrade` codeunit and enter it from `OnUpgradePerCompany` or `OnUpgradePerDatabase`.
-See sample: `install-code-does-not-run-on-version-upgrade.good.al`.
+See sample: [`install-code-does-not-run-on-version-upgrade.good.al`](install-code-does-not-run-on-version-upgrade.good.al).
## Anti Pattern
Putting a schema or data migration only in an install trigger and expecting it to run when a higher app version is upgraded. The migration is never invoked on that path.
-See sample: `install-code-does-not-run-on-version-upgrade.bad.al`.
+See sample: [`install-code-does-not-run-on-version-upgrade.bad.al`](install-code-does-not-run-on-version-upgrade.bad.al).
diff --git a/microsoft/knowledge/upgrade/minimize-onvalidate-upgrade-triggers.md b/microsoft/knowledge/upgrade/minimize-onvalidate-upgrade-triggers.md
index b9e5e13..3095655 100644
--- a/microsoft/knowledge/upgrade/minimize-onvalidate-upgrade-triggers.md
+++ b/microsoft/knowledge/upgrade/minimize-onvalidate-upgrade-triggers.md
@@ -17,10 +17,10 @@ Triggers such as `OnValidateUpgradePerCompany` run on every upgrade pass. A full
Filter directly to invalid rows and use `IsEmpty` or another bounded existence check where possible. If a broad validation is unavoidable, document the invariant that requires it and keep all data changes in `OnUpgrade...`.
-See sample: `minimize-onvalidate-upgrade-triggers.good.al`.
+See sample: [`minimize-onvalidate-upgrade-triggers.good.al`](minimize-onvalidate-upgrade-triggers.good.al).
## Anti Pattern
Reading every record in `OnValidateUpgradePerCompany` when a filtered existence check can prove the same invariant. The scan repeats on every upgrade.
-See sample: `minimize-onvalidate-upgrade-triggers.bad.al`.
+See sample: [`minimize-onvalidate-upgrade-triggers.bad.al`](minimize-onvalidate-upgrade-triggers.bad.al).
diff --git a/microsoft/knowledge/upgrade/no-external-calls-in-upgrade.md b/microsoft/knowledge/upgrade/no-external-calls-in-upgrade.md
index eb644b6..f04fb40 100644
--- a/microsoft/knowledge/upgrade/no-external-calls-in-upgrade.md
+++ b/microsoft/knowledge/upgrade/no-external-calls-in-upgrade.md
@@ -19,10 +19,10 @@ The rule applies inside any codeunit with `Subtype = Upgrade` and to any procedu
Defer external calls to runtime code. If a piece of upgrade work conceptually needs data from an external service, set a flag or write a queue row during upgrade and have the runtime code make the call later (for example on first user sign-in or via job queue), where retries and degraded modes are tractable.
-See sample: `no-external-calls-in-upgrade.good.al`.
+See sample: [`no-external-calls-in-upgrade.good.al`](no-external-calls-in-upgrade.good.al).
## Anti Pattern
Calling `HttpClient.Get`, `HttpClient.Post`, or DotNet interop methods from `OnUpgradePerCompany`, `OnUpgradePerDatabase`, or any procedure they invoke.
-See sample: `no-external-calls-in-upgrade.bad.al`.
+See sample: [`no-external-calls-in-upgrade.bad.al`](no-external-calls-in-upgrade.bad.al).
diff --git a/microsoft/knowledge/upgrade/obsolete-pending-to-removed-staging.md b/microsoft/knowledge/upgrade/obsolete-pending-to-removed-staging.md
index cb008ac..64a54f2 100644
--- a/microsoft/knowledge/upgrade/obsolete-pending-to-removed-staging.md
+++ b/microsoft/knowledge/upgrade/obsolete-pending-to-removed-staging.md
@@ -17,10 +17,10 @@ application-area: [all]
Stage the deprecation across releases. Step 1: mark `Pending` with reason and tag; consumers are warned but data and code keep working. Step 2: in a later release, transition to `Removed` and (if persisted data references the element) ship an upgrade procedure that migrates that data โ gated by an upgrade tag. The standard mechanic for retiring the actual implementation body is to remove the `#if not CLEAN` block in the same release that flips the state to `Removed`.
-See sample: `obsolete-pending-to-removed-staging.good.al`.
+See sample: [`obsolete-pending-to-removed-staging.good.al`](obsolete-pending-to-removed-staging.good.al).
## Anti Pattern
Jumping straight to `ObsoleteState = Removed` without a prior `Pending` release. Consumers have no deprecation window to migrate and any data still referencing the element is stranded. Equally wrong: leaving an element `Pending` indefinitely and never staging its removal โ the deprecation never completes.
-See sample: `obsolete-pending-to-removed-staging.bad.al`.
+See sample: [`obsolete-pending-to-removed-staging.bad.al`](obsolete-pending-to-removed-staging.bad.al).
diff --git a/microsoft/knowledge/upgrade/obsoletion-requires-reason-and-tag.md b/microsoft/knowledge/upgrade/obsoletion-requires-reason-and-tag.md
index d6ec37a..b468437 100644
--- a/microsoft/knowledge/upgrade/obsoletion-requires-reason-and-tag.md
+++ b/microsoft/knowledge/upgrade/obsoletion-requires-reason-and-tag.md
@@ -22,13 +22,13 @@ In both forms, the reason should name the replacement and the tag should identif
For an object or field, set all three properties together. For a method, variable, or event, provide both `[Obsolete]` arguments. Keep the original tag stable through the lifecycle rather than changing it to a planned removal version.
-See sample: `obsoletion-requires-reason-and-tag.good.al`.
+See sample: [`obsoletion-requires-reason-and-tag.good.al`](obsoletion-requires-reason-and-tag.good.al).
## Anti Pattern
Setting only `ObsoleteState = Pending`/`Removed` on an object or field, or using `[Obsolete('', '')]` on a method, variable, or event. Both forms produce deprecation metadata without useful replacement guidance or traceability.
-See sample: `obsoletion-requires-reason-and-tag.bad.al`.
+See sample: [`obsoletion-requires-reason-and-tag.bad.al`](obsoletion-requires-reason-and-tag.bad.al).
## See also
diff --git a/microsoft/knowledge/upgrade/register-upgrade-tags-with-subscribers.md b/microsoft/knowledge/upgrade/register-upgrade-tags-with-subscribers.md
index e5e1983..0bba7c2 100644
--- a/microsoft/knowledge/upgrade/register-upgrade-tags-with-subscribers.md
+++ b/microsoft/knowledge/upgrade/register-upgrade-tags-with-subscribers.md
@@ -19,10 +19,10 @@ Registration is not install-time seeding. When an extension is installed into an
In the upgrade codeunit, guard work with `HasUpgradeTag` and call `SetUpgradeTag` only after successful completion. Seed the same tag explicitly from `OnInstallAppPerCompany` when first-install logic should not run as a later upgrade. Also add historical per-company tags to `OnGetPerCompanyUpgradeTags` so `SetAllUpgradeTags` marks them complete for newly created companies. Keep the tag definition shared so all paths use the exact same value.
-See sample: `register-upgrade-tags-with-subscribers.good.al`.
+See sample: [`register-upgrade-tags-with-subscribers.good.al`](register-upgrade-tags-with-subscribers.good.al).
## Anti Pattern
Assuming an `OnGetPerCompanyUpgradeTags` subscriber sets tags during extension installation, or omitting the subscriber and allowing old upgrade steps to run when `SetAllUpgradeTags` initializes a new company. The subscriber supplies a list; only `SetAllUpgradeTags` or an explicit `SetUpgradeTag` call persists it.
-See sample: `register-upgrade-tags-with-subscribers.bad.al`.
+See sample: [`register-upgrade-tags-with-subscribers.bad.al`](register-upgrade-tags-with-subscribers.bad.al).
diff --git a/microsoft/knowledge/upgrade/skip-nonessential-work-via-execution-context.md b/microsoft/knowledge/upgrade/skip-nonessential-work-via-execution-context.md
index 0b441e6..c4558b7 100644
--- a/microsoft/knowledge/upgrade/skip-nonessential-work-via-execution-context.md
+++ b/microsoft/knowledge/upgrade/skip-nonessential-work-via-execution-context.md
@@ -19,10 +19,10 @@ This is the opposite of a load-bearing concern: code that MUST run during the up
In a runtime procedure that performs non-essential side effects, guard the side-effect block with `if GetExecutionContext() = ExecutionContext::Upgrade then exit;` and include a brief comment explaining what is being skipped and why.
-See sample: `skip-nonessential-work-via-execution-context.good.al`.
+See sample: [`skip-nonessential-work-via-execution-context.good.al`](skip-nonessential-work-via-execution-context.good.al).
## Anti Pattern
Using `GetExecutionContext()` to *enable* upgrade behaviour from outside an upgrade codeunit. Upgrade behaviour belongs in a codeunit with `Subtype = Upgrade`; runtime code should only use the check to *suppress* optional work.
-See sample: `skip-nonessential-work-via-execution-context.bad.al`.
+See sample: [`skip-nonessential-work-via-execution-context.bad.al`](skip-nonessential-work-via-execution-context.bad.al).
diff --git a/microsoft/knowledge/upgrade/triggers-call-helpers-not-implementations.md b/microsoft/knowledge/upgrade/triggers-call-helpers-not-implementations.md
index dcc21e3..078e109 100644
--- a/microsoft/knowledge/upgrade/triggers-call-helpers-not-implementations.md
+++ b/microsoft/knowledge/upgrade/triggers-call-helpers-not-implementations.md
@@ -19,10 +19,10 @@ Empty `OnUpgradePerCompany` / `OnUpgradePerDatabase` triggers are acceptable โ
Each upgrade trigger contains an ordered list of procedure calls, one per feature: `UpgradeFeatureA();` `UpgradeFeatureB();`. Each procedure handles its own upgrade tag, its own data work, and can be added or removed independently.
-See sample: `triggers-call-helpers-not-implementations.good.al`.
+See sample: [`triggers-call-helpers-not-implementations.good.al`](triggers-call-helpers-not-implementations.good.al).
## Anti Pattern
Implementing record loops, `ModifyAll`, or other data work directly in the trigger body. The trigger then mixes orchestration with implementation, and adding a second feature requires editing the trigger rather than appending one line.
-See sample: `triggers-call-helpers-not-implementations.bad.al`.
+See sample: [`triggers-call-helpers-not-implementations.bad.al`](triggers-call-helpers-not-implementations.bad.al).
diff --git a/microsoft/knowledge/upgrade/upgrade-codeunit-subtype.md b/microsoft/knowledge/upgrade/upgrade-codeunit-subtype.md
index a9fee7c..8bf558d 100644
--- a/microsoft/knowledge/upgrade/upgrade-codeunit-subtype.md
+++ b/microsoft/knowledge/upgrade/upgrade-codeunit-subtype.md
@@ -17,10 +17,10 @@ A codeunit only participates in the upgrade pipeline when it sets `Subtype = Upg
Place every piece of upgrade logic in a codeunit declared with `Subtype = Upgrade;` and expose entry points via the two triggers `OnUpgradePerCompany` and `OnUpgradePerDatabase`. Helper procedures may live in normal codeunits, but they inherit the upgrade-context rules (guarded reads, no external calls, upgrade tags, etc.) when called from an upgrade trigger.
-See sample: `upgrade-codeunit-subtype.good.al`.
+See sample: [`upgrade-codeunit-subtype.good.al`](upgrade-codeunit-subtype.good.al).
## Anti Pattern
Putting upgrade-style logic in a regular codeunit that the platform never invokes during upgrade โ for example a normal codeunit with a manually invented "RunUpgrade" procedure that nothing wires to the upgrade pipeline. The migration code will simply not run.
-See sample: `upgrade-codeunit-subtype.bad.al`.
+See sample: [`upgrade-codeunit-subtype.bad.al`](upgrade-codeunit-subtype.bad.al).
diff --git a/microsoft/knowledge/upgrade/use-upgrade-tags-not-version-checks.md b/microsoft/knowledge/upgrade/use-upgrade-tags-not-version-checks.md
index 62347d1..bb7649f 100644
--- a/microsoft/knowledge/upgrade/use-upgrade-tags-not-version-checks.md
+++ b/microsoft/knowledge/upgrade/use-upgrade-tags-not-version-checks.md
@@ -17,13 +17,13 @@ Each piece of upgrade logic must run exactly once per company (or database) acro
Every upgrade procedure starts with a `HasUpgradeTag` guard and ends with `SetUpgradeTag` once the work is committed. Each feature gets its own tag string so features can be re-run independently if needed.
-See sample: `use-upgrade-tags-not-version-checks.good.al`.
+See sample: [`use-upgrade-tags-not-version-checks.good.al`](use-upgrade-tags-not-version-checks.good.al).
## Anti Pattern
Branching on `MyApp.DataVersion().Major > N`, or chains of `< N` / `< M` to decide which upgrade step to run. Such code becomes unmaintainable after a few releases and silently does the wrong thing on tenants that skip versions.
-See sample: `use-upgrade-tags-not-version-checks.bad.al`.
+See sample: [`use-upgrade-tags-not-version-checks.bad.al`](use-upgrade-tags-not-version-checks.bad.al).
## See also
diff --git a/microsoft/knowledge/web-services/api-enum-values-are-a-contract-by-name-not-ordinal.md b/microsoft/knowledge/web-services/api-enum-values-are-a-contract-by-name-not-ordinal.md
index 7988326..66fe36e 100644
--- a/microsoft/knowledge/web-services/api-enum-values-are-a-contract-by-name-not-ordinal.md
+++ b/microsoft/knowledge/web-services/api-enum-values-are-a-contract-by-name-not-ordinal.md
@@ -21,7 +21,7 @@ LLMs treat one carrier as universal. Some assume the caption is serialised and r
Establish which schema versions the field is served under before changing anything about its enum. Under schema 2.0 (Microsoft's API v2.0, an explicit `$schemaversion=2.0` in the consumer contract, or another reliable context signal) the member name is the contract: keep names stable, put wording changes in `Caption`, add a value by appending a new name with an ordinal above every existing one, and retire a value through `ObsoleteState` rather than by deleting it. For a custom API that clients may still call as schema 1.0, any install of BC 17 to 23 or a caller that pins 1.0, the caption is a contract as well: change neither name nor caption in place, or publish the change as a new `APIVersion` on a new page object. A rename is out in every case: AppSourceCop AS0082 rejects it against a baseline, and dependent extensions bind to the name.
-See sample: `api-enum-values-are-a-contract-by-name-not-ordinal.good.al`.
+See sample: [`api-enum-values-are-a-contract-by-name-not-ordinal.good.al`](api-enum-values-are-a-contract-by-name-not-ordinal.good.al).
## Anti Pattern
@@ -31,7 +31,7 @@ Detection signal: a diff hunk that changes the name in a `value(...)` line while
The mirror image is a review defect: suppressing a caption-change finding because "the API serialises names". That holds only under schema 2.0. Do not flag a `Caption` change when the reviewer can establish schema 2.0 for every consumer; on a custom API where clients may select schema 1.0, report a caption change on an exposed value as a consumer-visible change and ask for versioning. A value appended at the end changes no contract under either schema and is never a finding.
-See sample: `api-enum-values-are-a-contract-by-name-not-ordinal.bad.al`.
+See sample: [`api-enum-values-are-a-contract-by-name-not-ordinal.bad.al`](api-enum-values-are-a-contract-by-name-not-ordinal.bad.al).
## See also
diff --git a/microsoft/knowledge/web-services/disable-write-operations-on-read-only-api-pages.md b/microsoft/knowledge/web-services/disable-write-operations-on-read-only-api-pages.md
index 2358a6b..8f6a73c 100644
--- a/microsoft/knowledge/web-services/disable-write-operations-on-read-only-api-pages.md
+++ b/microsoft/knowledge/web-services/disable-write-operations-on-read-only-api-pages.md
@@ -17,10 +17,10 @@ An API meant purely for reading โ a reporting or lookup endpoint โ is not re
For a read-only / reporting API page set all three CRUD guards off โ `InsertAllowed = false`, `ModifyAllowed = false`, `DeleteAllowed = false` โ and mark the page `Editable = false`. The endpoint then serves GET requests and rejects any insert, modify, or delete, matching the read-only contract regardless of the caller. Make the read-only stance explicit rather than depending on the writable default.
-See sample: `disable-write-operations-on-read-only-api-pages.good.al`.
+See sample: [`disable-write-operations-on-read-only-api-pages.good.al`](disable-write-operations-on-read-only-api-pages.good.al).
## Anti Pattern
An API intended for read-only consumption that omits the CRUD guards, leaving `InsertAllowed`, `ModifyAllowed`, and `DeleteAllowed` at their writable defaults. The endpoint silently accepts POST, PATCH, and DELETE, so a client can mutate or remove data the API was never meant to expose for writing. The detection signal: a read-only/reporting `PageType = API` page that does not set the three `*Allowed = false` properties.
-See sample: `disable-write-operations-on-read-only-api-pages.bad.al`.
+See sample: [`disable-write-operations-on-read-only-api-pages.bad.al`](disable-write-operations-on-read-only-api-pages.bad.al).
diff --git a/microsoft/knowledge/web-services/expose-only-committed-data-from-api-reads.md b/microsoft/knowledge/web-services/expose-only-committed-data-from-api-reads.md
index 739b5aa..4337b41 100644
--- a/microsoft/knowledge/web-services/expose-only-committed-data-from-api-reads.md
+++ b/microsoft/knowledge/web-services/expose-only-committed-data-from-api-reads.md
@@ -17,10 +17,10 @@ This is about the data-consistency contract of an API endpoint: what a consumer
For an API page that must expose only committed data, set the endpoint's read isolation once as the page opens: in the `OnOpenPage` trigger write `Rec.ReadIsolation := IsolationLevel::ReadCommitted;`. Every read the endpoint then serves ignores uncommitted writes from concurrent transactions, so a consumer never receives a row that another transaction might still roll back.
-See sample: `expose-only-committed-data-from-api-reads.good.al`.
+See sample: [`expose-only-committed-data-from-api-reads.good.al`](expose-only-committed-data-from-api-reads.good.al).
## Anti Pattern
An API intended to return committed-only data that sets no isolation level, leaving reads at the default that can observe in-flight, uncommitted writes. A consumer can fetch a row created by a concurrent transaction that is later rolled back โ a dirty read that surfaces data which never durably existed. The detection signal: a committed-only read API with no `Rec.ReadIsolation := IsolationLevel::ReadCommitted` in `OnOpenPage`.
-See sample: `expose-only-committed-data-from-api-reads.bad.al`.
+See sample: [`expose-only-committed-data-from-api-reads.bad.al`](expose-only-committed-data-from-api-reads.bad.al).
diff --git a/microsoft/knowledge/web-services/expose-operations-as-bound-actions.md b/microsoft/knowledge/web-services/expose-operations-as-bound-actions.md
index 7f0ed87..18908a7 100644
--- a/microsoft/knowledge/web-services/expose-operations-as-bound-actions.md
+++ b/microsoft/knowledge/web-services/expose-operations-as-bound-actions.md
@@ -17,10 +17,10 @@ An API consumer that needs to *do* something to a record โ post it, ship it, r
Declare the operation as `[ServiceEnabled] procedure Post(var ActionContext: WebServiceActionContext)` on the API page. Inside, perform the operation against `Rec`, then call a `SetActionResponse` helper that writes the result โ the bound record and its id โ back into the `WebServiceActionContext` so the caller receives a well-formed response. The operation is now an explicit, named endpoint action separate from ordinary field writes.
-See sample: `expose-operations-as-bound-actions.good.al`.
+See sample: [`expose-operations-as-bound-actions.good.al`](expose-operations-as-bound-actions.good.al).
## Anti Pattern
Exposing a writable Boolean (for example `posted`) whose `OnValidate` performs the posting. A client that PATCHes the field to `true` โ an action indistinguishable from any other data edit โ silently triggers a side-effecting business operation. The detection signal: an API page field whose `OnValidate` posts, ships, or releases, instead of a `[ServiceEnabled]` bound action.
-See sample: `expose-operations-as-bound-actions.bad.al`.
+See sample: [`expose-operations-as-bound-actions.bad.al`](expose-operations-as-bound-actions.bad.al).
diff --git a/microsoft/knowledge/web-services/expose-systemid-as-the-api-key.md b/microsoft/knowledge/web-services/expose-systemid-as-the-api-key.md
index 93d2dcd..f34e9a4 100644
--- a/microsoft/knowledge/web-services/expose-systemid-as-the-api-key.md
+++ b/microsoft/knowledge/web-services/expose-systemid-as-the-api-key.md
@@ -17,10 +17,10 @@ Every BC table carries a `SystemId` โ an immutable GUID assigned at insert and
Set `ODataKeyFields = SystemId` so OData routes records by the stable GUID, and expose it as `field(id; Rec.SystemId)` marked `Editable = false`. Clients then address a record at `.../customers()`, an identity that survives any rename of the business key. Keep the business key (for example `No.`) as an ordinary exposed field, not as the OData key.
-See sample: `expose-systemid-as-the-api-key.good.al`.
+See sample: [`expose-systemid-as-the-api-key.good.al`](expose-systemid-as-the-api-key.good.al).
## Anti Pattern
Setting `ODataKeyFields = "No."` so the endpoint addresses records by a renamable business field. As soon as a user changes that `No.`, every external reference built on the old value points at nothing, silently breaking integrations. The detection signal: `ODataKeyFields` set to a business field rather than `SystemId`, or an API page that exposes no `id` field bound to `Rec.SystemId`.
-See sample: `expose-systemid-as-the-api-key.bad.al`.
+See sample: [`expose-systemid-as-the-api-key.bad.al`](expose-systemid-as-the-api-key.bad.al).
diff --git a/microsoft/knowledge/web-services/link-api-parts-on-systemid-and-set-multiplicity.md b/microsoft/knowledge/web-services/link-api-parts-on-systemid-and-set-multiplicity.md
index 4cf81d6..2f92c0c 100644
--- a/microsoft/knowledge/web-services/link-api-parts-on-systemid-and-set-multiplicity.md
+++ b/microsoft/knowledge/web-services/link-api-parts-on-systemid-and-set-multiplicity.md
@@ -17,13 +17,13 @@ application-area: [all]
Define the child foreign key as `Guid` with a `TableRelation` to the parent table's `SystemId`, then use `SubPageLink = "" = Field(SystemId)` on the parent API page. A child collection may omit `Multiplicity` and rely on the default 1:N relationship, or declare `Multiplicity = Many` explicitly. Set `Multiplicity = ZeroOrOne` when the intended navigation metadata is a singleton.
-See sample: `link-api-parts-on-systemid-and-set-multiplicity.good.al`.
+See sample: [`link-api-parts-on-systemid-and-set-multiplicity.good.al`](link-api-parts-on-systemid-and-set-multiplicity.good.al).
## Anti Pattern
On a parent API with `ODataKeyFields = SystemId`, linking a child business field such as `"Order No."` to the parent's `"No."` creates a second identity scheme for navigation instead of using the contract's stable GUID. A separate defect is an explicit `Multiplicity` that conflicts with the intended shape, such as `ZeroOrOne` on an order-lines collection or `Many` on a singleton. Do not treat omission alone as a defect: it is valid for a collection because the default is 1:N, while an intended singleton must explicitly use `Multiplicity = ZeroOrOne`.
-See sample: `link-api-parts-on-systemid-and-set-multiplicity.bad.al`.
+See sample: [`link-api-parts-on-systemid-and-set-multiplicity.bad.al`](link-api-parts-on-systemid-and-set-multiplicity.bad.al).
## Source
diff --git a/microsoft/knowledge/web-services/set-required-api-page-properties.md b/microsoft/knowledge/web-services/set-required-api-page-properties.md
index 496db1a..24edc5d 100644
--- a/microsoft/knowledge/web-services/set-required-api-page-properties.md
+++ b/microsoft/knowledge/web-services/set-required-api-page-properties.md
@@ -17,10 +17,10 @@ An API page needs `APIPublisher`, `APIGroup`, `EntityName`, `EntitySetName`, and
Declare the five routing/entity properties required by the API page and set `APIVersion` explicitly for a stable published contract, for example `'v1.0'`. Expose the record's fields inside a repeater under `area(content)`. Review missing routing metadata as a malformed API definition, but review a missing `APIVersion` as unintended publication under `beta`, not as an unpublished endpoint.
-See sample: `set-required-api-page-properties.good.al`.
+See sample: [`set-required-api-page-properties.good.al`](set-required-api-page-properties.good.al).
## Anti Pattern
Leaving out `APIPublisher`, `APIGroup`, `EntityName`, `EntitySetName`, or `SourceTable` leaves the API definition incomplete. A subtler contract defect is declaring all of those but omitting `APIVersion`: the page is exposed as `beta`, which is valid runtime behavior but not the explicit stable route a production client expects.
-See sample: `set-required-api-page-properties.bad.al`.
+See sample: [`set-required-api-page-properties.bad.al`](set-required-api-page-properties.bad.al).
diff --git a/microsoft/knowledge/web-services/version-apis-by-adding-not-mutating-published-versions.md b/microsoft/knowledge/web-services/version-apis-by-adding-not-mutating-published-versions.md
index bfd2c9f..1e8417b 100644
--- a/microsoft/knowledge/web-services/version-apis-by-adding-not-mutating-published-versions.md
+++ b/microsoft/knowledge/web-services/version-apis-by-adding-not-mutating-published-versions.md
@@ -17,10 +17,10 @@ Once an API version is published, external clients depend on its exact shape โ
Keep the existing page object and its `APIVersion = 'v1.0'` contract unchanged. Copy the page to a new object ID, set that object's `APIVersion = 'v2.0'`, and make the v2-only shape changes there. A multi-value `APIVersion` list is appropriate only when the exact same page shape is supported under each listed version.
-See sample: `version-apis-by-adding-not-mutating-published-versions.good.al`.
+See sample: [`version-apis-by-adding-not-mutating-published-versions.good.al`](version-apis-by-adding-not-mutating-published-versions.good.al).
## Anti Pattern
Editing the published `v1.0` page in place breaks its clients. So does adding `v2.0` to that same page and assuming subsequent field changes apply only to v2: both routes use one object shape. The detection signal is a breaking shape change without a separate API page object retaining the old version.
-See sample: `version-apis-by-adding-not-mutating-published-versions.bad.al`.
+See sample: [`version-apis-by-adding-not-mutating-published-versions.bad.al`](version-apis-by-adding-not-mutating-published-versions.bad.al).
diff --git a/microsoft/knowledge/web-services/webhook-eligibility-and-validationtoken-renewal.md b/microsoft/knowledge/web-services/webhook-eligibility-and-validationtoken-renewal.md
index b35a05c..2aad620 100644
--- a/microsoft/knowledge/web-services/webhook-eligibility-and-validationtoken-renewal.md
+++ b/microsoft/knowledge/web-services/webhook-eligibility-and-validationtoken-renewal.md
@@ -17,13 +17,13 @@ Business Central can subscribe only to eligible API pages, not every endpoint th
Before creating a subscription, confirm the resource appears in `webhookSupportedResources` and that a custom endpoint is an API page with a single stable key over an eligible persistent table. Use one validation path that echoes `validationToken` for both create (`POST`) and renew (`PATCH`) handshakes. Track `expirationDateTime` and renew before expiry: online subscriptions expire after three days, while on-premises lifetime defaults to three days and can be changed with `ApiSubscriptionExpiration`.
-See samples: `webhook-eligibility-and-validationtoken-renewal.good.al` and `webhook-eligibility-and-validationtoken-renewal.good.js`.
+See samples: [`webhook-eligibility-and-validationtoken-renewal.good.al`](webhook-eligibility-and-validationtoken-renewal.good.al) and [`webhook-eligibility-and-validationtoken-renewal.good.js`](webhook-eligibility-and-validationtoken-renewal.good.js).
## Anti Pattern
Attempting to subscribe to an API query, temporary/composite/system-table/Job Queue Entry API page, or assuming a successful create handshake makes renewal automatic. Composite includes an explicit multi-field `ODataKeyFields` and a missing `ODataKeyFields` when the source table's primary key has multiple fields. A renewal issues the same validation challenge; a notification handler that ignores the query-string token cannot create or renew the subscription.
-See samples: `webhook-eligibility-and-validationtoken-renewal.bad.al` and `webhook-eligibility-and-validationtoken-renewal.bad.js`.
+See samples: [`webhook-eligibility-and-validationtoken-renewal.bad.al`](webhook-eligibility-and-validationtoken-renewal.bad.al) and [`webhook-eligibility-and-validationtoken-renewal.bad.js`](webhook-eligibility-and-validationtoken-renewal.bad.js).
## Source
diff --git a/microsoft/skills/review/al-breaking-changes-review.md b/microsoft/skills/review/al-breaking-changes-review.md
index 767c9af..1ddd810 100644
--- a/microsoft/skills/review/al-breaking-changes-review.md
+++ b/microsoft/skills/review/al-breaking-changes-review.md
@@ -134,7 +134,7 @@ Output conforms to the DO output contract. Every finding this skill emits MUST s
}
```
-The empty-corpus case โ BCQuality's state until breaking-changes knowledge files land โ produces:
+When no applicable breaking-changes knowledge is available, the report is:
```json
{
diff --git a/microsoft/skills/review/al-code-review.md b/microsoft/skills/review/al-code-review.md
index 41f0f78..9972aca 100644
--- a/microsoft/skills/review/al-code-review.md
+++ b/microsoft/skills/review/al-code-review.md
@@ -300,7 +300,9 @@ Output conforms to the DO output contract, extended with `sub-results` and `skip
}
```
-The empty-corpus case โ BCQuality's state until knowledge files land โ rolls up to `no-knowledge`:
+When the selected leaves find no applicable knowledge, the result rolls up to
+`no-knowledge`. This example shows two leaf results; a full run includes every
+invoked leaf:
```json
{
diff --git a/microsoft/skills/review/al-error-handling-review.md b/microsoft/skills/review/al-error-handling-review.md
index bc4598a..63c4d5e 100644
--- a/microsoft/skills/review/al-error-handling-review.md
+++ b/microsoft/skills/review/al-error-handling-review.md
@@ -132,7 +132,7 @@ Output conforms to the DO output contract. Every finding this skill emits MUST s
}
```
-The empty-corpus case โ BCQuality's state until error-handling knowledge files land โ produces:
+When no applicable error-handling knowledge is available, the report is:
```json
{
diff --git a/microsoft/skills/review/al-events-review.md b/microsoft/skills/review/al-events-review.md
index 7248a45..559d036 100644
--- a/microsoft/skills/review/al-events-review.md
+++ b/microsoft/skills/review/al-events-review.md
@@ -141,7 +141,7 @@ Output conforms to the DO output contract. Every finding this skill emits MUST s
}
```
-The empty-corpus case โ BCQuality's state until events knowledge files land โ produces:
+When no applicable events knowledge is available, the report is:
```json
{
diff --git a/microsoft/skills/review/al-performance-review.md b/microsoft/skills/review/al-performance-review.md
index d4738ac..f2fa80d 100644
--- a/microsoft/skills/review/al-performance-review.md
+++ b/microsoft/skills/review/al-performance-review.md
@@ -134,7 +134,7 @@ Output conforms to the DO output contract. Every finding this skill emits MUST s
}
```
-The empty-corpus case โ BCQuality's state until performance knowledge files land โ produces:
+When no applicable performance knowledge is available, the report is:
```json
{
diff --git a/microsoft/skills/review/al-security-review.md b/microsoft/skills/review/al-security-review.md
index 5d998c9..e3e4049 100644
--- a/microsoft/skills/review/al-security-review.md
+++ b/microsoft/skills/review/al-security-review.md
@@ -129,7 +129,7 @@ Output conforms to the DO output contract. Every finding this skill emits MUST s
}
```
-The empty-corpus case โ BCQuality's state until security knowledge files land โ produces:
+When no applicable security knowledge is available, the report is:
```json
{
diff --git a/skills/do.md b/skills/do.md
index 5234437..4ac433b 100644
--- a/skills/do.md
+++ b/skills/do.md
@@ -19,7 +19,12 @@ An action skill is a single markdown file with YAML frontmatter. It lives inside
- `/community/skills/` โ community-contributed action skills.
- `/custom/skills/` โ partner or customer action skills (typically in a consumer repo, not in BCQuality itself).
-Action skills do not live at the repo root. The files in `/skills/` โ the three meta-skill contracts (READ, DO, WRITE) and the entry-point skill (`entry.md`, `kind: entry-point`) โ are the only skills that sit outside a layer. The entry-point skill structurally follows this same four-step pattern but produces a dispatch record rather than a findings-report; see `skills/entry.md` for its contract.
+Action skills do not live at the repo root. Layer-independent files in
+`/skills/` contain the three meta-skill contracts (READ, DO, WRITE), the
+entry-point skill (`entry.md`, `kind: entry-point`), and host-format adapters.
+Adapters are not action skills. Entry structurally follows the same
+four-step pattern but produces a dispatch record rather than a findings-report;
+see [entry.md](entry.md) for its contract.
## Skills hold mechanics; knowledge files hold BC facts
@@ -319,15 +324,16 @@ A super-skill's top-level `suppressed[]` remains knowledge-file-only and is typi
## Worked example
-A minimal action skill that cites applicable guidance for a changed AL file, without generating findings of its own:
+A minimal action skill that reviews a changed AL file against applicable
+guidance. Relevance alone never produces a finding:
```yaml
---
kind: action-skill
-id: cite-applicable-guidance
+id: review-applicable-guidance
version: 1
-title: Cite applicable guidance
-description: Lists knowledge files relevant to a changed AL file.
+title: Review applicable guidance
+description: Reviews a changed AL file against applicable knowledge.
inputs: [file-path]
outputs: [findings-report]
technologies: [al]
@@ -345,7 +351,12 @@ Filter by `technologies: [al]` and `bc-version` matching the target environment.
Intersect `keywords` with tokens derived from the target file's object name and changed members.
## Action
-For each worklist entry, emit one finding with severity `info`, a message naming the concern, and a reference object pointing to the knowledge file.
+Read each worklisted article in full and compare its normative guidance to the
+input. Emit a finding only for a concrete violation or an observation the
+article explicitly defines, with justified severity, evidence, and a reference
+copied from the discovered article path. Do not report an article merely
+because it was relevant. If every item was evaluated and none warrants a
+finding, return `completed` with an empty `findings` array.
## Output
Conforms to the DO output contract.
diff --git a/skills/read.md b/skills/read.md
index 6a2080d..f2e9c1e 100644
--- a/skills/read.md
+++ b/skills/read.md
@@ -7,7 +7,9 @@ title: Schema + Use โ how to read a knowledge file
# READ
-Every consumer of BCQuality โ an agent, an action skill, a human reviewer โ reads this file first. It defines what a knowledge file is, what fields it contains, what they mean, and how to reconcile multiple files.
+Read this contract before interpreting knowledge files. Task execution starts
+at [Entry](entry.md); READ is loaded on demand when a dispatched skill needs
+it. It defines knowledge fields, their meaning, and how to reconcile files.
This contract is stable. Changes require a PR approved by both maintainers.
@@ -131,7 +133,7 @@ Rules:
- A sample file is identified by the article's slug followed by a `..` suffix. The supported kinds are `good` and `bad`. Additional kinds MAY be introduced by a layer; consumers MUST ignore unknown kinds without failing.
- The extension matches the technology (`al`, `ps1`, `js`, `kql`, โฆ). A single article MAY carry samples in multiple technologies if the article's frontmatter `technologies` lists them.
-- Articles MAY have a `good` sample only, a `bad` sample only, both, or neither. The article text SHOULD reference each sample it ships, using a relative path like `` `.good.al` ``.
+- Articles MAY have a `good` sample only, a `bad` sample only, both, or neither. The article text SHOULD reference each sample it ships with a relative Markdown link whose label retains the backticked filename, like `` [`.good.al`](.good.al) ``.
- Samples are **demonstration-only**. They are not deployed, not compiled as part of a published app, and not derived from the Business Central base application source. Each sample is self-contained and exists purely to make the accompanying article concrete for humans and agents.
- Layer precedence applies to sample files the same way it applies to articles: a `/custom/knowledge//.good.al` overrides a `/microsoft/knowledge//.good.al` for the same article in the same layer hierarchy.
diff --git a/skills/write.md b/skills/write.md
index 8096f1a..c2edab5 100644
--- a/skills/write.md
+++ b/skills/write.md
@@ -16,7 +16,7 @@ Before authoring anything, confirm a knowledge file is the right artifact. BCQua
- **Skills** (`*/skills/**`) hold only finder/applier mechanics โ how to discover, filter, worklist, and emit findings. See `skills/do.md`.
- **Knowledge files** (`*/knowledge/**`) hold every Business-Central-specific fact a skill acts on.
-A new BC fact is therefore a knowledge file, never a skill edit. In particular, if you arrived here because a review agent flagged something it should not have (a false positive) or missed something it should have caught, the remedy is a knowledge file โ apply the admission test in the [README](../README.md#what-belongs-here): *would a capable LLM get this wrong without the file?* If you find yourself editing a skill to stop it flagging something, stop and write a knowledge file instead.
+A new BC fact is therefore a knowledge file, never a skill edit. In particular, if you arrived here because a review agent flagged something it should not have (a false positive) or missed something it should have caught, the remedy is a knowledge file โ apply the [admission test](../docs/contributing.md#what-belongs-here): *would a capable LLM get this wrong without the file?* If you find yourself editing a skill to stop it flagging something, stop and write a knowledge file instead.
### Negative knowledge is first-class
@@ -56,6 +56,13 @@ Target under 100 lines. Ideal under 50. Long files almost always mean two concer
Custom `##` sections are permitted when they serve the concern (for example, `## Applies to` for scope caveats or `## See also` for related files). Consumers are not required to understand them, so do not put load-bearing content there.
+When adding or changing a platform claim, cite an authoritative public source
+where available. A short `## References` section can link the relevant API,
+property documentation, or public source definition. If no such source is
+available, identify the evidence or policy basis explicitly; do not imply an
+official guarantee. Keep the actual rule and its exceptions in normative
+sections, not only in references. See [sources and examples](../docs/contributing.md#sources-and-examples).
+
## No fenced code blocks
Knowledge files do not contain code. Samples live as **sibling files** next to the article โ `.good.al`, `.bad.al`, etc. โ in the same knowledge-layer folder. See `skills/read.md` for the full convention. This keeps knowledge files retrieval-friendly and prevents code from drifting out of sync with BC platform changes buried inside prose.
@@ -93,7 +100,7 @@ The `/custom/` layer is **empty by default** in the upstream `microsoft/BCQualit
Before authoring or scaffolding any file under `/custom/knowledge/` or `/custom/skills/`, an author โ human or agent โ MUST confirm the working repository is **not** `microsoft/BCQuality`:
- Check the `origin` remote: `git remote get-url origin`. If it points at `github.com/microsoft/BCQuality`, stop โ you are in the upstream repo, not a fork.
-- If you are in the upstream repo, do not write the file. Either fork the repository (or clone it into your organization's own repo) and add the custom content there, or โ if the guidance is genuinely shareable โ author it in `/community/knowledge/` instead.
+- If you are in the upstream repo, do not write the custom file. Either fork the repository (or clone it into your organization's own repo) and add the custom content there, or โ if the guidance is genuinely shareable โ use the shared layer that owns the domain, following *Choosing a layer* above. Community is not a staging area for Microsoft-owned domains.
A pull request that adds `/custom/` content to `microsoft/BCQuality` will be **automatically closed** by the `Guard custom layer` workflow. Validate the fork precondition first so authoring effort is not wasted on a PR that cannot be merged.
@@ -109,7 +116,8 @@ Before opening a pull request:
- Frontmatter `domain` exactly matches the containing domain folder.
- File is in the correct layer and domain folder.
- Name is kebab-case and descriptive.
-- Every companion sample is referenced by filename from the article, and every referenced sample exists.
+- Every companion sample has a clickable relative link retaining its backticked filename, and every referenced sample exists.
+- Platform claims link supporting sources where available; policy or empirical guidance is identified as such.
- Every review-leaf domain has at least one article with both `.good.al` and `.bad.al` companions; the evaluation harness derives positive and clean controls from that convention automatically.
Agents scaffolding new files SHOULD run this checklist programmatically before emitting the file.