Fix knowledge corpus integrity issues (#87)

Repair broken knowledge references and align review examples with canonical articles. Correct explicit version gates, restore a missing title, and recognize the plugin directory in the root guard.

Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Jesper Schulz-Wedde 2026-07-13 10:34:23 +02:00 committed by GitHub
parent 3aa3581f95
commit 34c931e1c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 58 additions and 65 deletions

View file

@ -40,7 +40,7 @@ jobs:
// Known, intended repository root. Anything else added at the root // Known, intended repository root. Anything else added at the root
// is flagged for a human to eyeball. // is flagged for a human to eyeball.
const ALLOWED_DIRS = new Set([ const ALLOWED_DIRS = new Set([
'.github', 'community', 'custom', 'microsoft', 'skills', 'tools', '.claude-plugin', '.github', 'community', 'custom', 'microsoft', 'skills', 'tools',
]); ]);
const ALLOWED_FILES = new Set([ const ALLOWED_FILES = new Set([
'.gitignore', 'CODEOWNERS', 'LICENSE', 'README.md', '.gitignore', 'CODEOWNERS', 'LICENSE', 'README.md',

View file

@ -19,8 +19,6 @@ Entitlements are license-level caps on what a user can access, derived automatic
When designing a permission set that ships with an extension, consult the entitlement model for the target user population before finalizing the grants. Every object and tabledata right the set expects to grant should be reachable within the intended entitlement tier; if it is not, the set needs to be scoped to licenses that permit it, or the feature needs a different access path. When designing a permission set that ships with an extension, consult the entitlement model for the target user population before finalizing the grants. Every object and tabledata right the set expects to grant should be reachable within the intended entitlement tier; if it is not, the set needs to be scoped to licenses that permit it, or the feature needs a different access path.
See sample: `do-not-grant-rights-beyond-a-users-entitlement.good.al`.
## Anti Pattern ## Anti Pattern
Authoring permission sets in a sandbox with full-license context and shipping them without verifying which entitlement tier customer users actually hold. The sets look complete in test; on a real customer they silently lose rights at runtime and the symptom is "the feature does not work for some users" with no obvious authorization error. Authoring permission sets in a sandbox with full-license context and shipping them without verifying which entitlement tier customer users actually hold. The sets look complete in test; on a real customer they silently lose rights at runtime and the symptom is "the feature does not work for some users" with no obvious authorization error.

View file

@ -1,5 +1,5 @@
--- ---
bc-version: [all] bc-version: [21..]
domain: style domain: style
keywords: [abouttitle, abouttext, teaching-tip, onboarding, page] keywords: [abouttitle, abouttext, teaching-tip, onboarding, page]
technologies: [al] technologies: [al]

View file

@ -1,5 +1,5 @@
--- ---
bc-version: [all] bc-version: [25..]
domain: style domain: style
keywords: [this, codeunit, self-reference, aa0248, scope] keywords: [this, codeunit, self-reference, aa0248, scope]
technologies: [al] technologies: [al]

View file

@ -7,6 +7,8 @@ countries: [w1]
application-area: [all] application-area: [all]
--- ---
# Preserve list scope after `SetSelectionFilter`
## Description ## Description
`CurrPage.SetSelectionFilter(Rec)` behaves differently depending on whether the user explicitly multi-selected rows. When no rows are marked — the cursor is simply positioned on a row — the method writes a primary key filter for that single row and leaves `MarkedOnly` as false. When the user explicitly selected multiple rows, the method marks those records and sets `MarkedOnly` to true. A batch action that calls `SetSelectionFilter` and then passes the record directly to a processing codeunit will therefore silently restrict to one row whenever the user has not made an explicit selection, which is almost never the intended behaviour for an action labelled "Verify All" or "Post All". `CurrPage.SetSelectionFilter(Rec)` behaves differently depending on whether the user explicitly multi-selected rows. When no rows are marked — the cursor is simply positioned on a row — the method writes a primary key filter for that single row and leaves `MarkedOnly` as false. When the user explicitly selected multiple rows, the method marks those records and sets `MarkedOnly` to true. A batch action that calls `SetSelectionFilter` and then passes the record directly to a processing codeunit will therefore silently restrict to one row whenever the user has not made an explicit selection, which is almost never the intended behaviour for an action labelled "Verify All" or "Post All".

View file

@ -1,5 +1,5 @@
--- ---
bc-version: [all] bc-version: [21..]
domain: upgrade domain: upgrade
keywords: [datatransfer, large-dataset, bulk-update, modifyall, copyfields, new-field] keywords: [datatransfer, large-dataset, bulk-update, modifyall, copyfields, new-field]
technologies: [al] technologies: [al]

View file

@ -1,5 +1,5 @@
--- ---
bc-version: [all] bc-version: [21..]
domain: upgrade domain: upgrade
keywords: [datatransfer, validate-trigger, event-subscriber, side-effects, business-logic] keywords: [datatransfer, validate-trigger, event-subscriber, side-effects, business-logic]
technologies: [al] technologies: [al]

View file

@ -127,36 +127,36 @@ Output conforms to the DO output contract, extended with `sub-results` and `skip
}, },
"findings": [ "findings": [
{ {
"id": "microsoft/knowledge/performance/filter-before-find.md", "id": "microsoft/knowledge/performance/apply-filters-before-iterating.md",
"severity": "major", "severity": "major",
"message": "FindSet is called on a record variable without any prior SetRange/SetFilter. This forces a full-table scan.", "message": "The Country/Region Code predicate is evaluated inside the loop instead of with SetRange before FindSet, so every row crosses the database boundary.",
"location": { "location": {
"file": "src/Sales/PostingRoutines.Codeunit.al", "file": "src/Sales/PostingRoutines.Codeunit.al",
"line": 140, "line": 140,
"range": { "start-line": 140, "end-line": 144 } "range": { "start-line": 140, "end-line": 144 }
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/performance/filter-before-find.md" } { "path": "microsoft/knowledge/performance/apply-filters-before-iterating.md" }
], ],
"confidence": "high", "confidence": "high",
"from-sub-skill": "al-performance-review" "from-sub-skill": "al-performance-review"
}, },
{ {
"id": "community/knowledge/performance/call-setloadfields-before-filters.md", "id": "community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.md",
"severity": "minor", "severity": "minor",
"message": "SetLoadFields is called after SetRange. Per the referenced guidance the call must come before filters to be folded into the query plan.", "message": "The loop reads an unlisted field after SetLoadFields, triggering a hidden JIT load for each record passed by value.",
"location": { "location": {
"file": "src/Sales/PostingRoutines.Codeunit.al", "file": "src/Sales/PostingRoutines.Codeunit.al",
"line": 152 "line": 152
}, },
"references": [ "references": [
{ "path": "community/knowledge/performance/call-setloadfields-before-filters.md" } { "path": "community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.md" }
], ],
"confidence": "high", "confidence": "high",
"from-sub-skill": "al-performance-review" "from-sub-skill": "al-performance-review"
}, },
{ {
"id": "microsoft/knowledge/security/use-secrettext-for-credentials.md", "id": "microsoft/knowledge/security/secrettext-for-credentials.md",
"severity": "blocker", "severity": "blocker",
"message": "A bearer token is declared as a Text parameter and passed through the HTTP request path as plain text. The referenced guidance requires credentials to flow as SecretText end-to-end.", "message": "A bearer token is declared as a Text parameter and passed through the HTTP request path as plain text. The referenced guidance requires credentials to flow as SecretText end-to-end.",
"location": { "location": {
@ -165,21 +165,21 @@ Output conforms to the DO output contract, extended with `sub-results` and `skip
"range": { "start-line": 85, "end-line": 89 } "range": { "start-line": 85, "end-line": 89 }
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/security/use-secrettext-for-credentials.md" } { "path": "microsoft/knowledge/security/secrettext-for-credentials.md" }
], ],
"confidence": "high", "confidence": "high",
"from-sub-skill": "al-security-review" "from-sub-skill": "al-security-review"
}, },
{ {
"id": "microsoft/knowledge/security/never-hardcode-secrets-in-al.md", "id": "community/knowledge/security/secrets-isolated-storage.md",
"severity": "minor", "severity": "minor",
"message": "An API key is assigned from a string literal rather than retrieved from IsolatedStorage or Key Vault at runtime.", "message": "A setup table stores an API key in an ordinary Text field, exposing it through table reads and exports. Persist it in IsolatedStorage instead.",
"location": { "location": {
"file": "src/Integration/ApiClient.Codeunit.al", "file": "src/Integration/ExternalServiceSetup.Table.al",
"line": 201 "line": 12
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/security/never-hardcode-secrets-in-al.md" } { "path": "community/knowledge/security/secrets-isolated-storage.md" }
], ],
"confidence": "medium", "confidence": "medium",
"from-sub-skill": "al-security-review" "from-sub-skill": "al-security-review"
@ -209,29 +209,29 @@ Output conforms to the DO output contract, extended with `sub-results` and `skip
}, },
"findings": [ "findings": [
{ {
"id": "microsoft/knowledge/performance/filter-before-find.md", "id": "microsoft/knowledge/performance/apply-filters-before-iterating.md",
"severity": "major", "severity": "major",
"message": "FindSet is called on a record variable without any prior SetRange/SetFilter. This forces a full-table scan.", "message": "The Country/Region Code predicate is evaluated inside the loop instead of with SetRange before FindSet, so every row crosses the database boundary.",
"location": { "location": {
"file": "src/Sales/PostingRoutines.Codeunit.al", "file": "src/Sales/PostingRoutines.Codeunit.al",
"line": 140, "line": 140,
"range": { "start-line": 140, "end-line": 144 } "range": { "start-line": 140, "end-line": 144 }
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/performance/filter-before-find.md" } { "path": "microsoft/knowledge/performance/apply-filters-before-iterating.md" }
], ],
"confidence": "high" "confidence": "high"
}, },
{ {
"id": "community/knowledge/performance/call-setloadfields-before-filters.md", "id": "community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.md",
"severity": "minor", "severity": "minor",
"message": "SetLoadFields is called after SetRange. Per the referenced guidance the call must come before filters to be folded into the query plan.", "message": "The loop reads an unlisted field after SetLoadFields, triggering a hidden JIT load for each record passed by value.",
"location": { "location": {
"file": "src/Sales/PostingRoutines.Codeunit.al", "file": "src/Sales/PostingRoutines.Codeunit.al",
"line": 152 "line": 152
}, },
"references": [ "references": [
{ "path": "community/knowledge/performance/call-setloadfields-before-filters.md" } { "path": "community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.md" }
], ],
"confidence": "high" "confidence": "high"
} }
@ -247,7 +247,7 @@ Output conforms to the DO output contract, extended with `sub-results` and `skip
}, },
"findings": [ "findings": [
{ {
"id": "microsoft/knowledge/security/use-secrettext-for-credentials.md", "id": "microsoft/knowledge/security/secrettext-for-credentials.md",
"severity": "blocker", "severity": "blocker",
"message": "A bearer token is declared as a Text parameter and passed through the HTTP request path as plain text. The referenced guidance requires credentials to flow as SecretText end-to-end.", "message": "A bearer token is declared as a Text parameter and passed through the HTTP request path as plain text. The referenced guidance requires credentials to flow as SecretText end-to-end.",
"location": { "location": {
@ -256,20 +256,20 @@ Output conforms to the DO output contract, extended with `sub-results` and `skip
"range": { "start-line": 85, "end-line": 89 } "range": { "start-line": 85, "end-line": 89 }
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/security/use-secrettext-for-credentials.md" } { "path": "microsoft/knowledge/security/secrettext-for-credentials.md" }
], ],
"confidence": "high" "confidence": "high"
}, },
{ {
"id": "microsoft/knowledge/security/never-hardcode-secrets-in-al.md", "id": "community/knowledge/security/secrets-isolated-storage.md",
"severity": "minor", "severity": "minor",
"message": "An API key is assigned from a string literal rather than retrieved from IsolatedStorage or Key Vault at runtime.", "message": "A setup table stores an API key in an ordinary Text field, exposing it through table reads and exports. Persist it in IsolatedStorage instead.",
"location": { "location": {
"file": "src/Integration/ApiClient.Codeunit.al", "file": "src/Integration/ExternalServiceSetup.Table.al",
"line": 201 "line": 12
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/security/never-hardcode-secrets-in-al.md" } { "path": "community/knowledge/security/secrets-isolated-storage.md" }
], ],
"confidence": "medium" "confidence": "medium"
} }
@ -310,4 +310,3 @@ The empty-corpus case — BCQuality's state until knowledge files land — rolls
] ]
} }
``` ```

View file

@ -89,29 +89,29 @@ Output conforms to the DO output contract. A populated example:
}, },
"findings": [ "findings": [
{ {
"id": "microsoft/knowledge/performance/filter-before-find.md", "id": "microsoft/knowledge/performance/apply-filters-before-iterating.md",
"severity": "major", "severity": "major",
"message": "FindSet is called on a record variable without any prior SetRange/SetFilter. This forces a full-table scan.", "message": "The Country/Region Code predicate is evaluated inside the loop instead of with SetRange before FindSet, so every row crosses the database boundary.",
"location": { "location": {
"file": "src/Sales/PostingRoutines.Codeunit.al", "file": "src/Sales/PostingRoutines.Codeunit.al",
"line": 140, "line": 140,
"range": { "start-line": 140, "end-line": 144 } "range": { "start-line": 140, "end-line": 144 }
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/performance/filter-before-find.md" } { "path": "microsoft/knowledge/performance/apply-filters-before-iterating.md" }
], ],
"confidence": "high" "confidence": "high"
}, },
{ {
"id": "community/knowledge/performance/call-setloadfields-before-filters.md", "id": "community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.md",
"severity": "minor", "severity": "minor",
"message": "SetLoadFields is called after SetRange. Per the referenced guidance the call must come before filters to be folded into the query plan.", "message": "The loop reads an unlisted field after SetLoadFields, triggering a hidden JIT load for each record passed by value.",
"location": { "location": {
"file": "src/Sales/PostingRoutines.Codeunit.al", "file": "src/Sales/PostingRoutines.Codeunit.al",
"line": 152 "line": 152
}, },
"references": [ "references": [
{ "path": "community/knowledge/performance/call-setloadfields-before-filters.md" } { "path": "community/knowledge/performance/setloadfields-unlisted-field-triggers-jit-load.md" }
], ],
"confidence": "high" "confidence": "high"
} }
@ -134,4 +134,3 @@ The empty-corpus case — BCQuality's state until performance knowledge files la
"suppressed": [] "suppressed": []
} }
``` ```

View file

@ -89,16 +89,16 @@ Output conforms to the DO output contract. A populated example:
}, },
"findings": [ "findings": [
{ {
"id": "microsoft/knowledge/privacy/strsubstno-prebuild-breaks-error-telemetry-classification.md", "id": "microsoft/knowledge/privacy/data-classification-required-on-pii-fields.md",
"severity": "major", "severity": "major",
"message": "Error receives a pre-built Text produced by StrSubstNo with customer name and email as arguments. Per the referenced guidance the platform cannot classify or strip PII from an opaque Text and will export the full message to telemetry.", "message": "The new Customer E-Mail table field has no DataClassification property, leaving personal data unclassified.",
"location": { "location": {
"file": "src/Sales/CustomerValidation.Codeunit.al", "file": "src/Sales/Customer.TableExt.al",
"line": 64, "line": 64,
"range": { "start-line": 60, "end-line": 64 } "range": { "start-line": 60, "end-line": 64 }
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/privacy/strsubstno-prebuild-breaks-error-telemetry-classification.md" } { "path": "microsoft/knowledge/privacy/data-classification-required-on-pii-fields.md" }
], ],
"confidence": "high" "confidence": "high"
} }
@ -106,4 +106,3 @@ Output conforms to the DO output contract. A populated example:
"suppressed": [] "suppressed": []
} }
``` ```

View file

@ -89,7 +89,7 @@ Output conforms to the DO output contract. A populated example:
}, },
"findings": [ "findings": [
{ {
"id": "microsoft/knowledge/security/use-secrettext-for-credentials.md", "id": "microsoft/knowledge/security/secrettext-for-credentials.md",
"severity": "blocker", "severity": "blocker",
"message": "A bearer token is declared as a Text parameter and passed through the HTTP request path as plain text. The referenced guidance requires credentials to flow as SecretText end-to-end.", "message": "A bearer token is declared as a Text parameter and passed through the HTTP request path as plain text. The referenced guidance requires credentials to flow as SecretText end-to-end.",
"location": { "location": {
@ -98,20 +98,20 @@ Output conforms to the DO output contract. A populated example:
"range": { "start-line": 85, "end-line": 89 } "range": { "start-line": 85, "end-line": 89 }
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/security/use-secrettext-for-credentials.md" } { "path": "microsoft/knowledge/security/secrettext-for-credentials.md" }
], ],
"confidence": "high" "confidence": "high"
}, },
{ {
"id": "microsoft/knowledge/security/never-hardcode-secrets-in-al.md", "id": "community/knowledge/security/secrets-isolated-storage.md",
"severity": "minor", "severity": "minor",
"message": "An API key is assigned from a string literal rather than retrieved from IsolatedStorage or Key Vault at runtime.", "message": "A setup table stores an API key in an ordinary Text field, exposing it through table reads and exports. Persist it in IsolatedStorage instead.",
"location": { "location": {
"file": "src/Integration/ApiClient.Codeunit.al", "file": "src/Integration/ExternalServiceSetup.Table.al",
"line": 201 "line": 12
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/security/never-hardcode-secrets-in-al.md" } { "path": "community/knowledge/security/secrets-isolated-storage.md" }
], ],
"confidence": "medium" "confidence": "medium"
} }
@ -134,4 +134,3 @@ The empty-corpus case — BCQuality's state until security knowledge files land
"suppressed": [] "suppressed": []
} }
``` ```

View file

@ -87,7 +87,7 @@ Output conforms to the DO output contract. A populated example:
}, },
"findings": [ "findings": [
{ {
"id": "microsoft/knowledge/style/apply-approved-label-suffixes.md", "id": "microsoft/knowledge/style/label-suffix-approved-list.md",
"severity": "minor", "severity": "minor",
"message": "A Label named Text000 has no approved suffix (Msg/Err/Qst/Tok/Lbl/Txt). Per the referenced CodeCop AA0074 guidance, every Label and TextConst carries a suffix indicating its consuming call.", "message": "A Label named Text000 has no approved suffix (Msg/Err/Qst/Tok/Lbl/Txt). Per the referenced CodeCop AA0074 guidance, every Label and TextConst carries a suffix indicating its consuming call.",
"location": { "location": {
@ -95,7 +95,7 @@ Output conforms to the DO output contract. A populated example:
"line": 42 "line": 42
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/style/apply-approved-label-suffixes.md" } { "path": "microsoft/knowledge/style/label-suffix-approved-list.md" }
], ],
"confidence": "high" "confidence": "high"
} }
@ -103,4 +103,3 @@ Output conforms to the DO output contract. A populated example:
"suppressed": [] "suppressed": []
} }
``` ```

View file

@ -87,15 +87,15 @@ Output conforms to the DO output contract. A populated example:
}, },
"findings": [ "findings": [
{ {
"id": "microsoft/knowledge/ui/field-tooltips-start-with-specifies-and-end-with-period.md", "id": "microsoft/knowledge/ui/show-caption-on-editable-fields.md",
"severity": "minor", "severity": "minor",
"message": "Field ToolTip is a fragment ('Customer name') — missing the 'Specifies' opener and the terminating period the house-style guidance requires.", "message": "An editable page field sets ShowCaption = false, removing the visible and accessible label. Leave ShowCaption enabled or use a documented exception pattern.",
"location": { "location": {
"file": "src/Sales/CustomerCard.Page.al", "file": "src/Sales/CustomerCard.Page.al",
"line": 58 "line": 58
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/ui/field-tooltips-start-with-specifies-and-end-with-period.md" } { "path": "microsoft/knowledge/ui/show-caption-on-editable-fields.md" }
], ],
"confidence": "high" "confidence": "high"
} }
@ -103,4 +103,3 @@ Output conforms to the DO output contract. A populated example:
"suppressed": [] "suppressed": []
} }
``` ```

View file

@ -89,7 +89,7 @@ Output conforms to the DO output contract. A populated example:
}, },
"findings": [ "findings": [
{ {
"id": "microsoft/knowledge/upgrade/enum-changes-must-be-additive-at-the-end.md", "id": "microsoft/knowledge/upgrade/enum-values-additive-at-end.md",
"severity": "blocker", "severity": "blocker",
"message": "A new enum value was inserted at ordinal 1, shifting every subsequent value by one. Rows that store the old ordinal 1 will silently resolve to the new value. Per the referenced guidance, enum values must be appended at the end.", "message": "A new enum value was inserted at ordinal 1, shifting every subsequent value by one. Rows that store the old ordinal 1 will silently resolve to the new value. Per the referenced guidance, enum values must be appended at the end.",
"location": { "location": {
@ -97,7 +97,7 @@ Output conforms to the DO output contract. A populated example:
"line": 7 "line": 7
}, },
"references": [ "references": [
{ "path": "microsoft/knowledge/upgrade/enum-changes-must-be-additive-at-the-end.md" } { "path": "microsoft/knowledge/upgrade/enum-values-additive-at-end.md" }
], ],
"confidence": "high" "confidence": "high"
} }
@ -105,4 +105,3 @@ Output conforms to the DO output contract. A populated example:
"suppressed": [] "suppressed": []
} }
``` ```

View file

@ -115,7 +115,7 @@ Consumers MUST NOT silently treat missing context as a match.
## Citing a knowledge file ## Citing a knowledge file
A consumer that produces output referencing a knowledge file MUST cite it by its repo-relative path (for example, `microsoft/knowledge/performance/filter-before-find.md`). Line numbers are not stable references; use the file path only. If a commit SHA is available to the consumer, it SHOULD be included alongside the path. A consumer that produces output referencing a knowledge file MUST cite it by its repo-relative path (for example, `microsoft/knowledge/performance/apply-filters-before-iterating.md`). Line numbers are not stable references; use the file path only. If a commit SHA is available to the consumer, it SHOULD be included alongside the path.
## Sample files ## Sample files