mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
Merge branch 'stable' into fix/bc-mcp-restart-theory-to-stable
This commit is contained in:
commit
dd4dc4e1f3
18 changed files with 602 additions and 83 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
kind: action-skill
|
kind: action-skill
|
||||||
id: curabis-standards-inspector
|
id: curabis-standards-inspector
|
||||||
version: 7
|
version: 8
|
||||||
title: Rømer — Standards Inspector
|
title: Rømer — Standards Inspector
|
||||||
description: >
|
description: >
|
||||||
Owns the uniformity inspection across CURABIS repos: walks one full
|
Owns the uniformity inspection across CURABIS repos: walks one full
|
||||||
|
|
@ -140,6 +140,15 @@ Walk ALL stations, every time. A partial round creates false confidence
|
||||||
mistake. This station has nothing to check against an org that has
|
mistake. This station has nothing to check against an org that has
|
||||||
never run Mode C — a clean round with an empty registry is one line,
|
never run Mode C — a clean round with an empty registry is one line,
|
||||||
same as any other station.
|
same as any other station.
|
||||||
|
16. **Legacy repo-committed `.claude/settings.json`** (2026-08-03). Grep
|
||||||
|
`.claude/settings.json` for the old Dynamic Tool Mode tool names
|
||||||
|
(`bc_actions_search`, `bc_actions_describe`, `bc_actions_invoke`) or an
|
||||||
|
`enabledMcpjsonServers` entry for `al`/`businesscentral` — same
|
||||||
|
multi-developer coordination caveat as the `.mcp.json` migration
|
||||||
|
(station covers detection only; removal needs explicit developer
|
||||||
|
confirmation via curabis-standard.agent.md's Mode B step 5). Found live
|
||||||
|
in the `Wareco` repo: causes MCP servers to show duplicated under
|
||||||
|
"Project" scope in the panel alongside the correct "User" scope entry.
|
||||||
|
|
||||||
CURABIS-ROEMER-001 Measure against the written standard only. Every finding
|
CURABIS-ROEMER-001 Measure against the written standard only. Every finding
|
||||||
cites the standard it deviates from — a rule file, the template table, or a
|
cites the standard it deviates from — a rule file, the template table, or a
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: error-handling
|
||||||
|
keywords: [testfield, setup-table, configuration, mandatory-field, silent-fallback, correctness]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: error-handling/fielderror-vs-testfield.md
|
||||||
|
---
|
||||||
|
# TestField a Setup-Table Value Before Using It in a Correctness-Critical Branch
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
`fielderror-vs-testfield.md` covers choosing between `TestField` and `FieldError` once a check is already known to be needed. This rule covers the decision one step earlier: recognizing that a check is needed at all when a procedure reads a field from a *different* record — typically a setup/configuration table — inside a branch where the surrounding logic has already determined that value is required for correct behavior. A common anti-pattern silently treats "blank" the same as "feature not wanted": `if SetupRec."Some Field" <> '' then exit(UseIt); exit(SomeOtherDefault);`. `Get()` succeeding on the setup record proves the record exists, not that the specific field was ever configured — and the fallback path makes a missing configuration indistinguishable from a deliberate one.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Once a business rule has decided that a setup-table field's value is required for this branch to behave correctly, call `TestField` on it before use — even though a plain read would "work" (return a blank string or zero without erroring). Do this specifically when the value drives a decision whose wrong outcome is silent and consequential — financial, tax, or compliance-relevant postings are the clearest case, because the failure mode is not a crash a tester would notice, but a quietly wrong result that only surfaces on audit. Write a test that blanks the setup field and asserts the resulting error, so the guard itself is verified rather than merely present.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
Reading a required setup-table field with a presence check that falls through to a default instead of erroring: `if Setup."Field" <> '' then exit(Setup."Field"); exit(Default);`. This looks defensive — it never crashes — but it converts "administrator forgot to configure this" into "system silently did something else," which is worse than a hard failure because nobody is told anything went wrong. A reviewer can spot this by finding a setup-table field read inside an already-decided business-rule branch with no `TestField`/`FieldError`/`Error` anywhere on the path.
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: error-handling
|
||||||
|
keywords: [table-events, oninsert, onmodify, ondelete, transaction, rollback, commit, batch, subscriber]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: community/error-handling/table-event-subscriber-rolls-back-whole-batch.md
|
||||||
|
---
|
||||||
|
# A Throw In A Table-Event Subscriber Rolls Back The Whole Batch
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Table-trigger event subscribers (`OnAfterInsertEvent`, `OnAfterModifyEvent`, `OnAfterDeleteEvent`, and their `OnBefore` counterparts) execute synchronously inside the transaction of the write that fired them. Because AL runs on a single implicit transaction with no per-record savepoint, an error raised in such a subscriber rolls back **all work since the last `COMMIT`** — not just the record that triggered it. In a batch loop with no intermediate `COMMIT`s, a single failing record discards the entire batch. The intuition that subscriber validation fails only the current record is wrong on the BC platform.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Decide the failure granularity deliberately. If a batch must continue past individual failures, do not throw from the table-event subscriber — collect the error (for example via `ErrorInfo`/collectible errors) and let the loop continue, or isolate each record's work behind a `Codeunit.Run` / `if Codeunit.Run() then` boundary so its failure rolls back only that record. Insert intermediate `COMMIT`s only with full awareness of the durability trade-off.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
Putting `Error`/`TestField`/`FieldError` validation inside a table-event subscriber and assuming it rejects just the offending record during bulk processing. The first failure unwinds every uncommitted record in the run, turning a one-row data problem into a whole-batch rollback.
|
||||||
|
|
@ -62,37 +62,25 @@ The `Internal_CompanyNotFound` symptom recurred on 2026-08-03 on two
|
||||||
independent developer machines, both with `company` already set to the
|
independent developer machines, both with `company` already set to the
|
||||||
correct `Navn` value (`"Curabis ApS"`) per this rule. The header-mismatch
|
correct `Navn` value (`"Curabis ApS"`) per this rule. The header-mismatch
|
||||||
cause above was confirmed absent both times — yet the error still occurred,
|
cause above was confirmed absent both times — yet the error still occurred,
|
||||||
intermittently, within the same working day.
|
intermittently, within the same working day. Restarting Claude Code (which
|
||||||
|
respawns the `bc-mcp-bridge.js` process and re-establishes the MCP session)
|
||||||
|
was observed to restore working state, though this has not been root-caused.
|
||||||
|
|
||||||
This means a correct `Navn`-matching header is **necessary but not
|
This means a correct `Navn`-matching header is **necessary but not proven
|
||||||
sufficient**: the same-looking error can have a second cause unrelated to
|
sufficient**: the same-looking error can have a second, distinct cause tied
|
||||||
the header value. When this happens, the header-match check (Verification,
|
to the long-running bridge/session rather than a static config value. A
|
||||||
above) has nothing left to find — do not keep re-checking that same field.
|
developer who has already verified the header matches `Navn` character for
|
||||||
|
character should not keep re-checking that same field. Next steps, in order:
|
||||||
|
|
||||||
**Ruled out on 2026-08-03, with evidence — do not re-investigate these:**
|
1. Restart Claude Code once and retry.
|
||||||
- **Stale client process.** A theory that a long-running `bc-mcp-bridge.js`
|
2. If it recurs, check BC-side state that a config file can't reveal: the
|
||||||
process was running pre-fix code from before its 2026-08-01 update, and
|
Entra app registration's (`BC_DevelopmentMCP`) company-permission
|
||||||
that restarting Claude Code would pick up the fix. **Falsified same day:**
|
assignment, and whether the relevant MCP Server Configuration
|
||||||
a full machine reboot (strictly stronger than a Claude Code restart — kills
|
(`Model Context Protocol (MCP) Server Configurations`, BC page 8351) is
|
||||||
every process, clears all in-memory state, re-establishes every network
|
still Active.
|
||||||
connection) left the exact same error unchanged immediately after. An
|
3. If it recurs across restarts and BC-side checks pass, treat it as a
|
||||||
earlier apparent "it works after a restart" observation was very likely
|
SaaS-side incident and escalate to Microsoft support rather than
|
||||||
coincidental with an intermittent server-side state, not causal.
|
re-diagnosing the client config a third time.
|
||||||
- **MCP Server Configuration misconfigured.** Verified via BC UI screenshot:
|
|
||||||
`CURABIS_DEV` configuration is `Aktiv` (Active) = on, `Standard` (Default)
|
|
||||||
= on, with the expected tool set and permissions present.
|
|
||||||
- **Company record wrong or `Navn` mismatched.** Verified via BC UI
|
|
||||||
screenshot of the company list: `Navn` = `Curabis ApS` exactly (matches
|
|
||||||
config character-for-character), `Vist navn` = `CURABIS ApS` (confirming
|
|
||||||
why the original 2026-07-31 mix-up was easy to make), setup status
|
|
||||||
`Completed`.
|
|
||||||
|
|
||||||
**Conclusion:** with the header confirmed correct, the MCP configuration
|
Root cause of the session/restart-correlated failure mode is still open —
|
||||||
confirmed active/default, and the company record confirmed correct — all
|
this section records the observed correlation, not a confirmed mechanism.
|
||||||
via direct BC UI inspection, not inference — and the error still recurring
|
|
||||||
intermittently, immune even to a full machine reboot, this is not a client-
|
|
||||||
fixable condition. Escalate to Microsoft support with the evidence bundle
|
|
||||||
(exact error text, `~/.bc-mcp.config.json` values, both BC UI screenshots,
|
|
||||||
and timestamps of both failing and working calls) rather than continuing
|
|
||||||
local troubleshooting. Root cause of the intermittent failure itself remains
|
|
||||||
unconfirmed — likely a BC/SaaS-side condition outside CURABIS's visibility.
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: performance
|
||||||
|
keywords: [deleteall, ondelete, run-trigger, set-based-delete, bulk-delete, triggers, validation]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: community/performance/deleteall-skips-ondelete-unless-runtrigger.md
|
||||||
|
---
|
||||||
|
# DeleteAll Skips OnDelete Unless You Pass RunTrigger
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
`Record.DeleteAll()` — equivalently `DeleteAll(false)` — translates to a single set-based SQL `DELETE` and **does not** run AL `OnDelete` triggers or field/table validations. Only database-level referential constraints still apply. To run `OnDelete` logic you must call `DeleteAll(true)`, which then deletes record-by-record and forfeits the set-based performance, making it equivalent to a `FindSet` loop calling `Delete(true)`. The common misconception, which training data reproduces, is that `DeleteAll` iterates and fires `OnDelete` per record; it does not. (Parameterless `Delete()` likewise defaults to `Delete(false)` and skips `OnDelete`.)
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Use `DeleteAll()` / `DeleteAll(false)` for bulk deletion only when no AL `OnDelete` cleanup is required — it is the fast, set-based form. When `OnDelete` logic must run (cascading deletes, ledger cleanup, integration events), pass `DeleteAll(true)` and accept the row-by-row cost, or refactor the cleanup to run explicitly before the bulk delete.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
Calling `DeleteAll()` and assuming dependent records, integration events, or validation side effects are handled by `OnDelete`. The deletion succeeds but the AL-side cleanup never runs, leaving orphaned data — and adding a manual `FindSet`/`Delete` loop "for safety" reintroduces the per-record cost the set-based form was chosen to avoid.
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: security
|
||||||
|
keywords: [oauth2, api-key, authentication, httpclient, token-refresh]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: community/security/prefer-oauth2-over-api-keys-for-external-http-calls.md
|
||||||
|
---
|
||||||
|
# Prefer OAuth2 Over API Keys For External HTTP Calls
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
External HTTP integrations from AL can authenticate using OAuth 2.0 (client-credentials for service-to-service, authorization-code for user-delegated), API keys, basic authentication, or credentials in URLs. The mechanisms differ substantially in the blast radius of a leaked secret and in how cleanly tokens can be rotated. OAuth-issued tokens expire on their own schedule and rotate cleanly; API keys and basic-auth passwords typically have to be rotated manually and usually live unencrypted in a configuration table. When the partner supports OAuth, the difference is a material security improvement, not a stylistic preference.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
When the partner supports OAuth, use the platform `OAuth2` codeunit (`AcquireTokenWithClientCredentials` for service-to-service, `AcquireAuthorizationCodeTokenFromCache` for user-delegated flows) rather than hand-rolled token acquisition. Carry tokens and client secrets as `SecretText`, persist them only in IsolatedStorage (see `secrets-isolated-storage`), and refresh tokens proactively — on a buffer before the documented expiry — so routine calls never block on a token refresh.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
Accepting an API-key or basic-auth integration because it is the first option documented, even when the partner supports OAuth. The shared secret usually ends up in a setup-table `Text` field, rotation becomes a manual operation that rarely happens, and a single disclosure exposes every tenant using the extension.
|
||||||
21
custom/knowledge/security/secrets-isolated-storage.md
Normal file
21
custom/knowledge/security/secrets-isolated-storage.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: security
|
||||||
|
keywords: [isolatedstorage, secrets, api-key, oauth-token, connection-string, table-field, credentials]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: community/security/secrets-isolated-storage.md
|
||||||
|
---
|
||||||
|
# A Secret Belongs In IsolatedStorage, Never In A Table Field
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
API keys, OAuth tokens, client secrets, and connection strings must not be stored in an ordinary table `Text` field — not even on a hidden setup table. A regular field is exposed through record reads, page display, RapidStart and Excel export, report datasets, and surfaces in `DataClassification` review; anyone with table permission can read it. The correct home is `IsolatedStorage`, which is invisible to database queries, API pages, and configuration packages. The storage-*location* decision is the rule here; how to scope and encrypt the value once it is in IsolatedStorage is covered separately.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Persist every credential with `IsolatedStorage`, write it at the point of capture, and read it only when needed. For the per-secret details — choosing the right `DataScope`, encrypting at rest, and typing the value as `SecretText` so it cannot leak into logs — follow the companion rules on IsolatedStorage `DataScope`, `SetEncrypted`, and `SecretText`.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
A "Setup" or "Connection" table carrying a `Text` field named `API Key`, `Password`, or `Client Secret`. The value is now readable by any object with table permission, ships in RapidStart packages and Excel exports, and appears in record snapshots — a credential disclosure that no amount of encryption-in-transit elsewhere makes up for. Reviewer signal: a secret-shaped field declared on a table instead of an `IsolatedStorage` call.
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: telemetry
|
||||||
|
keywords: [telemetry, session-logmessage, telemetryscope, application-insights, extensionpublisher, ingestion-cost]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: community/telemetry/default-telemetryscope-to-extensionpublisher.md
|
||||||
|
---
|
||||||
|
# Default TelemetryScope to ExtensionPublisher, not All
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
The `TelemetryScope` parameter of `Session.LogMessage` (and `LogError`) controls *where* a custom telemetry signal is routed, not just whether it is emitted. `TelemetryScope::ExtensionPublisher` sends the signal only to the extension publisher's own Application Insights resource. `TelemetryScope::All` sends it to **both** the publisher's resource **and** the customer's environment-level Application Insights resource. The distinction is easy to get wrong because both values compile and both "emit telemetry" — but `All` silently adds to the customer's ingestion volume and cost. Promoted to always-on: CURABIS builds telemetry-based health checks across customer environments (Wareco, Allnet, Guardique), so a scope mistake here doesn't just cost the customer — it pollutes the very signal CURABIS's own tooling reads.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Default to `TelemetryScope::ExtensionPublisher` for diagnostic telemetry that only the publisher acts on. Reserve `TelemetryScope::All` for signals the customer's own administrators are expected to monitor and act on (for example, a business event surfaced to their environment telemetry). Treat the choice as a deliberate routing decision per signal, not a copy-paste default.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
Emitting all custom telemetry with `TelemetryScope::All` "to be safe." This pushes the publisher's internal diagnostics into every customer's Application Insights, inflating their ingestion cost and burying their own signals in noise — a footgun a code reviewer can catch by flagging `All` on any signal the customer would not act on.
|
||||||
21
custom/knowledge/ui/factbox-design.md
Normal file
21
custom/knowledge/ui/factbox-design.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: ui
|
||||||
|
keywords: [factbox, subpagelink, listpart, cardpart, page-part, related-information, flowfield-sift]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: community/ui/factbox-design.md
|
||||||
|
---
|
||||||
|
# Filter ListPart FactBoxes With SubPageLink To The Parent Record
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
A FactBox is a page `part` that surfaces related data beside the main record so users avoid navigating away. Every FactBox runs a database query as its host page loads, so an unfiltered one is a hidden performance tax paid on every page open. The remedial trap: a `ListPart` FactBox with no `SubPageLink` does not show "the related rows" — it loads and pages through the entire source table, because nothing ties it to the host record. This makes correct `SubPageLink` linkage, not visual layout, the load-bearing design decision.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Give every `ListPart` FactBox a `SubPageLink` that maps a field on the part's source table to a `field()` of the host record (for example `SubPageLink = "Document No." = field("No.")`), so it returns only rows belonging to the current record. Prefer a `CardPart` when you only need summary figures (balance, availability, status) — it reads a single record and avoids list overhead entirely. When a FactBox shows FlowFields, ensure the calculated total is backed by a SIFT key (`MaintainSIFTIndex`) so the sum is read from the index rather than aggregated row-by-row on each load. Keep FactBox count modest and avoid heavy `OnAfterGetRecord` logic in the part.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
Adding a `ListPart` FactBox without a `SubPageLink`, expecting it to "just show related lines." The consequence is a full-table scan on every page load that grows with the dataset and is felt worst on list pages, where the FactBox re-queries on each row selection. Reviewer signal: any `part(...)` referencing a list-type page part where the `SubPageLink` property is absent, or a FactBox FlowField filtered on non-indexed fields. A second smell is duplicating data already on the page or stacking many FactBoxes, which multiplies queries for little context gain.
|
||||||
21
custom/knowledge/ui/fasttab-field-importance.md
Normal file
21
custom/knowledge/ui/fasttab-field-importance.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: ui
|
||||||
|
keywords: [importance, promoted, additional, fasttab, show-more, summary-line, progressive-disclosure, field-visibility]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: community/ui/fasttab-field-importance.md
|
||||||
|
---
|
||||||
|
# Set Field Importance To Drive FastTab Progressive Disclosure
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
A FastTab field's `Importance` property controls whether the field is visible immediately, hidden behind "Show more", or surfaced on the collapsed FastTab header summary line. The three values are `Standard` (the default, shown in the expanded FastTab), `Promoted` (also rendered on the FastTab header when the tab is collapsed), and `Additional` (hidden until the user clicks "Show more"). Misusing these values either clutters the summary line or buries fields users need on every transaction, so reviewers should treat `Importance` as a deliberate layout decision rather than an afterthought.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Promote only the two to four identifying fields per FastTab that users must read at a glance without expanding — name, status, key amount — so the collapsed header summary line stays scannable. Leave the everyday working fields at `Standard`, and push rarely-touched fields (legacy compatibility fields, system timestamps, seldom-changed configuration) to `Additional`. Note that field-level `Importance = Promoted` is unrelated to action promotion on the page action bar; it governs FastTab field visibility only. Do not rely on initial expand or collapse state, which you cannot set programmatically and which the platform may personalize per user — design assuming any FastTab may be collapsed.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
Setting `Importance = Promoted` on most fields of a FastTab so "everything is important" defeats progressive disclosure: the collapsed summary line overflows and conveys nothing at a glance. The opposite failure is marking frequently edited fields `Additional`, forcing users to click "Show more" on every record. A detectable signal is a FastTab whose fields are nearly all `Promoted`, or a FastTab containing only `Additional` fields, which renders as an empty tab until expanded.
|
||||||
21
custom/knowledge/ui/page-background-tasks.md
Normal file
21
custom/knowledge/ui/page-background-tasks.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: ui
|
||||||
|
keywords: [enqueuebackgroundtask, async-calculation, child-session, factbox, cue-tile, onaftergetcurrrecord, responsive-page, read-only]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: community/ui/page-background-tasks.md
|
||||||
|
---
|
||||||
|
# Offload Slow Read-Only Page Calculations To Background Tasks
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Pages that compute statistics, aggregates, or external lookups inline block the page from rendering until the calculation finishes, producing a visible freeze on FactBoxes, cue tiles, and calculated fields. Business Central provides page background tasks: `CurrPage.EnqueueBackgroundTask` runs a dedicated codeunit in a read-only child session and returns values via `OnPageBackgroundTaskCompleted`, so the page opens immediately and fills in computed values as they arrive. This matters because users should never wait on a calculation they may not need. The mechanism has specific rules that are easy to get wrong, which is why it warrants an explicit pattern.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Move any noticeable read-only computation off the synchronous render path into a background task. Enqueue from `OnAfterGetCurrRecord` so the task is tied to the currently focused record, and pass small payloads through the `Dictionary of [Text, Text]` input/output, converting types with `Format` and `Evaluate`. Keep each task focused on one value or a small related set rather than one large task, and show a placeholder until results land. Because tasks auto-cancel when the page closes, the record changes, or a same-ID task is re-enqueued, always supply sensible defaults and handle the timeout path in `OnPageBackgroundTaskError` — never let critical functionality depend on completion. For tests, drive the task synchronously with `RunPageBackgroundTask`.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
Enqueuing from `OnAfterGetRecord` on a list page fires the task for every row, and each cancels the instant the selection moves to the next row — pure wasted child-session churn; a reviewer spots `EnqueueBackgroundTask` called from `OnAfterGetRecord` (or from `OnOpenPage`, where the record context is not yet stable). The other tell is a task codeunit attempting a database write or `Modify`: background tasks run read-only and the write fails at runtime. Inline heavy calculation directly in `OnAfterGetCurrRecord` with no task at all is the baseline smell — it reintroduces the page freeze the feature exists to remove.
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [21..]
|
||||||
|
domain: ui
|
||||||
|
keywords: [actionref, promoted-actions, area-promoted, promotedcategory, promotedonly, action-bar, legacy-syntax]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: community/ui/prefer-actionref-syntax-for-promoted-actions.md
|
||||||
|
---
|
||||||
|
# Promote Actions With The Modern actionref Syntax, Never The Legacy Promoted Properties
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Business Central 2022 release wave 2 (v21) introduced the `area(Promoted)` block with `actionref` as the way to promote page actions, separating an action's definition from its promotion. The older approach set `Promoted`, `PromotedCategory`, `PromotedOnly`, and `PromotedIsBig` directly on each action. The two syntaxes cannot be mixed within a single page or page extension, and choosing the legacy one entangles definition with presentation, making the action bar harder to maintain and to extend. Applies to new pages and page extensions on BC21 or later; existing objects already on legacy syntax are not required to be rewritten (see Anti Pattern).
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
For new pages and page extensions, define actions in their normal `area`, then promote selected ones with `actionref` inside `area(Promoted)`, grouping them under explicit categories such as `Category_Process` and entity-named groups. This keeps each action defined once and referenced where it should appear, supports split buttons via `ShowAs`, and lets an extension promote a base action without redefining it. When extending a page, you may use modern syntax even if the base page used legacy properties (and vice versa) — the no-mixing rule is per-object, not per-dependency-tree.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
Setting `Promoted = true` (with `PromotedCategory`, `PromotedOnly`, or `PromotedIsBig`) on actions in new code, or attempting to combine those properties with an `area(Promoted)` block in the same object — the latter fails to compile. The reviewer signal is any `Promoted`-prefixed property on an action in a newly authored page or page extension; flag it and convert to `actionref` (VS Code offers an automated conversion). Note separately that once an action is promoted in a published app, removing the promotion is a breaking change (AS0031/AW0013), so promote conservatively rather than walking it back later.
|
||||||
21
custom/knowledge/ui/promoted-action-groups.md
Normal file
21
custom/knowledge/ui/promoted-action-groups.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [21..]
|
||||||
|
domain: ui
|
||||||
|
keywords: [action-groups, area-promoted, actionref, showas, split-button, group-caption, navigate-group, entity-group]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: community/ui/promoted-action-groups.md
|
||||||
|
---
|
||||||
|
# Use Standard Promoted Action Group Names And Placements
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Business Central ships a fixed vocabulary of promoted action groups, and users build muscle memory around where each kind of action lives. When you define `area(Promoted)` groups, reusing the standard caption and placement for a given action class makes the page feel native; inventing your own caption or putting an action in the wrong group forces every user to relearn your page. Frontier models tend to emit plausible-but-nonstandard captions (`Go To`, `Vendor Actions`, `Related`) instead of the established BC names, which is exactly what breaks cross-page consistency — a risk that applies directly to AI-assisted AL development.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Map each action to its conventional group and use the exact standard caption: `Home`/`Process` for data-modifying and workflow actions (entity/card/document pages use `Home`, lists and worksheets use `Process`); an entity-named group (`Customer`, `Item`, `Order`) for navigation tied to the current record (statistics, ledger entries, dimensions); `Navigate` for related pages that are useful regardless of the selected record; `Report` for printing and analysis; and the workflow groups `Posting`, `Release`, `Approve`, `Request Approval`, and `Prepare` for their respective document lifecycle actions. Only `Posting` (Post / Post and Print / Preview) and `Release` (Release / Reopen) should render as split buttons via `ShowAs = SplitButton`; everything else is a normal dropdown. Within a common group keep the same action sequence you see on the matching base-app page (e.g. mirror Sales Order for a sales document) so order stays predictable. See the companion rule on `ShowAs = SplitButton` scope for the split-button decision specifically.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
Custom captions for what is really a standard group (`Vendor Actions` instead of the `Vendor` entity group, `Go To` instead of `Navigate`), posting or statistics actions dropped into the wrong group, or many tiny one-action groups that fragment the ribbon. The reviewer signal is an `area(Promoted)` block whose `group` captions do not match the base-application names for the same page type, or a `ShowAs = SplitButton` on anything other than `Posting`/`Release`.
|
||||||
21
custom/knowledge/upgrade/no-series-bc24-migration.md
Normal file
21
custom/knowledge/upgrade/no-series-bc24-migration.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
bc-version: [24..]
|
||||||
|
domain: upgrade
|
||||||
|
keywords: [no-series, noseriesmanagement, codeunit-310, getnextno, peeknextno, testmanual, arerelated, no-series-batch, business-foundation, obsolete-codeunit]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
extends: community/upgrade/no-series-bc24-migration.md
|
||||||
|
---
|
||||||
|
# Migrate No. Series Calls From NoSeriesManagement To The BC24 No. Series Module
|
||||||
|
|
||||||
|
> Contributions welcome — open a PR to refine or extend this article.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
In BC24 (2024 Wave 1) Microsoft moved number generation into the Business Foundation `No. Series` codeunit (310) and obsoleted the legacy `NoSeriesManagement` codeunit (396). Code that still declares `Codeunit NoSeriesManagement` or calls its methods compiles only against the temporary obsolete shim and will break once Microsoft removes it. The new API is not a drop-in rename: the facade exposes a small, specific set of real methods, parameter shapes changed, and the old single method that both previewed and consumed a number was split into two. Getting the mapping wrong silently consumes numbers when you only meant to preview, leaving gaps in the sequence. Applies to projects on BC24 or later; on earlier versions `NoSeriesManagement` remains the correct API.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Replace the `NoSeriesManagement` variable with `Codeunit "No. Series"` and map each call deliberately using the facade's actual methods — `GetNextNo`, `PeekNextNo`, `GetLastNoUsed`, `TestManual`, `IsManual`, and `AreRelated`. Use `GetNextNo(SeriesCode, RefDate)` only when you intend to consume and advance the series for a committed document, and `PeekNextNo(SeriesCode, RefDate)` for any display, validation, or preview-posting path where you must not consume. Replace `InitSeries` with a guarded `if "No." = '' then "No." := NoSeries.GetNextNo(...)`. Map `SelectSeries` to `LookupRelatedNoSeries`, relationship checks the old code did by hand to `AreRelated`, and both `TestManual` and `ManualNoAllowed` to `TestManual` (which now raises its own error). For multi-document allocation use `Codeunit "No. Series - Batch"` and persist its state once with `SaveState` instead of committing per iteration.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
Mechanically swapping the codeunit reference while keeping the old boolean call shape. The legacy `GetNextNo(Series, Date, false)` meant "peek" and `GetNextNo(Series, Date, true)` meant "consume"; the new `GetNextNo` always consumes and takes no boolean. Equally common is inventing validation helpers such as `IsValidNo`, `VerifySeriesExists`, `IsValidForDate`, or `TryGetNextNo` — these names are not on the `No. Series` or `No. Series - Batch` codeunits and will not compile, a frequent LLM hallucination for this migration. A reviewer can detect the defect by the residual third boolean argument, by any lingering `NoSeriesMgt`/`NoSeriesManagement` identifier, by a fabricated method name, or by an `OnBeforeGetNextNo`/`OnAfterGetNextNo` subscriber — those events were removed without replacement, so that logic must be rewritten as inline pre/post procedures, not re-subscribed. A subtler signal is `GetNextNo` used merely to display a preview, which silently advances the series and creates number gaps; that should be `PeekNextNo`.
|
||||||
|
|
@ -15,6 +15,14 @@ const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const ENDPOINT = "https://mcp.businesscentral.dynamics.com";
|
const ENDPOINT = "https://mcp.businesscentral.dynamics.com";
|
||||||
|
// 2026-08-04: neither fetch() call below had a timeout, and the BC session
|
||||||
|
// (sessionId) + underlying HTTP connection were reused for the bridge
|
||||||
|
// process's entire lifetime with no proactive refresh. A long-running Claude
|
||||||
|
// Code session with gaps between calls can leave either stale -- observed as
|
||||||
|
// a 30-minute hang on one call, and separately as an "Internal_CompanyNotFound"
|
||||||
|
// that wasn't actually a company problem. REQUEST_TIMEOUT_MS + the one-retry
|
||||||
|
// wrapper below fail fast and self-heal with a forced-fresh session instead.
|
||||||
|
const REQUEST_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
function die(m) { process.stderr.write(`[bc-mcp-bridge] ${m}\n`); process.exit(1); }
|
function die(m) { process.stderr.write(`[bc-mcp-bridge] ${m}\n`); process.exit(1); }
|
||||||
|
|
||||||
|
|
@ -42,7 +50,18 @@ function loadConfig() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
let token = null, tokenExp = 0, sessionId = null;
|
let token = null, tokenExp = 0, sessionId = null, lastInitializeMsg = null, lastActivityAt = 0;
|
||||||
|
// 2026-08-07: the reactive re-initialize below only fires after BC has already
|
||||||
|
// rejected a request. It self-heals invisibly to the caller, but still burns
|
||||||
|
// two round trips (reject, initialize, retry) on the first call after any idle
|
||||||
|
// gap. A client that opens a fresh session per call (e.g. a Power Automate
|
||||||
|
// flow) never hits this at all, which is why it reads as more stable than a
|
||||||
|
// long-lived interactive session that caches sessionId for its whole
|
||||||
|
// lifetime. Proactively refreshing before a call that has been idle a while
|
||||||
|
// gets the same effective robustness without waiting for BC to reject first.
|
||||||
|
// No API exposes the session's actual TTL, so this threshold is a heuristic,
|
||||||
|
// not a documented guarantee.
|
||||||
|
const SESSION_IDLE_THRESHOLD_MS = 3 * 60 * 1000; // 3 min
|
||||||
|
|
||||||
async function getToken() {
|
async function getToken() {
|
||||||
if (token && Date.now() < tokenExp - 60000) return token; // forny 1 min foer udloeb
|
if (token && Date.now() < tokenExp - 60000) return token; // forny 1 min foer udloeb
|
||||||
|
|
@ -56,6 +75,7 @@ async function getToken() {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
body,
|
body,
|
||||||
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||||
});
|
});
|
||||||
if (!r.ok) throw new Error(`token ${r.status}: ${await r.text()}`);
|
if (!r.ok) throw new Error(`token ${r.status}: ${await r.text()}`);
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
|
|
@ -76,7 +96,7 @@ function parseSSE(text) {
|
||||||
return msgs;
|
return msgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function forward(msg) {
|
async function forwardOnce(msg) {
|
||||||
const tok = await getToken();
|
const tok = await getToken();
|
||||||
const headers = {
|
const headers = {
|
||||||
"Authorization": `Bearer ${tok}`,
|
"Authorization": `Bearer ${tok}`,
|
||||||
|
|
@ -88,7 +108,12 @@ async function forward(msg) {
|
||||||
"ConfigurationName": enc(cfg.config),
|
"ConfigurationName": enc(cfg.config),
|
||||||
};
|
};
|
||||||
if (sessionId) headers["Mcp-Session-Id"] = sessionId;
|
if (sessionId) headers["Mcp-Session-Id"] = sessionId;
|
||||||
const r = await fetch(ENDPOINT, { method: "POST", headers, body: JSON.stringify(msg) });
|
const r = await fetch(ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(msg),
|
||||||
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||||
|
});
|
||||||
const sid = r.headers.get("mcp-session-id"); if (sid) sessionId = sid;
|
const sid = r.headers.get("mcp-session-id"); if (sid) sessionId = sid;
|
||||||
const ct = r.headers.get("content-type") || "";
|
const ct = r.headers.get("content-type") || "";
|
||||||
const text = await r.text();
|
const text = await r.text();
|
||||||
|
|
@ -103,6 +128,58 @@ async function forward(msg) {
|
||||||
return ct.includes("text/event-stream") ? parseSSE(text) : (text.trim() ? [text.trim()] : []);
|
return ct.includes("text/event-stream") ? parseSSE(text) : (text.trim() ? [text.trim()] : []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function forward(msg) {
|
||||||
|
if (msg.method === "initialize") lastInitializeMsg = msg;
|
||||||
|
if (sessionId && msg.method !== "initialize" && lastActivityAt &&
|
||||||
|
Date.now() - lastActivityAt > SESSION_IDLE_THRESHOLD_MS) {
|
||||||
|
process.stderr.write(`[bc-mcp-bridge] session idle ${Math.round((Date.now() - lastActivityAt) / 1000)}s, refreshing proactively\n`);
|
||||||
|
sessionId = null;
|
||||||
|
if (lastInitializeMsg) {
|
||||||
|
try {
|
||||||
|
await forwardOnce(lastInitializeMsg);
|
||||||
|
} catch (initErr) {
|
||||||
|
process.stderr.write(`[bc-mcp-bridge] proactive re-initialize failed: ${initErr.message || initErr}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await forwardOnce(msg);
|
||||||
|
lastActivityAt = Date.now();
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
// 2026-08-04: one retry with a forced-fresh BC session (sessionId cleared).
|
||||||
|
// A long-idle Claude Code session can leave the cached Mcp-Session-Id or
|
||||||
|
// the underlying HTTP connection stale -- observed as a request hanging
|
||||||
|
// until REQUEST_TIMEOUT_MS, or as an "Internal_CompanyNotFound" that was
|
||||||
|
// actually a dead session, not a real company problem. Quick, frequent
|
||||||
|
// calls never idle long enough to hit this; a long session with gaps
|
||||||
|
// between calls does.
|
||||||
|
process.stderr.write(`[bc-mcp-bridge] retrying after: ${e.message || e}\n`);
|
||||||
|
sessionId = null;
|
||||||
|
// 2026-08-05: clearing sessionId is not enough on its own. BC's own error
|
||||||
|
// is explicit -- "A new session can only be created by an initialize
|
||||||
|
// request" -- so simply resending the original non-initialize message
|
||||||
|
// with no session header reproduces the exact same error, not a fresh
|
||||||
|
// session. Replay the client's actual initialize message first (cached
|
||||||
|
// above, the only thing BC will accept to hand out a new session), THEN
|
||||||
|
// retry the original call with the session that establishes. If there's
|
||||||
|
// no cached initialize yet (this failure IS the very first call, or is
|
||||||
|
// itself the initialize), skip straight to the plain retry below.
|
||||||
|
if (msg.method !== "initialize" && lastInitializeMsg) {
|
||||||
|
try {
|
||||||
|
await forwardOnce(lastInitializeMsg);
|
||||||
|
} catch (initErr) {
|
||||||
|
process.stderr.write(`[bc-mcp-bridge] re-initialize failed: ${initErr.message || initErr}\n`);
|
||||||
|
// Fall through anyway -- retrying the original message will still
|
||||||
|
// surface a clear, real error if the session truly can't be restored.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = await forwardOnce(msg);
|
||||||
|
lastActivityAt = Date.now();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Splits text into chunks of max maxLen chars, breaking at word boundaries.
|
// Splits text into chunks of max maxLen chars, breaking at word boundaries.
|
||||||
function splitTextToChunks(text, maxLen = 250) {
|
function splitTextToChunks(text, maxLen = 250) {
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
kind: action-skill
|
kind: action-skill
|
||||||
id: curabis-standard-setup
|
id: curabis-standard-setup
|
||||||
version: 24
|
version: 25
|
||||||
title: CURABIS Standard — Project Setup
|
title: CURABIS Standard — Project Setup
|
||||||
description: >
|
description: >
|
||||||
Configures a new or existing repository to the CURABIS Standard development
|
Configures a new or existing repository to the CURABIS Standard development
|
||||||
|
|
@ -43,6 +43,46 @@ This agent runs when the developer says any of:
|
||||||
|
|
||||||
Detect which mode based on the trigger phrase and proceed accordingly.
|
Detect which mode based on the trigger phrase and proceed accordingly.
|
||||||
|
|
||||||
|
## Company policy — glide through when there's no real decision (2026-08-04)
|
||||||
|
|
||||||
|
Observed live: a consultant onboarding a machine hit "no" on a setup
|
||||||
|
confirmation reflexively while moving fast, and only noticed afterward. Most
|
||||||
|
of this document's `ja/nej` gates aren't actual choices — the standard is
|
||||||
|
company policy, not a per-developer preference, and a blocking question
|
||||||
|
where there's only one sane answer just invites exactly that kind of
|
||||||
|
misclick.
|
||||||
|
|
||||||
|
The rule going forward, applied throughout this document:
|
||||||
|
|
||||||
|
- **No real decision + no real risk → don't ask.** If the file is
|
||||||
|
git-tracked (repo history is the safety net) and the replacement content
|
||||||
|
is already verified correct/present, act directly and report what
|
||||||
|
changed. This covers: the v6-era repo-mirror cleanup, CLAUDE.md's
|
||||||
|
obsolete-forms replacement, and the v24 migration's repo-local agent-file
|
||||||
|
removal — all git-committed, all fully recoverable via `git revert` if
|
||||||
|
anyone ever wants the old version back.
|
||||||
|
- **Genuine irreducible ambiguity → don't ask either; default to the safe
|
||||||
|
side and make it visible.** `.mcp.json`'s and `.claude/settings.json`'s
|
||||||
|
multi-developer migration steps are the one real exception: whether every
|
||||||
|
developer on a repo has migrated their own machine is a fact only a human
|
||||||
|
knows, and Claude cannot verify it. But per this policy, that still isn't
|
||||||
|
a reason to interrupt setup with a blocking question — leave the legacy
|
||||||
|
entries in place by default (already documented as harmless, not a fault
|
||||||
|
state), report the finding, and offer removal as a separate, explicitly-
|
||||||
|
requested action later, once a developer confirms the migration is
|
||||||
|
actually complete.
|
||||||
|
- **Real risk of losing uncommitted work → still stop and ask.** This is
|
||||||
|
the one case that's a genuine decision: if a file has actual uncommitted
|
||||||
|
local changes, a human has to decide what happens to them; that isn't
|
||||||
|
something to glide past.
|
||||||
|
- **The developer's personal machine file (`~/.claude/CLAUDE.md`) still
|
||||||
|
gets asked.** It's not git-tracked and carries the developer's own
|
||||||
|
Identity section — there's no VCS safety net the way there is for
|
||||||
|
repo-committed files.
|
||||||
|
- **Committing on the developer's behalf stays a real question** (Step 5,
|
||||||
|
and the post-update commit prompt) — timing of a commit is a legitimate
|
||||||
|
choice, not company policy with one right answer.
|
||||||
|
|
||||||
## Source: the channel clone (BCQuality is PRIVATE — no raw URLs)
|
## Source: the channel clone (BCQuality is PRIVATE — no raw URLs)
|
||||||
|
|
||||||
All artifacts come from the machine's **channel clone** — a git clone of
|
All artifacts come from the machine's **channel clone** — a git clone of
|
||||||
|
|
@ -73,6 +113,7 @@ old HTTP-encoding pitfalls do not exist here).
|
||||||
|---|---|
|
|---|---|
|
||||||
| bc-mcp-bridge.js | `{BASE}/bc-mcp-bridge.js` |
|
| bc-mcp-bridge.js | `{BASE}/bc-mcp-bridge.js` |
|
||||||
| bc-mcp.config.template.json | `{BASE}/machine/bc-mcp.config.template.json` |
|
| bc-mcp.config.template.json | `{BASE}/machine/bc-mcp.config.template.json` |
|
||||||
|
| settings.json (permissions template) | `{BASE}/machine/settings.json` (merged into `~/.claude/settings.json`, never overwritten wholesale — see Step 3c) |
|
||||||
| bcquality.agent.md | `{BASE}/templates/bcquality.agent.md` |
|
| bcquality.agent.md | `{BASE}/templates/bcquality.agent.md` |
|
||||||
| immanuel.agent.md | `{AGENTS_BASE}/immanuel.agent.md` |
|
| immanuel.agent.md | `{AGENTS_BASE}/immanuel.agent.md` |
|
||||||
| carlin.agent.md | `{AGENTS_BASE}/carlin.agent.md` |
|
| carlin.agent.md | `{AGENTS_BASE}/carlin.agent.md` |
|
||||||
|
|
@ -161,8 +202,12 @@ If any of the above machine-level artifacts are missing, Step 3 will deploy them
|
||||||
`sync-bcquality-knowledge.ps1` — no separate action needed here beyond noting it in
|
`sync-bcquality-knowledge.ps1` — no separate action needed here beyond noting it in
|
||||||
the setup report.
|
the setup report.
|
||||||
|
|
||||||
If `CLAUDE.md` already exists, ask: "CLAUDE.md eksisterer allerede. Overskrive? (ja/nej)"
|
If `CLAUDE.md` already exists: check `git status` for it specifically. If it
|
||||||
Stop if the developer answers no.
|
has uncommitted local changes, stop and tell the developer to commit or
|
||||||
|
stash first — that is real, potentially-lost work, a genuine decision, not
|
||||||
|
a policy gate. Otherwise (clean, or already tracked with nothing pending),
|
||||||
|
overwrite it directly and report what changed — no `ja/nej` needed, per the
|
||||||
|
company policy above; git history already protects the old version.
|
||||||
|
|
||||||
### Step 1b — Structural readiness: raise the flags (Rømer)
|
### Step 1b — Structural readiness: raise the flags (Rømer)
|
||||||
|
|
||||||
|
|
@ -181,9 +226,13 @@ Report every finding immediately and prominently:
|
||||||
- <manglende test-app for <App>: kør CreateTestApp-workflowet>
|
- <manglende test-app for <App>: kør CreateTestApp-workflowet>
|
||||||
```
|
```
|
||||||
|
|
||||||
Setup MAY continue on a non-compliant repo — but the flags go in the setup
|
Setup MAY continue on a non-compliant repo — the flags go in the setup
|
||||||
report, and the developer must acknowledge them before Step 2. Never
|
report, prominently, and setup continues directly to Step 2 without waiting
|
||||||
restructure silently; migration is a deliberate, planned change.
|
for an acknowledgment (company policy above: reporting the flag IS the
|
||||||
|
record, and a blocking `ja/nej` here has no real decision behind it since
|
||||||
|
setup proceeds regardless of the answer). Never restructure silently,
|
||||||
|
though — actually migrating the repo's structure stays a separate,
|
||||||
|
deliberate, explicitly-requested action.
|
||||||
|
|
||||||
### Step 2 — Ask exactly three questions
|
### Step 2 — Ask exactly three questions
|
||||||
|
|
||||||
|
|
@ -621,16 +670,18 @@ Projects configured under setup v6 have the mirror committed INSIDE the repo.
|
||||||
Detect and clean up:
|
Detect and clean up:
|
||||||
|
|
||||||
1. If `.github/.agents/bcquality-knowledge/` exists in the repo (tracked or not),
|
1. If `.github/.agents/bcquality-knowledge/` exists in the repo (tracked or not),
|
||||||
propose removing it — ask for confirmation first:
|
remove it directly and report it — no `ja/nej` (company policy above:
|
||||||
|
git-tracked, fully recoverable via `git revert`, and the machine-local
|
||||||
|
mirror it's replaced by is already confirmed present earlier in this
|
||||||
|
same run):
|
||||||
|
|
||||||
```
|
```
|
||||||
⚠️ Dette repo indeholder en v6-æra repo-lokal BCQuality-mirror
|
ℹ️ Dette repo indeholdt en v6-æra repo-lokal BCQuality-mirror
|
||||||
(.github/.agents/bcquality-knowledge/, ~[antal] filer). Standarden er nu
|
(.github/.agents/bcquality-knowledge/, ~[antal] filer) — fjernet.
|
||||||
maskin-lokal mirror (~/.claude/bcquality-knowledge/). Må jeg fjerne
|
Standarden er nu maskin-lokal mirror (~/.claude/bcquality-knowledge/).
|
||||||
repo-mirroren og gitignore stien? (ja/nej)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
On yes: `git rm -r --cached .github/.agents/bcquality-knowledge/` (if tracked),
|
`git rm -r --cached .github/.agents/bcquality-knowledge/` (if tracked),
|
||||||
delete the folder, delete `.github/.agents/sync-bcquality-knowledge.ps1` (its
|
delete the folder, delete `.github/.agents/sync-bcquality-knowledge.ps1` (its
|
||||||
`$PSScriptRoot`-relative destination is what created the repo mirror), and add
|
`$PSScriptRoot`-relative destination is what created the repo mirror), and add
|
||||||
`.github/.agents/bcquality-knowledge/` to `.gitignore`.
|
`.github/.agents/bcquality-knowledge/` to `.gitignore`.
|
||||||
|
|
@ -643,9 +694,10 @@ Detect and clean up:
|
||||||
- ANY remaining `raw.githubusercontent.com`-based self-heal or onboarding
|
- ANY remaining `raw.githubusercontent.com`-based self-heal or onboarding
|
||||||
command (v15-v18 era) — the repo is private; raw URLs are dead. The
|
command (v15-v18 era) — the repo is private; raw URLs are dead. The
|
||||||
current form is git-based via the channel clone.
|
current form is git-based via the channel clone.
|
||||||
If any match, propose replacing the section with the current template from
|
If any match, replace the section with the current template from Step 4a
|
||||||
Step 4a and ask for confirmation before editing CLAUDE.md (same confirmation
|
directly and report it — no confirmation needed (company policy above:
|
||||||
gate as the agent-synligheds-check below).
|
these forms are objectively obsolete, CLAUDE.md is git-tracked, and this
|
||||||
|
only touches the flagged section, never project-specific content).
|
||||||
|
|
||||||
### Machine CLAUDE.md refresh (Mode B)
|
### Machine CLAUDE.md refresh (Mode B)
|
||||||
|
|
||||||
|
|
@ -659,33 +711,39 @@ structurally (e.g. still reference raw URLs or GitHub-API SHA checks, or are
|
||||||
missing the v24 "CURABIS Standard — Shared Roster" section entirely),
|
missing the v24 "CURABIS Standard — Shared Roster" section entirely),
|
||||||
propose the update — show the diff, preserve the Identity section verbatim,
|
propose the update — show the diff, preserve the Identity section verbatim,
|
||||||
and ask for confirmation before editing: it is the developer's personal file.
|
and ask for confirmation before editing: it is the developer's personal file.
|
||||||
|
Unlike the repo-committed files above, this one stays a real question under
|
||||||
|
the company policy above — it isn't git-tracked, so there's no revert
|
||||||
|
safety net, and it carries the developer's own personal customizations
|
||||||
|
mixed in alongside the CURABIS sections.
|
||||||
|
|
||||||
### v23 → v24 migration (existing repos, one-time per repo)
|
### v23 → v24 migration (existing repos, one-time per repo)
|
||||||
|
|
||||||
v24 moved 19 agent files, `.mcp.json`'s two standard entries, and
|
v24 moved 19 agent files, `.mcp.json`'s two standard entries, and
|
||||||
`find-altool.ps1` from repo-local to machine-global (see the Source section's
|
`find-altool.ps1` from repo-local to machine-global (see the Source section's
|
||||||
"machine vs. repo split" note). A repo configured under v23 or earlier still
|
"machine vs. repo split" note). A repo configured under v23 or earlier still
|
||||||
has the old repo-local copies. Detect and migrate — always confirm before
|
has the old repo-local copies. Detect and migrate. Steps 1, 2, and 4 below
|
||||||
removing anything, same gate as the v6-era cleanup above:
|
act directly and report, per the company policy above (git-tracked,
|
||||||
|
verified-safe); step 3 is the one genuine multi-developer ambiguity and
|
||||||
|
defaults to leaving things in place, not asking:
|
||||||
|
|
||||||
**1. Extra `.github/.agents/*.agent.md` files**
|
**1. Extra `.github/.agents/*.agent.md` files**
|
||||||
|
|
||||||
List `.github/.agents/*.agent.md`. Anything other than `bcquality.agent.md`
|
List `.github/.agents/*.agent.md`. Anything other than `bcquality.agent.md`
|
||||||
and `feynman.agent.md` is a pre-v24 repo-local copy of a now-machine-global
|
and `feynman.agent.md` is a pre-v24 repo-local copy of a now-machine-global
|
||||||
agent. Before proposing removal, confirm Step 3c has run on THIS machine in
|
agent. Before removing, confirm Step 3c has run on THIS machine in THIS
|
||||||
THIS Mode B pass (it always does, earlier in this flow) — that guarantees
|
Mode B pass (it always does, earlier in this flow) — that guarantees the
|
||||||
the roster is available globally before the repo-local copies disappear.
|
roster is available globally before the repo-local copies disappear. Then
|
||||||
|
remove and report directly, no `ja/nej`:
|
||||||
|
|
||||||
```
|
```
|
||||||
⚠️ v24-migrering: dette repo har [N] agent-filer i .github/.agents/ som nu er
|
ℹ️ v24-migrering: [N] agent-filer i .github/.agents/ er nu maskin-globale
|
||||||
maskin-globale (~/.claude/curabis-agents/ + ~/.claude/agents/florence.md).
|
(~/.claude/curabis-agents/ + ~/.claude/agents/florence.md) — fjernet, siden
|
||||||
Maskinen her har allerede den globale roster (bekræftet i dette Mode B-kald).
|
maskinen her allerede har den globale roster (bekræftet i dette Mode B-kald).
|
||||||
Må jeg fjerne de [N] repo-lokale kopier? (ja/nej)
|
|
||||||
|
|
||||||
- immanuel.agent.md, francis.agent.md, columbo.agent.md, ... [list them]
|
- immanuel.agent.md, francis.agent.md, columbo.agent.md, ... [list them]
|
||||||
```
|
```
|
||||||
|
|
||||||
On yes: `git rm` each file not in `{bcquality.agent.md, feynman.agent.md}`.
|
`git rm` each file not in `{bcquality.agent.md, feynman.agent.md}`.
|
||||||
|
|
||||||
**2. Old-style CLAUDE.md (inline generic sections)**
|
**2. Old-style CLAUDE.md (inline generic sections)**
|
||||||
|
|
||||||
|
|
@ -693,48 +751,88 @@ Check for any of these headings still present verbatim in the project
|
||||||
CLAUDE.md: `## Smiley — Session Watchdog`, `## Carlin — Bullshit Detector`,
|
CLAUDE.md: `## Smiley — Session Watchdog`, `## Carlin — Bullshit Detector`,
|
||||||
`## On-demand agents`, `## Francis — proaktiv regelobservation`,
|
`## On-demand agents`, `## Francis — proaktiv regelobservation`,
|
||||||
`## Shared project memory`, `## Project documentation`. Their presence means
|
`## Shared project memory`, `## Project documentation`. Their presence means
|
||||||
this repo predates v24. Propose REMOVING only those headings/sections and
|
this repo predates v24. Remove only those headings/sections directly and
|
||||||
replacing them with the short pointer paragraph from Step 4a. Do NOT touch
|
replace them with the short pointer paragraph from Step 4a — no `ja/nej`.
|
||||||
`## BCQuality` (the self-heal section) or `## Feynman — Support-sessioner` —
|
Do NOT touch `## BCQuality` (the self-heal section) or `## Feynman —
|
||||||
both stay, unchanged, in every version. Preserve everything project-specific
|
Support-sessioner` — both stay, unchanged, in every version. Preserve
|
||||||
(project name, AL_PROJECTS_SECTION, running-tests, about-this-project)
|
everything project-specific (project name, AL_PROJECTS_SECTION,
|
||||||
verbatim. Show the diff and ask for confirmation before editing (same gate
|
running-tests, about-this-project) verbatim. Report the diff after editing,
|
||||||
as the v6-era obsolete-forms check above).
|
same as the v6-era obsolete-forms replacement above.
|
||||||
|
|
||||||
**3. `.mcp.json` — standard entries (multi-developer coordination required)**
|
**3. `.mcp.json` — standard entries (the one genuine ambiguity — default to leaving it)**
|
||||||
|
|
||||||
This is the one migration step that is NOT safe to do unilaterally from a
|
This is the one migration step that is NOT safe to do unilaterally from a
|
||||||
single Mode B run, because `.mcp.json` is git-committed and shared: removing
|
single Mode B run, because `.mcp.json` is git-committed and shared: removing
|
||||||
it assumes EVERY developer working on this repo has already run Step 3c on
|
it assumes EVERY developer working on this repo has already run Step 3c on
|
||||||
their OWN machine. Doing this before that is true silently breaks AL/BC MCP
|
their OWN machine. Doing this before that is true silently breaks AL/BC MCP
|
||||||
for anyone who pulls the change and hasn't migrated yet.
|
for anyone who pulls the change and hasn't migrated yet — and whether every
|
||||||
|
developer has migrated is a fact only a human knows, not something Claude
|
||||||
|
can verify. Per the company policy above, that's still not a reason to
|
||||||
|
interrupt setup with a blocking question:
|
||||||
|
|
||||||
If `.mcp.json` contains the standard `al` and/or `businesscentral` entries:
|
If `.mcp.json` contains the standard `al` and/or `businesscentral` entries,
|
||||||
|
leave them in place by default and report it — no `ja/nej`:
|
||||||
|
|
||||||
```
|
```
|
||||||
⚠️ .mcp.json indeholder de to standard MCP-servere (al, businesscentral), som
|
ℹ️ .mcp.json indeholder stadig de to standard MCP-servere (al, businesscentral)
|
||||||
i v24 er maskin-globale i stedet. At fjerne dem fra .mcp.json er kun sikkert
|
fra før v24 — det er maskin-globalt nu, men filen skader ikke noget stående
|
||||||
naar ALLE udviklere paa dette repo har koert maskin-opsaetningen (Step 3c) paa
|
som den er (duplikeret konfiguration, ikke en fejltilstand). Fjernes kun når
|
||||||
egen maskine - ellers mister de AL/BC MCP naar de henter aendringen.
|
du eksplicit bekræfter at ALLE udviklere på repoet er migreret — sig til når
|
||||||
|
det er tilfældet.
|
||||||
Har alle udviklere paa dette repo allerede migreret deres maskine? (ja/nej)
|
|
||||||
Hvis usikker: svar nej - .mcp.json kan blive staaende uden problemer, det er
|
|
||||||
kun en smule duplikeret konfiguration, ikke en fejltilstand.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Only remove the two entries (never the whole file — a repo may have
|
Only remove the two entries (never the whole file — a repo may have
|
||||||
legitimate additional MCP servers) if the developer explicitly confirms yes.
|
legitimate additional MCP servers) when a developer separately and
|
||||||
If the file becomes empty afterward (`{"mcpServers": {}}`), propose deleting
|
explicitly confirms every developer on the repo has migrated — never as
|
||||||
`.mcp.json` entirely in the same confirmation.
|
part of this automatic setup/update flow itself. If the file becomes empty
|
||||||
|
afterward (`{"mcpServers": {}}`), offer deleting `.mcp.json` entirely at
|
||||||
|
that same later point.
|
||||||
|
|
||||||
**4. `.vscode/find-altool.ps1`**
|
**4. `.vscode/find-altool.ps1`**
|
||||||
|
|
||||||
If present, propose removal — it is superseded by `~/.claude/find-altool.ps1`
|
If present, remove it directly and report it — no `ja/nej`. It is
|
||||||
(machine-global, cwd-walkup discovery, no repo dependency). Safe to remove
|
superseded by `~/.claude/find-altool.ps1` (machine-global, cwd-walkup
|
||||||
|
discovery, no repo dependency), git-tracked, and safe to remove
|
||||||
independently of the `.mcp.json` migration above, since removing the *file*
|
independently of the `.mcp.json` migration above, since removing the *file*
|
||||||
doesn't affect any `.mcp.json` entry that still references the old
|
doesn't affect any `.mcp.json` entry that still references the old
|
||||||
repo-relative walk-up form until that entry itself is migrated per step 3.
|
repo-relative walk-up form until that entry itself is migrated per step 3.
|
||||||
|
|
||||||
|
**5. `.claude/settings.json` — legacy repo-committed permissions block
|
||||||
|
(2026-08-03, same genuine ambiguity as step 3 — default to leaving it)**
|
||||||
|
|
||||||
|
A repo from before the BC MCP static-tool-mode migration (2026-08-03) may
|
||||||
|
have a git-committed `.claude/settings.json` with a `permissions.allow`
|
||||||
|
block naming the OLD Dynamic Tool Mode tool names
|
||||||
|
(`mcp__businesscentral__bc_actions_search` / `bc_actions_describe` /
|
||||||
|
`bc_actions_invoke`) and/or an `enabledMcpjsonServers` entry for `al` /
|
||||||
|
`businesscentral`. Found live in the `Wareco` repo during a full developer
|
||||||
|
onboarding: the stale `enabledMcpjsonServers` entry causes the MCP servers
|
||||||
|
panel to show `al`/`businesscentral` duplicated under "Project" scope
|
||||||
|
alongside the correct "User" scope registration — confusing, and every
|
||||||
|
other CURABIS repo from before the migration likely has the same file.
|
||||||
|
|
||||||
|
If `.claude/settings.json` contains `bc_actions_search`, `bc_actions_describe`,
|
||||||
|
`bc_actions_invoke`, or an `enabledMcpjsonServers` entry for `al`/`businesscentral`,
|
||||||
|
leave it in place by default and report it — no `ja/nej`, same reasoning as
|
||||||
|
step 3 (a developer who hasn't migrated their own machine's
|
||||||
|
`~/.claude/settings.json` yet would lose their auto-approvals if this were
|
||||||
|
pulled before they have, and Claude can't verify that from here):
|
||||||
|
|
||||||
|
```
|
||||||
|
ℹ️ .claude/settings.json indeholder en forældet tilladelsesliste fra før
|
||||||
|
static-tool-mode-migreringen (bc_actions_search/describe/invoke er de gamle
|
||||||
|
værktøjsnavne) og/eller enabledMcpjsonServers for al/businesscentral, som
|
||||||
|
duplikerer den korrekte user-scope-registrering under "Project" i MCP-panelet.
|
||||||
|
De aktuelle, korrekte værktøjsnavne dækkes allerede af ~/.claude/settings.json
|
||||||
|
(maskin-globalt, Step 3c). Fjernes kun når du eksplicit bekræfter at ALLE
|
||||||
|
udviklere på repoet er migreret — sig til når det er tilfældet.
|
||||||
|
```
|
||||||
|
|
||||||
|
Only remove the stale entries once a developer separately and explicitly
|
||||||
|
confirms every developer on the repo has migrated — never as part of this
|
||||||
|
automatic setup/update flow itself. If the file becomes empty afterward,
|
||||||
|
offer deleting it entirely at that same later point.
|
||||||
|
|
||||||
### HEARTBEAT.md token substitution (Mode B)
|
### HEARTBEAT.md token substitution (Mode B)
|
||||||
|
|
||||||
When creating HEARTBEAT.md from template in Mode B:
|
When creating HEARTBEAT.md from template in Mode B:
|
||||||
|
|
|
||||||
36
custom/setup/machine/settings.json
Normal file
36
custom/setup/machine/settings.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"mcp__businesscentral__List_ActiveTasks_PAG6102900",
|
||||||
|
"mcp__businesscentral__List_Consultants_PAG50009",
|
||||||
|
"mcp__businesscentral__List_NewTasks_PAG6102905",
|
||||||
|
"mcp__businesscentral__List_ProjectAIScores_PAG6102906",
|
||||||
|
"mcp__businesscentral__List_ProjectRepositories_PAG6102904",
|
||||||
|
"mcp__businesscentral__List_ProjectWeberScores_PAG6102908",
|
||||||
|
"mcp__businesscentral__List_Projects_PAG6102901",
|
||||||
|
"mcp__businesscentral__List_TaskComments_PAG6102902",
|
||||||
|
"mcp__businesscentral__Create_NewTask_PAG6102905",
|
||||||
|
"mcp__businesscentral__Create_ProjectAIScore_PAG6102906",
|
||||||
|
"mcp__businesscentral__Create_ProjectWeberScore_PAG6102908",
|
||||||
|
"mcp__businesscentral__Create_TaskComment_PAG6102902",
|
||||||
|
"mcp__businesscentral__Modify_ActiveTask_PAG6102900",
|
||||||
|
"mcp__businesscentral__Modify_ProjectRepository_PAG6102904",
|
||||||
|
"mcp__businesscentral__Modify_TaskComment_PAG6102902",
|
||||||
|
"mcp__al__al_addproject",
|
||||||
|
"mcp__al__al_auth_login",
|
||||||
|
"mcp__al__al_auth_logout",
|
||||||
|
"mcp__al__al_build",
|
||||||
|
"mcp__al__al_compile",
|
||||||
|
"mcp__al__al_downloadsymbols",
|
||||||
|
"mcp__al__al_getdiagnostics",
|
||||||
|
"mcp__al__al_getpackagedependencies",
|
||||||
|
"mcp__al__al_publish",
|
||||||
|
"mcp__al__al_run_tests",
|
||||||
|
"mcp__al__al_symbolsearch",
|
||||||
|
"mcp__al__al_symbolrelations",
|
||||||
|
"mcp__microsoft-learn__microsoft_docs_search",
|
||||||
|
"mcp__microsoft-learn__microsoft_code_sample_search",
|
||||||
|
"mcp__microsoft-learn__microsoft_docs_fetch"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -222,3 +222,62 @@ function Ensure-UserMcpHttpServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ensure-UserMcpHttpServer -Name 'microsoft-learn' -Url 'https://learn.microsoft.com/api/mcp'
|
Ensure-UserMcpHttpServer -Name 'microsoft-learn' -Url 'https://learn.microsoft.com/api/mcp'
|
||||||
|
|
||||||
|
# --- 10. Permissions allowlist -> ~/.claude/settings.json (MERGE, never overwrite) ---
|
||||||
|
# Unlike the other artifacts above, settings.json is not pure BCQuality content -
|
||||||
|
# it also carries a developer's personal settings (theme, model, hooks, etc).
|
||||||
|
# Only merge the permissions.allow entries from the template below into whatever
|
||||||
|
# already exists; never replace the file wholesale. Scope: the three CURABIS-
|
||||||
|
# managed MCP servers only (businesscentral, al, microsoft-learn) - all either
|
||||||
|
# read-only or already gated by Smiley's own protocol checks (red/green
|
||||||
|
# confirmation, independent review), so the tool-permission prompt is redundant
|
||||||
|
# friction here, not a real safety boundary. 2026-08-03: added after a developer
|
||||||
|
# had to click through the same MCP approval prompts repeatedly across sessions.
|
||||||
|
$settingsTemplate = Join-Path $clone 'custom\setup\machine\settings.json'
|
||||||
|
$settingsDest = Join-Path $env:USERPROFILE '.claude\settings.json'
|
||||||
|
|
||||||
|
if (Test-Path $settingsTemplate) {
|
||||||
|
# NB: -AsHashtable (ConvertFrom-Json) findes kun i PowerShell 6+. Dette script
|
||||||
|
# koeres ogsaa via `powershell` (Windows PowerShell 5.1) paa udviklermaskiner,
|
||||||
|
# saa vi bruger PSCustomObject + Add-Member i stedet - virker paa begge.
|
||||||
|
$templateAllow = (Get-Content $settingsTemplate -Raw | ConvertFrom-Json).permissions.allow
|
||||||
|
|
||||||
|
$settings = $null
|
||||||
|
if (Test-Path $settingsDest) {
|
||||||
|
$raw = Get-Content $settingsDest -Raw
|
||||||
|
if ($raw -and $raw.Trim()) {
|
||||||
|
$settings = $raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $settings) { $settings = [PSCustomObject]@{} }
|
||||||
|
|
||||||
|
if (-not (Get-Member -InputObject $settings -Name 'permissions' -MemberType NoteProperty)) {
|
||||||
|
$settings | Add-Member -MemberType NoteProperty -Name 'permissions' -Value ([PSCustomObject]@{})
|
||||||
|
}
|
||||||
|
if (-not (Get-Member -InputObject $settings.permissions -Name 'allow' -MemberType NoteProperty)) {
|
||||||
|
$settings.permissions | Add-Member -MemberType NoteProperty -Name 'allow' -Value @()
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingAllow = [System.Collections.Generic.List[string]]::new()
|
||||||
|
foreach ($r in @($settings.permissions.allow)) { $existingAllow.Add([string]$r) }
|
||||||
|
|
||||||
|
$added = 0
|
||||||
|
foreach ($rule in $templateAllow) {
|
||||||
|
if ($existingAllow -notcontains $rule) {
|
||||||
|
$existingAllow.Add($rule)
|
||||||
|
$added++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$settings.permissions.allow = $existingAllow.ToArray()
|
||||||
|
|
||||||
|
# NB: Set-Content -Encoding utf8 skriver en BOM i Windows PowerShell 5.1 (ingen
|
||||||
|
# utf8NoBOM-mulighed der) - fundet under test 2026-08-03: settings.json havde
|
||||||
|
# ingen BOM originalt, og en tilfoejet BOM kan knaekke JSON-parsere der laeser
|
||||||
|
# filen. [System.IO.File]::WriteAllText med en explicit no-BOM UTF8Encoding
|
||||||
|
# virker identisk paa PS5.1 og PS7.
|
||||||
|
$json = $settings | ConvertTo-Json -Depth 20
|
||||||
|
[System.IO.File]::WriteAllText($settingsDest, $json, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
Write-Host "Permissions-allowlist merged ind i $settingsDest - $added ny(e) regel(er) tilfoejet."
|
||||||
|
} else {
|
||||||
|
Write-Warning "settings.json-skabelon ikke fundet i klonen, springer over."
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue