[TEST – DO NOT MERGE] Validate BCQuality Copilot PR-review wiring#8772
[TEST – DO NOT MERGE] Validate BCQuality Copilot PR-review wiring#8772JesperSchulz wants to merge 1 commit into
Conversation
…te BCQuality wiring Replicates the exact byte-identical sample diff from microsoft/BCAppsBCQuality#47 (adds external quality result import codeunit 20584, log table 20585, list page 20586, integration events 20590, and extends QltyFileImport.Codeunit.al) to verify that the BCQuality Copilot PR-review wiring added in #8700 triggers and produces findings on microsoft/BCApps. This PR is intentionally crafted with reviewable issues and is NOT meant to be merged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Could not find a linked ADO work item. Please link one by using the pattern 'AB#' followed by the relevant work item number. You may use the 'Fixes' keyword to automatically resolve the work item when the pull request is merged. E.g. 'Fixes AB#1234' |
| for I := 1 to MaxFiles do begin | ||
| Clear(ServerFileName); | ||
| if not PromptAndImportIntoInStream(FilterString, FileInStream, ServerFileName) then | ||
| exit(ImportedCount); |
There was a problem hiding this comment.
In ImportBatch, TempBlob.CreateInStream(FileInStream) is called immediately after PromptAndImportIntoInStream has populated FileInStream with the uploaded file's content.
CreateInStream overwrites FileInStream with a new, empty InStream backed by TempBlob's empty internal buffer — the uploaded file's data is silently discarded on every iteration. The import count is incremented as if the file were processed. To buffer uploaded content into TempBlob, first create an OutStream from TempBlob, copy the uploaded stream into it, then create the InStream; alternatively, remove TempBlob entirely and process FileInStream directly after the upload succeeds.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
|
|
||
| if ResultBuffer.FindSet() then | ||
| repeat | ||
| // GET inside the loop re-reads the same customer on every iteration. |
There was a problem hiding this comment.
Customer.Get(CustomerNo) is called on every iteration of the repeat...until loop in ImportResults.
Because CustomerNo is a fixed parameter that does not change between iterations, this issues N identical round-trips to the Customer table. The fix is to hoist the Get call to before the FindSet check so the record is loaded exactly once.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| ImportLogEntry."Result Value" := ResultBuffer."Result Value"; | ||
| ImportLogEntry.Insert(); | ||
|
|
||
| // Commit inside the loop defeats the implicit transaction boundary. |
There was a problem hiding this comment.
Commit() is called inside a repeat...until loop in ImportResults.
This creates one transaction per imported row, defeating the platform's ability to roll back the whole import atomically and adding a transaction-boundary round-trip on every row. Per the referenced guidance, place Commit at a bounded checkpoint after N rows, or wrap each chunk in Codeunit.Run for native rollback on failure.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| ImportLogEntry: Record "Qlty. Import Log Entry"; | ||
| begin | ||
| ImportLogEntry.SetRange("Customer No.", CustomerNo); | ||
| // Count() materializes a number the caller does not need. |
There was a problem hiding this comment.
HasImportedResults uses Count() > 0 to test for the existence of any row, then falls through to FindFirst() — which is also only testing existence.
Both operations do more work than needed: Count() materialises a count the caller does not use; FindFirst() materialises a row the caller does not read. Per the referenced guidance, replace both with a single exit(not ImportLogEntry.IsEmpty()) after the SetRange.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
|
|
||
| TempBlob.CreateInStream(FileInStream); | ||
| ImportedCount += 1; | ||
|
|
There was a problem hiding this comment.
Commit() is called inside the for-loop in ImportBatch.
Every uploaded file gets its own transaction, removing atomicity and adding lock overhead on each iteration. Per the referenced guidance, commit at a clearly defined safe boundary outside the loop, or use Codeunit.Run for sub-operation atomicity.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| Dimensions: Dictionary of [Text, Text]; | ||
| begin | ||
| // PII (customer name) placed directly in the telemetry message string, | ||
| // and a placeholder event id that was never registered. |
There was a problem hiding this comment.
LogImportCompleted passes Customer.Name via StrSubstNo as the message text of Session.LogMessage.
The platform sends the message string to telemetry verbatim regardless of the DataClassification argument on the same call, so the customer name is now in Application Insights. Per the referenced guidance, replace the message with a static string such as 'Import completed' and move any structured context to custom dimensions — provided those dimensions carry no PII per featuretelemetry-customdimensions-no-pii.md.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| DataClassification::CustomerContent, | ||
| TelemetryScope::All, | ||
| 'Category', 'QualityImport'); | ||
|
|
There was a problem hiding this comment.
FeatureTelemetry.LogUsage receives a Dimensions dictionary containing 'CustomerName' -> Customer.Name.
Customer names are CustomerContent-class PII and must not appear in FeatureTelemetry custom dimensions; the platform sends them verbatim to telemetry. Per the referenced guidance, pass only non-personal context such as feature names, status codes, counts, or durations.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| local procedure RaiseImportError(CustomerNo: Code[20]) | ||
| begin | ||
| // Error message composed with StrSubstNo instead of passing the | ||
| // parameter directly to Error(). |
There was a problem hiding this comment.
RaiseImportError calls Error(StrSubstNo('Could not import results for customer %1', CustomerNo)).
The StrSubstNo result is a plain Text with all substitutions already performed; the platform cannot apply DataClassification to its contents when Error runs, so the interpolated customer identifier is logged to telemetry verbatim. Per the referenced guidance, pass the format Label and the argument separately — Error(CouldNotImportErr, CustomerNo) — so the platform classifies each argument individually.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| } | ||
| // PII fields below carry no DataClassification. | ||
| field(3; "Customer Name"; Text[100]) | ||
| { |
There was a problem hiding this comment.
Table field 3 'Customer Name' (Text[100]) stores a personal name but has no DataClassification property.
The platform defaults missing classification to SystemMetadata, which misclassifies customer name data in telemetry, GDPR exports, and admin reports. Per the referenced guidance, set DataClassification = CustomerContent on this field.
| { | |
| field(3; "Customer Name"; Text[100]) | |
| { | |
| Caption = 'Customer Name'; | |
| DataClassification = CustomerContent; | |
| } |
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| Caption = 'Customer Name'; | ||
| } | ||
| field(4; "Contact Email"; Text[80]) | ||
| { |
There was a problem hiding this comment.
Table field 4 'Contact Email' (Text[80]) stores an email address but has no DataClassification property.
The platform defaults to SystemMetadata, under-classifying this PII field in telemetry and GDPR exports. Per the referenced guidance, set DataClassification = CustomerContent on this field.
| { | |
| field(4; "Contact Email"; Text[80]) | |
| { | |
| Caption = 'Contact Email'; | |
| DataClassification = CustomerContent; | |
| } |
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
Seems like we need to revisit suggestions. It should only insert the DataClassification, not the entire field!
| /// </summary> | ||
| [IntegrationEvent(false, false)] | ||
| procedure OnBeforeCallLabApi(Endpoint: Text; ApiKey: Text; var BearerToken: Text) | ||
| begin |
There was a problem hiding this comment.
OnBeforeCallLabApi exposes ApiKey: Text and var BearerToken: Text as IntegrationEvent parameters.
Any extension on the tenant can subscribe to this event and read — or overwrite — both values. Per the referenced guidance, credentials must never flow through event parameters; restrict the payload to non-sensitive context (the business record, an IsHandled flag, or a mutable payload object whose contents the publisher controls). Authentication should be handled entirely within the publisher.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| end; | ||
|
|
||
| local procedure FetchResultsIntoBuffer(CustomerNo: Code[20]; var ResultBuffer: Record "Qlty. Import Log Entry" temporary) | ||
| var |
There was a problem hiding this comment.
ApiKey and BearerToken are declared as Text in both FetchResultsIntoBuffer's var block and DownloadResults's parameter list.
Any value that carries an API key, bearer token, or other secret must be SecretText end-to-end so the compiler prevents silent plain-text exposure and the debugger cannot read the value. Per the referenced guidance, declare both as SecretText and cascade the type change through GetApiKey/GetAccessToken return types and DownloadResults parameters.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| local procedure RaiseImportError(CustomerNo: Code[20]) | ||
| begin | ||
| // Error message composed with StrSubstNo instead of passing the | ||
| // parameter directly to Error(). |
There was a problem hiding this comment.
RaiseImportError wraps a hardcoded string literal in StrSubstNo and passes the result as the sole argument to Error().
Per the referenced guidance (CodeCop AA0231), Error() accepts a format Label and arguments directly; wrapping with StrSubstNo hides the placeholders from the translation pipeline and removes the format-string identity from the call site. Declare a Label with the 'Err' suffix and call Error(CouldNotImportResultsForCustomerErr, CustomerNo).
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| end; | ||
|
|
||
| // Uppercase reserved keywords throughout this procedure body. | ||
| local procedure CountValidEntries(var ImportLogEntry: Record "Qlty. Import Log Entry"): Integer |
There was a problem hiding this comment.
CountValidEntries uses uppercase reserved keywords throughout its body: VAR, BEGIN, IF, REPEAT, UNTIL, EXIT.
Per the referenced guidance (CodeCop AA0241), all AL reserved keywords must be lowercase. Run the AL formatter to normalise this procedure.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| local procedure DescribeEntry(EntryNo: Integer): Text | ||
| var | ||
| ImportLogEntry: Record "Qlty. Import Log Entry"; | ||
| begin |
There was a problem hiding this comment.
DescribeEntry has a space between the method name and its opening parenthesis on three call sites: 'Get (EntryNo)', 'exit (ImportLogEntry...)', and 'exit ('')'.
Per the referenced guidance (CodeCop AA0002), there must be no whitespace between a method name and its '('.
| begin | |
| if ImportLogEntry.Get(EntryNo) then | |
| exit(ImportLogEntry."Customer Name"); | |
| exit(''); |
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| begin | ||
| // PII (customer name) placed directly in the telemetry message string, | ||
| // and a placeholder event id that was never registered. | ||
| Session.LogMessage( |
There was a problem hiding this comment.
Session.LogMessage uses the placeholder event ID '0000'.
Per the referenced guidance, event IDs must be stable, unique, and non-placeholder — IDs like '0000' collide with every other extension that uses the same placeholder and make the event unsearchable in Application Insights or KQL dashboards. Assign a real, registered ID from the extension's telemetry catalogue before this code ships.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| ToolTip = 'Specifies the number of the import log entry.'; | ||
| } | ||
| field("Customer No."; Rec."Customer No.") | ||
| { |
There was a problem hiding this comment.
Page field 'Customer No.' has no ToolTip property.
Per the referenced guidance (CodeCop AA0218), every field control on a regular page requires a non-empty ToolTip starting with 'Specifies'. AppSource technical validation rejects pages with missing tooltips.
| { | |
| field("Customer No."; Rec."Customer No.") | |
| { | |
| ToolTip = 'Specifies the customer number.'; | |
| } |
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| { | ||
| } | ||
| field("Customer Name"; Rec."Customer Name") | ||
| { |
There was a problem hiding this comment.
Page field 'Customer Name' has no ToolTip property.
Per the referenced guidance (CodeCop AA0218), every field control on a regular page requires a non-empty ToolTip starting with 'Specifies'.
| { | |
| field("Customer Name"; Rec."Customer Name") | |
| { | |
| ToolTip = 'Specifies the customer name.'; | |
| } |
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| { | ||
| } | ||
| field("Contact Email"; Rec."Contact Email") | ||
| { |
There was a problem hiding this comment.
Page field 'Contact Email' has no ToolTip property.
Per the referenced guidance (CodeCop AA0218), every field control on a regular page requires a non-empty ToolTip starting with 'Specifies'.
| { | |
| field("Contact Email"; Rec."Contact Email") | |
| { | |
| ToolTip = 'Specifies the contact email address.'; | |
| } |
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| ToolTip = 'Specifies the measured result value.'; | ||
| } | ||
| field("Imported At"; Rec."Imported At") | ||
| { |
There was a problem hiding this comment.
Page field 'Imported At' has no ToolTip property.
Per the referenced guidance (CodeCop AA0218), every field control on a regular page requires a non-empty ToolTip starting with 'Specifies'.
| { | |
| field("Imported At"; Rec."Imported At") | |
| { | |
| ToolTip = 'Specifies the date and time when the result was imported.'; | |
| } |
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| /// <summary> | ||
| /// Returns the upload dialog title, looked up with a space before the parenthesis. | ||
| /// </summary> | ||
| procedure GetDialogTitle(): Text |
There was a problem hiding this comment.
GetDialogTitle has a space before the parenthesis in 'exit (ImportFromLbl)'.
Per the referenced guidance (CodeCop AA0002), there must be no whitespace between a method name and its '('.
| procedure GetDialogTitle(): Text | |
| exit(ImportFromLbl); |
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
This suggestion seems wrong. It suggests to replace the procedure name, while it should replace the exit statement.
Copilot PR ReviewIteration 1 · Outcome: completed Knowledge source: https://github.com/microsoft/BCQuality@822cae1b2771ac25f665f73369f69093bd4fd630 Findings by domainFindings split into Knowledge-backed (cite a BCQuality article) and Agent (the agent's own judgement, no matching BCQuality rule).
Totals: 20 knowledge-backed · 1 agent findings. Orchestrator pre-filter (13 file(s) excluded)
Findings produced by the Copilot CLI agent against BCQuality at |
) ## Problem On [#8772](#8772) two ```suggestion``` blocks were mis-applied, as flagged in review comments: - **`QltyFileImport` (line 68):** the comment anchored `procedure GetDialogTitle(): Text` but the fix `exit(ImportFromLbl);` was meant for line 70. Applying it would overwrite the procedure signature. - **`QltyImportLogEntry` (line 34):** the comment anchored `{` but the suggested code was the whole 4-line field. Applying it replaced `{` with the entire block, duplicating the field instead of just inserting the `DataClassification` line. Root cause: the orchestrator always posted a **single-line** comment at the model's reported `location.line`, but a GitHub suggestion replaces *exactly* the anchored line(s). When the model anchors a different/narrower line than the fix targets, the suggestion corrupts the file. ## Fix `tools/Code Review/scripts/Invoke-CopilotPRReview.ps1` now validates and re-anchors each suggestion against the PR-head file content before posting: - **Single-line fix** → snapped onto the line it actually rewrites (whitespace-insensitive match within a small window; trusts the model's anchor when no unique match exists). - **Multi-line fix** that edits a construct → posted as a **multi-line comment** over the full span it replaces, so inserted lines land in place. - **Unplaceable fix** → the applicable block is dropped and the change is shown as a manual, non-applicable snippet. New helpers: `Get-PrHeadFileLines`, `ConvertTo-LooseLine`, `Test-OrderedSubsequence`, `Resolve-SuggestionPlacement`; `New-ReviewComment` gained `start_line`/`start_side` support; `Build-CommentBody` gained `-SuppressSuggestion`. ## Validation Unit-tested the resolver against both real cases (snaps to 70..70; expands to 33..36) plus edge cases (genuine single-line rewrite trusts anchor, ambiguous match trusts anchor, unplaceable → drop, empty file → drop). Full script parses cleanly. Fixes: [AB#640364](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/640364) Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This PR exists only to verify that the BCQuality Copilot PR-review wiring added in #8700 (
Consume BCQuality knowledge base for Copilot PR review) is functioning correctly onmicrosoft/BCApps. It should be closed once the review agent has run.What it does
It replicates the exact byte-identical sample diff from microsoft/BCAppsBCQuality#47 (350 additions, 0 deletions) so we can do an apples-to-apples comparison of the review agent's findings against that known baseline.
Changes under
src/Apps/W1/Quality Management/app/:src/Integration/ExternalResults/QltyExtResultImport.Codeunit.alsrc/Integration/ExternalResults/QltyImportLogEntry.Table.alsrc/Integration/ExternalResults/QltyImportLog.Page.alsrc/Integration/ExternalResults/QltyExtResultEvents.Codeunit.alsrc/Utilities/QltyFileImport.Codeunit.alExpected outcome
The sample code is intentionally crafted with reviewable issues (magic numbers,
Commit()inside loops,Get/FindFirstresults thrown away, PII fields withoutDataClassification, plain-text credentials, uppercase reserved keywords, spaces before parentheses, an off-by-one loop, PII in telemetry, etc.). A successful wiring should surface these as review comments — mirroring the ~25 findings seen on the #47 baseline.If the review agent posts findings here, the wiring is working. 🎯
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com