AKML SQL — Architecture Overview
1. High-Level Component Map
┌─────────────────────────────────────────────────────────────────────────┐
│ HOST IDEs (SSMS 22 · VS 2026) │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Shell Extension (.NET Framework 4.7.2 VSIX) │ │
│ │ │ │
│ │ AkmlSql.Shell.Shared (.projitems — shared source) │ │
│ │ ├── Commands/ Menu commands (Format, Analyze, …) │ │
│ │ ├── Dialogs/ WinForms UI (Settings, LogViewer, …) │ │
│ │ ├── Formatting/ Format-on-type/paste/save triggers │ │
│ │ ├── Analysis/ Squiggle / error-list integration │ │
│ │ ├── IntelliSense/ Native IntelliSense conflict manager │ │
│ │ ├── Update/ Update launcher & result reader │ │
│ │ └── Ipc/ PipeRpcClient + EngineProcessManager │ │
│ │ │ │
│ │ Compiled twice against each host's VS SDK: │ │
│ │ AkmlSql.Ssms22 (VS SDK 17.14, x64) │ │
│ │ AkmlSql.VS2026 (VS SDK 17.14, x64) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ Named Pipe (owner-SID ACL, MessagePack frames) │
└───────────────────────────────────────────────────────────────────────── ┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ AkmlSql.Engine (.NET 10, self-contained, single-file, win-x64) │
│ │
│ PipeRpcServer │
│ ├── SessionManager Active editor sessions + document text │
│ ├── TsqlParserService Thread-safe TSql170Parser wrapper │
│ ├── CompletionEngine IntelliSense completions │
│ │ ├── KeywordProvider │
│ │ ├── SchemaProvider Uses SchemaCacheManager │
│ │ ├── SnippetProvider │
│ │ └── FunctionProvider │
│ ├── SignatureProvider Parameter info │
│ ├── QuickInfoProvider Hover tooltips │
│ ├── FormatRequestHandler Delegates to FormatterPipeline │
│ │ └── BulkFormatter Parallel file-level formatting │
│ ├── SnippetRequestHandler Expand / list / save / delete │
│ │ ├── SnippetLoader Reads .akmlsnippet JSON files │
│ │ └── SnippetIndex Shortcode / category / search index │
│ ├── AnalysisEngine Static code analysis (70+ rules) │
│ │ ├── RuleRegistry Discovers all IAnalysisRule implementations │
│ │ ├── CaSettingsLoader Per-project .casettings overrides │
│ │ └── SuppressionMap -- akml-disable/enable comment parsing │
│ ├── RefactoringEngine Preview + apply refactoring operations │
│ │ ├── Lightweight ops Pure-text transforms (no schema needed) │
│ │ └── Heavyweight ops Schema-aware transforms │
│ └── SchemaCacheManager Server:database → DatabaseCache │
│ ├── SchemaMetadataService Phase A/B population via sys.* views │
│ └── ChangeDetector Periodic schema-change detection │
│ │
│ AkmlSql.Formatting (referenced by Engine) │
│ ├── FormatterPipeline 7-stage formatting pipeline │
│ ├── NoformatScanner -- noformat / -- endnoformat regions │
│ ├── SqlcmdPreprocessor :r / :setvar directive handling │
│ ├── AstAnnotator Attaches comments to AST nodes │
│ ├── LayoutEngine Builds whitespace/newline IR │
│ ├── CasingEngine Keyword & identifier casing │
│ ├── TextEmitter IR → final string │
│ ├── SemanticValidator Round-trips parse to verify equivalence │
│ └── ProfileManager Load / save / list .akmlstyle profiles │
└────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ AkmlSql.Core (netstandard2.0 + net10.0) — shared by all │
│ ├── IPC types RpcMessage, FrameProtocol, MessageTypes │
│ ├── Message models All request/response POCOs (MessagePack) │
│ ├── Config AppSettings, ConfigManager │
│ ├── Update models UpdateManifest, UpdateResult │
│ └── Logging LoggerFactory (Serilog wrapper) │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ AkmlSql.Updater (.NET 10, self-contained) │
│ Launched by shell → fetches manifest JSON → writes update-available.json│
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ AkmlSql.Installer (Inno Setup 7) │
│ Detects installed IDEs → copies extension files → clears MEF caches │
└─────────────────────────────────────────────────────────────────────────┘
2. Process Boundary: Shell ↔ Engine
The shell extension and the engine run in separate processes. The shell is hosted inside a .NET Framework 4.7.2 VS/SSMS process; the engine is a .NET 10 self-contained executable.
Why out-of-process?
| Concern | Solution |
|---|---|
| .NET Framework ↔ .NET 10 incompatibility | Separate processes, shared message protocol |
| Engine crash isolation | VS/SSMS never sees engine crashes |
| Native AOT / trimming | Engine can be trimmed; shell cannot |
| Dependency conflicts | Engine uses modern libs freely |
Pipe naming
akmlsql-engine-{user-SID}-{shell-PID}
Each shell process gets its own named pipe. The pipe ACL allows only the current user's SID and explicitly denies the Network SID.
Frame protocol
┌─────────┬─────────┬──────────────────────────────────┐
│ 4 bytes │ 4 bytes │ N bytes │
│ Length │ XOR CRC │ MessagePack(RpcMessage) │
└─────────┴─────────┴──────────────────────────────────┘
RpcMessage carries three fields:
MessageType(int) — identifies the operationRequestId(int) — correlates request to response (0 for notifications)Payload(byte[]) — MessagePack-serialized request or response object
Max frame size: 16 MB.
3. Shell Extension Startup Sequence
IDE loads VSIX
→ Package.InitializeAsync()
→ LoggerFactory.Initialize()
→ ConfigManager.Load()
→ LoadValidator.Validate() (extension files present?)
→ NativeIntelliSenseManager.CheckAndPromptOnFirstLoad()
→ EngineProcessManager.LaunchAsync() → spawns AkmlSql.Engine.exe
→ PipeRpcClient.ConnectAsync() (retry up to 5×, 200ms apart)
→ Register menu commands
→ Register text-view event handlers (format-on-type, analysis triggers)
→ SchemaCacheManager.StartPeriodicRefresh()
Engine crashes trigger automatic restart via Process.Exited event, up to 5 times.
4. IntelliSense Data Flow
User types SQL
→ AnalysisController.OnTextChanged() (debounced 300ms)
→ PipeRpcClient.SendAsync(RequestAnalyze)
→ Engine: AnalysisEngine.AnalyzeAsync()
→ Parse AST (TsqlParserService)
→ Run enabled rules against AST
→ Resolve suppressions
→ Return CodeAnalysisResponse
→ Shell: render squiggles + error list entries
User requests completion (Ctrl+Space / auto)
→ PipeRpcClient.SendAsync(RequestCompletion)
→ Engine: CompletionEngine.GetCompletions()
→ Parse tokens at cursor
→ Merge: keywords + schema objects + snippets + functions
→ Return CompletionResponse (ranked list)
→ Shell: show VS completion list
5. Schema Cache Lifecycle
ConnectionChanged notification
→ SchemaCacheManager.GetOrCreateCache(server, db)
→ If Phase == NotLoaded:
Task.Run → SchemaMetadataService.PopulatePhaseAAsync()
→ sys.objects + sys.schemas + sys.partitions (< 500ms target)
→ DatabaseCache.Phase = PhaseA
Background (after Phase A):
→ SchemaMetadataService.PopulatePhaseBAsync()
→ LoadColumnsAsync() (sys.columns + joins)
→ LoadForeignKeysAsync() → RebuildFkIndex()
→ LoadParametersAsync() (sys.parameters)
→ LoadDescriptionsAsync() (sys.extended_properties)
→ DatabaseCache.Phase = PhaseB
Periodic refresh (default: 60s):
→ ChangeDetector.CheckForChangesAsync()
→ CHECKSUM_AGG(BINARY_CHECKSUM(object_id, modify_date, create_date, type))
→ If checksum changed → cache.IsStale = true
DDL detection:
→ ChangeDetector.DetectDdlInQuery(queryText)
→ Regex: ^\s*(CREATE|ALTER|DROP)\s+(TABLE|VIEW|...)
→ If matched → trigger immediate Phase A refresh
6. Formatting Pipeline
Input SQL
Stage 0a: NoformatScanner → noformat regions
Stage 0b: SqlcmdPreprocessor → replace :r/:setvar with placeholders
Stage 1: TSql170Parser → TSqlScript AST
Stage 2: AstAnnotator → attach comments to AST nodes
Stage 3: LayoutEngine → LayoutNode list (tokens + whitespace rules)
Stage 4: CasingEngine → apply keyword/identifier case from profile
Stage 5: TextEmitter → formatted string
Stage 5b: SqlcmdPreprocessor → restore SQLCMD placeholders
Stage 6: SemanticValidator → re-parse & normalize both; compare
Stage 7: IdempotencyCheck → format again; verify identical output
Output FormattedSQL
Failed validation (stage 6) → returns original SQL unchanged. Failed idempotency (stage 7) → adds a diagnostic warning, still returns formatted SQL.
7. Update Flow
Shell startup (or user clicks "Check for Updates")
→ UpdateLauncher.LaunchUpdaterAndWait(15s timeout)
→ Spawns AkmlSql.Updater.exe --check
→ HTTPS GET https://updates.akmlsql.com/manifest.json
→ Compares manifest.Version with Constants.Version (SemVer)
→ If newer: writes %AppData%\AKML SQL\update-available.json
→ If same or older: deletes stale result file
Shell reads update-available.json
→ Shows "Update available" dialog if Available == true
→ User clicks Yes → opens DownloadUrl in default browser
8. Installer Flow
AKMLSQLSetup.exe
→ DetectInstalledIDEs() (registry + vswhere + filesystem)
→ For each detected target:
→ Copy extension files to IDE extensions directory
→ Clear MEF / component-model cache
→ Write %AppData%\AKML SQL\config.json (if absent)
→ Register uninstall entry
Silent mode flags:
/VERYSILENT /ACCEPTEULA /TARGETS=20,22,2022 /NOUPDATE
9. Spec 020 — SQL Prompt Visual Parity (Theme tokens, Format Styles editor)
Spec 020 layered on the existing theme + formatting infrastructure rather than replacing it. The new surface area:
Theme token system (US1, builds on spec 016)
ThemeRegistry was extended with 25 new brush tokens and 8 invariant scalar / typography tokens, all in src/AkmlSql.Shell.Shared/Ui/Theme/:
| Family | Count | Source of truth for hex |
|---|---|---|
IconBadge.* (Table, View, Column, StoredProc, Function, Snippet, Keyword, Database, Schema, Trigger, Index, Synonym) |
12 | doc/SQL-PROMPT/SQL-Prompt-Features/SQL_Prompt_Features_Core.md §1.2 |
TabColor.* (Red, Amber, Green, Blue, Teal, Purple, Pink, Gray) |
8 | EnvironmentMatcher default palette |
History.* (OpenIcon, ClosedIcon, StarActive, StarInactive, MatchHighlight) |
5 | doc/SQL-PROMPT/SQL-Prompt-History/SQL_Prompt_SQL_History.md §16.2 |
Spacing.* (XS / S / M / L scalars in DIU) |
4 | Theme-invariant; delegates to existing Spacing static class |
Typography.* (Chrome, ChromeTitle, Editor, IconBadge composites) |
4 | Theme-invariant; delegates to existing Typography static class |
ThemeMigrationManager.cs writes a one-time themeMigration.v1.json marker at first launch; on existing-customisation detection it surfaces a notice flag.
Format Styles editor (US3)
A new modal DialogWindow in src/AkmlSql.Shell.Shared/Formatting/:
| File | Role |
|---|---|
FormatStylesEditorWindow.cs |
Three-column shell: style list (left), settings tree (middle), controls + live preview (right). Programmatic WPF only (no XAML) per the ProfileEditorDialog pattern. |
FormatStylesEditorViewModel.cs |
Loads profiles via ProfileList IPC, schema via RequestStyleEditorSchema IPC, holds _workingValues overlaying schema defaults. Debounced QueuePreviewAsync (100 ms) drives the live preview. |
The editor is launched via FormatStylesEditorWindow.Launch() from: Tools → AKML SQL → "Format Styles..." (both hosts, cmdFormatStyles 0x0916), the SSMS DTE-injected fallback menu, the Command Palette (akml.formatStyles), and the Options Format › Styles page's "Edit formatting styles…" button (spec 033). Spec 033 also promoted the window from a preview-only browser to a full editor: load-on-select via ProfileGet (34) raw reads, dirty-tracked merge-saves through ProfileSave (15) preserving metadata/extension data, rename via ProfileRename (35), delete guarded against the active style, and a schema-v2 settings tree (5 SQL Prompt categories, enum dropdowns, per-setting descriptions).
SQL Prompt round-trip (US2)
src/AkmlSql.Formatting/Profiles/:
| File | Direction |
|---|---|
SqlPromptImporter.cs (pre-existing) |
.sqlpromptstylev2 XML → FormattingProfile |
SqlPromptExporter.cs (spec 020) |
FormattingProfile → .sqlpromptstylev2 XML |
FormatSettingSchema.cs (spec 020) |
Reflection-discovered descriptor; powers the editor tree |
IPC additions
| Message | Value | Purpose |
|---|---|---|
RequestStyleEditorSchema / StyleEditorSchemaResult |
28 / 128 | Schema descriptor request — see doc/ipc-api.md |
9b. Spec 021 — M0 Transport Abstraction
Spec 021 (web edition) introduced an IRpcTransport + IRpcRequestHandler<TRequest, TResponse> abstraction so the same engine handlers can serve named pipes (IDE plugins, today), in-process calls (Blazor WASM running engine logic in the browser tab; engine unit tests with zero serialisation), and WebSocket (future browser ↔ engine, M3+) without per-transport handler duplication. The spec 022 closure finished the deferred polish: a single composition root, the named-pipe transport trimmed to a reference-shape file, and the cached settings single-owned by RpcContext.
Wire format and message-type integer codes are unchanged — existing SSMS/VS shell extensions need zero updates after the M0 refactor.
New types (under src/AkmlSql.Engine/)
| Type | Path | Role |
|---|---|---|
IRpcTransport |
Transports/IRpcTransport.cs |
Frame I/O + lifecycle. One impl per medium. |
NamedPipeTransport |
Transports/NamedPipeTransport.cs |
Named-pipe accept loop, pipe ACL, framed read/write; implements IRpcTransport, raising RequestReceived for each decoded RpcMessage. 147 LOC — the M0 reference shape. |
InProcessTransport |
Transports/InProcessTransport.cs |
Method-call dispatch, no serialisation. Used by Blazor WASM and engine unit tests. |
WebSocketTransport |
Transports/WebSocketTransport.cs |
Localhost-by-default WebSocket bridge — see § 9d. |
IRpcRequestHandler<TRequest, TResponse> |
Transports/IRpcRequestHandler.cs |
One impl per message-type integer code. Two opt-in DIM properties: AllowsEmptyPayload (for messages with no payload, e.g. ProfileList) and SwallowCancellation (for handlers where OCE → null response is preferable to tearing down the pipe loop, e.g. AnalysisHandler). |
RpcRouter |
RpcRouter.cs |
Per-process dispatch surface. Register<,>(handler) wires a typed handler; RegisterRaw(code, func) wires a delegating handler that consumes/produces a raw RpcMessage itself. Resolves the message-type code, deserialises the payload, dispatches. Replaces the pre-closure _pluggableHandlers dictionary. |
RpcContext |
RpcContext.cs |
Per-process shared state passed to every handler: Sessions, SchemaCache, Logger, ParserService, SchemaMetadata, and — after the spec 022 closure — the sole owner of the cached AppSettings. Handlers read settings through EnsureSettings() (idempotent lazy load); the AnalysisSettingsChanged handler drops the cache via InvalidateSettings(). No transport holds a settings field. |
EngineComposition |
EngineComposition.cs |
The single composition root — see below. |
EngineHandlerRegistry |
EngineHandlerRegistry.cs |
Static handler-registration surface — see below. |
Handler folders
All ~50 dispatch handlers now live under src/AkmlSql.Engine/Handlers/ grouped by category:
Handlers/
├── Completion/ CompletionHandler, WildcardExpansionHandler, SignatureHelpHandler, QuickInfoHandler
├── Formatting/ FormattingHandlers (9 typed wrappers) + BulkFormatHandlers (2)
├── Analysis/ AnalysisHandler (+ SwallowCancellation=true), AnalysisSettingsChangedHandler
├── Snippets/ 5 handlers (Expand/List/Save/Delete/Import)
├── Refactoring/ RefactorPreviewHandler, RefactorApplyHandler (both SwallowCancellation=true)
├── Schema/ SchemaRefreshHandler, SchemaStatusHandler
├── Control/ DocumentChangedHandler, PingHandler, ShutdownHandler, ConnectionChangedHandler
└── Ai/ AiHandlerBase + 7 per-message handlers (TextToSql, Explain, Fix, Optimize, IndexAnalysis, Chat, GhostText)
NamedPipeTransport and the composition root (spec 022 closure)
The named-pipe transport is src/AkmlSql.Engine/Transports/NamedPipeTransport.cs — 147 LOC, within the M0 PRD's ≤ 150-LOC reference-shape target. It implements IRpcTransport (T027) like the in-process and WebSocket transports, owning only named-pipe concerns: the pipe ACL (CreatePipeSecurity), the accept loop (RunAsync / HandleClientAsync), framed read/write, and the dispatch hand-off — each decoded RpcMessage is raised via the RequestReceived event, with the composition root wiring RpcRouter.RouteAsync as the subscriber. No service-construction or handler-registration code lives in the transport.
Service construction and handler registration moved to two new files:
EngineComposition(src/AkmlSql.Engine/EngineComposition.cs) — the single composition root.EngineComposition.Build()constructs theRpcContext, anRpcRouterwith every handler registered, and theHistoryRetentionService, and returns the three.EngineHostcallsBuild()once at startup and handsContext+Routertonew NamedPipeTransport(...); the in-process and (future) WebSocket transports consume the sameBuild()output — no transport replicates wiring.EngineHandlerRegistry(src/AkmlSql.Engine/EngineHandlerRegistry.cs) — a static class whoseRegisterAllHandlers(router, ctx)is the one place every message type is wired: typed handlers viaRpcRouter.Register<,>, delegating handlers (session, history, navigation, productivity, …) viaRpcRouter.RegisterRaw.
The pre-closure _pluggableHandlers dictionary and the PipeRpcServer partial-class pair (PipeRpcServer.cs + PipeRpcServer.Handlers.cs) are gone — RpcRouter is the single dispatch surface.
Three helpers extracted during M0 stay extracted:
RpcResponseFactory(src/AkmlSql.Engine/RpcResponseFactory.cs) —CreateResponse<T>andCreateErrorResponse. Standalone static class.SchemaRefreshService(src/AkmlSql.Engine/Schema/SchemaRefreshService.cs) — handles the manualCtrl+Shift+Dschema refresh; wired intoSchemaRefreshHandler.FindFunctionAtCursor— folded intoSignatureHelpHandleras a private static helper.
Existing IDE-plugin path is byte-for-byte compatible
The shell extensions (SSMS 22, VS 2026) send the same MessagePack frames over the same named pipe with the same ACL. No shell code was modified. The frame format [length][CRC][MessagePack(RpcMessage)] is unchanged.
M0 closure (spec 022, 2026-05-20). The four M0 PRD success metrics deferred when M0 merged (PR #236) —
NamedPipeTransport≤ 150 LOC, the_cachedSettingsfield moved ontoRpcContext,AiHandlerBase+ 7 per-message subclasses, and the perf-regression gate at 5 % — all landed via spec 022. Seespecs/022-m0-engine-closure/and the closure plandocs/superpowers/plans/2026-05-19-m0-engine-transport-closure.md.
9c. Spec 021 — Library extraction (IntelliSense / Analysis / AI)
To let Blazor WASM run the formatter, analyser, IntelliSense, and AI prompt building in the browser tab (without an engine), three previously engine-internal subsystems were extracted into standalone net10.0 libraries:
| Library | Path | Source moved from | Notes |
|---|---|---|---|
AkmlSql.IntelliSense |
src/AkmlSql.IntelliSense/ |
src/AkmlSql.Engine/{Completion,Parser,Schema/{DatabaseCache,Models}} |
32 files. Namespaces preserved (AkmlSql.Engine.Completion.*) so engine call sites need zero updates. DatabaseProvider stayed in the engine because it depends on SqlClient; CompletionEngine no longer hard-codes RegisterProvider(new DatabaseProvider()) — the engine registers it externally on startup. |
AkmlSql.Analysis |
src/AkmlSql.Analysis/ |
src/AkmlSql.Engine/{Analysis,Rules} |
141 files including the 130+ rule classes. AnalysisEngine.AnalyzeAsync was refactored to take (int serverVersion, DatabaseCache? schemaCache) directly instead of SessionManager + SchemaCacheManager, so the library has no engine dependency. Callers resolve session/cache themselves. |
AkmlSql.AI |
src/AkmlSql.AI/ |
src/AkmlSql.Engine/Ai/{Prompts,Context,Privacy,Providers,Streaming} |
18 files. AiRequestHandler + AiProviderTestHandler + Security/CredentialManager stayed in the engine. Two decoupling refactors: AiProviderFactory.KeyDecryptor is a pluggable static Func<string?, string> delegate (engine wires CredentialManager.Decrypt at startup; web edition leaves the default identity since Web Crypto unwraps the key BEFORE calling the factory); SchemaContextBuilder takes a Func<string, string, DatabaseCache?> cacheLookup callback instead of SchemaCacheManager. |
Each library has a minimal smoke-test project under tests/AkmlSql.{IntelliSense,Analysis,AI}.Tests/ that proves the surface is reachable from a project that references only the new library — no transitive engine dependency. Full functional coverage stays in tests/AkmlSql.Engine.Tests/ via the transitive reference.
9d. Spec 021 — M3 WebSocket bridge
The web edition's browser tab talks to a local engine over a WebSocket. The bridge is localhost-only by default (HttpListener bound to 127.0.0.1); LAN mode requires an installer-generated self-signed TLS cert and the explicit "Network exposure: LAN" installer choice.
| Component | Path | Role |
|---|---|---|
WebSocketTransport |
src/AkmlSql.Engine/Transports/WebSocketTransport.cs |
HttpListener + System.Net.WebSockets.WebSocket. One WebSocket binary message = one RpcMessage MessagePack payload. Refuses non-loopback binding unless TlsCertPath is set. |
HandshakeHandler |
src/AkmlSql.Engine/Handlers/Handshake/HandshakeHandler.cs |
First-frame dispatch (MessageTypes.HandshakeRequest = 200). Protocol-version overlap check; pairing PIN / bearer-token validation; capability advertisement. Parameterless ctor accepts any localhost inbound; full ctor takes callbacks for pairingRequired, pinValidator, bearerValidator, bearerMinter, and serverCanonicalIdentityProvider. |
PairingService |
src/AkmlSql.Engine/Pairing/PairingService.cs |
6-digit numeric PIN, 24-hour TTL, 5-attempts-per-minute sliding-window rate limit, constant-time compare. Emits a PinChanged event for the (deferred) tray-UI surface. |
BearerTokenStore |
src/AkmlSql.Engine/Pairing/BearerTokenStore.cs |
SHA-256 hashes at rest only — raw tokens never touch disk. Atomic temp-file-plus-rename persistence. |
Capabilities |
src/AkmlSql.Engine/Capabilities.cs |
Defines the stable capability identifiers (e.g. core.format.v1, schema.cache.v1) and the engine's currently-advertised list. The web client gates per-feature UI on the list — missing capability renders an inline notice, not a full-page blocker. |
Wire details (request/response shapes, status strings, capability table) are in doc/ipc-api.md § Spec 021 — Web Edition Bridge Messages.
Engine-host composition (spec 025 FR-027): both NamedPipeTransport and WebSocketTransport are started concurrently when config.Bridge.Enabled == true; both share the same RpcRouter so the SSMS plugin and the web edition serve identical handler chains. When the bridge section is absent or disabled, only the named pipe runs — IDE-plugin-only deployments are byte-for-byte unchanged.
LAN-mode TLS plumbing (spec 025 FR-001..FR-006): non-loopback bindings switch to https:// prefixes and consume the installer-bound cert (netsh http add sslcert ipport=…). The transport's startup-time ValidateCertBindingOrThrow asserts PFX existence + thumbprint match against the active netsh binding before opening the listener; mismatch throws with both thumbprints in the message. The validated thumbprint flows into every HandshakeResponse.ServerTlsThumbprint so the browser can pin it on first connect and log a Warn diagnostic on drift.
Threat model: see m3-security.md for the LAN-mode threat table, on-disk artefact audit, and the list of deferred follow-ups.
Reconnect, schema tree, and E2E (spec 025 US3–US5): EngineBridge runs an exponential-backoff reconnect loop (500 ms / 2× / 30 s cap / ±100 ms jitter) that replays the stored bearer on every retry and exits to Failed on a PinRequired response (the bearer was revoked) — the browser-side counterpart to the engine's BearerTokenStore.RevokeByHash. A SchemaTreeComponent renders the cached SchemaSnapshot (Database → Schema → Object-Kind → Object → Column) with click-to-insert into the editor, refreshed on ISchemaSync.ChecksumDrifted. A real-engine E2E harness (tests/AkmlSql.E2E.Tests/Harness/EngineLaunchFixture.cs) covers wire-level handshake / restart / backoff via BridgeHandshakeTests under the opt-in [Trait("Category","BridgeE2E")]; the harness builds the engine, redirects AppData via AKML_APP_DATA_ROOT, and probes WebSocket readiness on a per-test free port. Handshake registration fix: spec 025 also wired HandshakeHandler into EngineHandlerRegistry — the handler existed since spec 021 T060 but was never registered with the router, so any WebSocket inbound that depended on it (every browser handshake) silently timed out. The named-pipe transport doesn't run handshakes, which is why the bug stayed latent until the first end-to-end E2E test fired.
9e. Spec 021 — M5 schema-cache identity
The web edition's offline IntelliSense uses an IndexedDB schema cache keyed by the composite pair (serverCanonicalIdentity, databaseName). The pair is stable across host-string variations: two distinct DNS aliases pointing at the same SQL Server resolve to the same identity, so they share one cache entry (clarification 3 in spec.md).
The browser resolves the pair via two paths:
- Handshake response —
HandshakeResponse.ServerCanonicalIdentitycovers the single-DB case (engine has exactly one connection at handshake time). - SchemaIdentify request (
MessageTypes.SchemaIdentifyRequest= 202) — covers the multi-session case where the browser asks perSessionId.
The handler (Handlers/Schema/SchemaIdentifyHandler) is callback-pure (Func<string, string?> databaseLookup, Func<string, string?> identityResolver) so it unit-tests without a live SQL connection. Resolver exceptions are captured into the response's ErrorMessage rather than crashing the bridge.
Production wiring in EngineHandlerRegistry plugs SessionManager for databaseLookup and SchemaIdentifyHandlerSupport.ParseServerFromConnectionString (extracts Data Source / Server / Address from the SqlClient-style connection string) for the initial identityResolver. Swapping in the real SELECT @@SERVERNAME query is a follow-up — the handler surface does not change.
The schema.cache.v1 capability is advertised in Capabilities.Current to signal availability.
Contract details (IndexedDB layout, change-detection polling, eviction policy, online/offline matrix) live in specs/021-web-edition/contracts/schema-cache-shape.md.
10. Key Design Decisions
| Decision | Rationale |
|---|---|
| Out-of-process engine | .NET version isolation; crash safety; trimming |
| Shared .projitems | One source compiles against 6 different VS SDKs |
| MessagePack for IPC | ~3× faster + smaller than JSON; strongly typed |
| ConcurrentDictionary for schema cache | Lock-free reads; multiple background writers |
| Phase A / Phase B schema loading | Phase A < 500ms for fast first completion; Phase B in background |
| Atomic config writes (File.Replace / File.Move overwrite) | Prevents partial-write corruption |
| Named pipe + SID ACL | Local-only IPC; no network exposure |
| CHECKSUM_AGG for change detection | Single scalar query vs full re-read |
| ProfileMetadata.SkipValidation | Allows test pipelines to bypass semantic round-trip |
| EnableIdempotencyCheck flag | Allows bulk operations to skip the expensive second parse pass |
Spec 021 M0 IRpcTransport abstraction |
Same engine handlers serve named-pipe (IDE plugins) + in-process (Blazor WASM) + future WebSocket transports without per-transport duplication. Wire format unchanged for backward compat. |
Two opt-in DIM properties on IRpcRequestHandler<,> (AllowsEmptyPayload, SwallowCancellation) |
Lets specific handlers (ProfileList, AnalysisSettingsChanged; AnalysisHandler, RefactorPreview/Apply) opt out of default error-on-null-payload and OCE-propagation behaviour without polluting the contract for the common case. |