# Script-Artifact Invalidation Contract (`ScriptArtifactsChanged`) **Status:** Seam + first producer shipped in PLAN-05 (T14). **Consumer (a) shipped in PLAN-R2-06 T1** (`ScriptArtifactChangeSubscriber`). (b) Management producers remain unwired by design — delete calls `InvalidateMethod` directly; edits self-heal via the revision check. (c) is satisfied by the per-request revision check (PLAN-06 T1). **Problem.** When a script-bearing artifact's definition changes at runtime — an Inbound API method's script, a shared script, or a template's scripts — any node that has already **compiled and cached** a handler/delegate for it keeps serving the stale version until a process restart. This is the root of two recorded project-memory gotchas (a DB-direct edit to a broken inbound method keeps returning "Script compilation failed" from `_knownBadMethods`; a non-compiling update leaves the old delegate serving). A bundle import is a third mutation path with the same hazard. The fix is a small, explicit contract for "a script artifact changed → invalidate anything compiled from it," decoupled so producers (importer, management edits, failover) and consumers (compiled-handler caches) never reference each other. ## The seam (Commons) ```csharp // Commons/Messages/Scripting/ScriptArtifactsChanged.cs public sealed record ScriptArtifactsChanged( string ArtifactKind, // ScriptArtifactKinds.ApiMethod | SharedScript | Template IReadOnlyList Names, // post-resolution (renamed) names string Source, // "BundleImport" | "Management" | "FailoverActivation" DateTimeOffset OccurredAtUtc); public static class ScriptArtifactKinds { public const string ApiMethod = "ApiMethod"; public const string SharedScript = "SharedScript"; public const string Template = "Template"; } // Commons/Interfaces/Scripting/IScriptArtifactChangeBus.cs public interface IScriptArtifactChangeBus { void Publish(ScriptArtifactsChanged notification); IDisposable Subscribe(Action handler); } ``` ## Semantics (normative) 1. **Advisory, at-least-once.** A notification is a hint to invalidate, not a command with delivery guarantees. Duplicates are allowed; over-notification (e.g. an `Overwrite` resolution that actually fell through to `Add`) is allowed and harmless. 2. **Published only AFTER commit.** A producer publishes *after* its mutating transaction commits — never inside it. A notification for a rolled-back write is worse than a missed one. 3. **Never faults the producer.** The bus swallows-and-logs any subscriber-handler exception; the producer additionally guards its own publish call. A misbehaving consumer can never turn a committed write into a reported failure. 4. **In-process, per node.** `InProcessScriptArtifactChangeBus` (Host) is node-local: a lock-free copy-on-write subscriber list. It does **not** cross the cluster. Cross-node and failover freshness are the **consumer's** responsibility — every consumer MUST also self-heal without a notification (e.g. a content-hash / revision check on next use), because a notification published on node A does not reach node B, and a node that was down during the change never hears it. 5. **Names are post-resolution.** `Names` carries the artifact's persisted name after import name-resolution (a `Rename` carries `RenameTo`). ## What plan 05 ships - The Commons seam (record + kinds + interface). - `InProcessScriptArtifactChangeBus` in the Host, registered as a central-role singleton. - The **Transport bundle importer as the first producer**: `BundleImporter.ApplyAsync` collects the Overwrite/Rename resolutions for `ApiMethod` / `SharedScript` / `Template` and publishes one `ScriptArtifactsChanged` per kind **after** `tx.CommitAsync`. The importer's dependency on the bus is optional (`IScriptArtifactChangeBus?`) so Transport-only hosts (and unit tests) run without it. ## Handoff to plan 06 — dispositions | Item | Disposition | |---|---| | **(a) Inbound API consumer** | **Shipped (PLAN-R2-06 T1).** `ScriptArtifactChangeSubscriber` (an InboundAPI `IHostedService` registered by `AddInboundAPI`) subscribes to the bus and, on an `ApiMethod` notification, calls `InboundScriptExecutor.InvalidateMethod` per name — evicting the named entries from `_scriptHandlers` **and** `_knownBadMethods`, closing the "DB-direct edit keeps returning compile error" and "non-compiling update keeps old handler" gotchas. | | **(b) Management producers** | **Unwired by design.** The `ManagementActor` delete path calls `InboundScriptExecutor.InvalidateMethod` directly; the update/edit paths self-heal via the per-request revision check, so no notification is needed for correctness. Left unwired deliberately rather than publishing `Source = "Management"` notifications. | | **(c) Cross-node / failover freshness** | **Satisfied by the per-request revision check (PLAN-06 T1).** The compiled handler is revision-checked against the freshly-fetched `ApiMethod.Script` on every request, so a node that missed the in-process notification still self-heals on next use, and a newly-active failover node never serves a stale compile. | Consumer (a) is now wired, so plan 05's `ApiMethod` publisher reaches a real subscriber. `SharedScript`/`Template` notifications still publish with **no consumer** — that is safe: nothing at central caches compiled artifacts of those kinds, so there is nothing to invalidate. The seam is stable and the producer is correct.