Sprint SPRINT_20260417_023_Concelier_truthful_affected_symbol_runtime.
UnsupportedAffectedSymbolServices shim returning a clear
501/unsupported response until the durable affected-symbol backend ships.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260417_022_AdvisoryAI_truthful_testing_only_runtime_fallback.
AdvisoryAiRuntimeStartupContractTests documenting the testing-only
in-memory fallback and its boundary versus the durable runtime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260417_002_JobEngine_scheduler_storage_compose_compatibility
(SCHEDULER-COMPAT-001 still DOING — sprint remains active).
Adds scheduler storage configuration adapter layer so the web host
accepts the compose-shaped storage configuration without manual remapping,
plus SchedulerStorageConfigurationTests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260416_013_Authority_issuerdirectory_truthful_persistence_runtime.
IssuerDirectory.WebService Postgres persistence, options,
program wiring, tests. Sample config under etc/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260416_009_Notify_truthful_escalation_oncall_runtime.
PostgresEscalationRuntimeServices plus Notify + Notifier WebService
compat shims for escalation policy and on-call schedule service.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260416_008_Notify_truthful_suppression_admin_runtime.
Postgres-backed suppression runtime services wired through the admin
runtime extension registered in the durable storage bootstrap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260416_006_BinaryIndex_symbols_truthful_manifest_runtime.
Symbols.Server: in-memory symbol source read repository with real
endpoints, program wiring, migrations, tests services.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260415_001 — track execution across the cutover
sub-sprints and record per-module evidence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New StellaOps.Workflow.ArtifactExporter project: a post-build console app that
reads the generator's bundled workflow registry from the compiled plugin DLL and
writes canonical JSON (authoritative, fail-build) plus SVG/PNG visual artifacts
(graceful warn) next to each *Workflow.cs source file. Replaces per-csproj
rendering boilerplate with a single targets import.
Key design choices:
- Console app invoked via <Exec>, not an MSBuild ITask DLL — easier to debug,
no rendering-lib loading into the MSBuild process.
- Links WorkflowRenderGraphCompiler.cs from Engine as a compiled file instead of
ProjectReference, avoiding EF Core + Oracle transitive deps in the tool.
- Parallel.ForEachAsync across workflows with file-lock + PID-sentinel
"latest-wins" cross-process coordinator (FileShare.None + FileOptions
.DeleteOnClose — no thread-affinity issues unlike Mutex).
- Hash-based cache: expected canonical-hash marker injected into
.definition.json; unchanged workflows skip re-render. First build 167
workflows in ~143s; no-change rebuild in ~0.1s.
- Atomic write-via-rename on every artifact.
Targets file (StellaOps.Workflow.ArtifactExporter.targets) plugins can import
to get: analyzer wiring + JSON/SVG/PNG export in one <Import>. Configurable via
StellaOpsWorkflowArtifactExport / StellaOpsWorkflowSkipSvg /
StellaOpsWorkflowSkipPng properties. Also surfaces CanonicalTemplates/*.json as
AdditionalFiles so the analyzer's fragment loader can inline runtime-loaded
fragments at compile time.
Verified: builds clean against upstream Abstractions/Contracts/Renderer.ElkSharp/
Renderer.Svg (net10.0, 0 warnings, 0 errors).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port analyzer improvements developed downstream that extend canonical artifact
emission and non-trusted-call exemptions:
- WorkflowCanonicalArtifactGenerator: SubWorkflow / Fork / WhenStateEquals /
WhenPayloadEquals step handlers; Call desugaring with
WorkflowHandledBranchAction; HelperContext with parent chain and
ResolveParameter identifier chase for multi-hop parameter forwarding;
fluent-helper inliner (TryInlineFluentHelper + WalkFluentHelper*); spec-level
inliner (TryInlineSpecHelper); JSON-fragment loader surfaced via
AdditionalFiles (TryResolveLazyFragmentValue + TryResolveDirectFragmentCall);
ContinueWith HelperContext threading; null-coalesce support; const-name in
ParseNamedExpr; conditional-spread via TryExpandConditionalSpread.
- CanonicalSteps: add SubWorkflowStep and ForkStep IR classes.
- CanonicalJsonFragmentParser (new): minimal recursive-descent JSON→CanonicalExpr
parser to support compile-time inlining of pre-built
WorkflowExpressionDefinition fragments loaded at runtime via LoadFragment<T>.
- WorkflowCanonicalityAnalyzer: helpers returning trusted workflow types
(WorkflowSpec<T>, WorkflowExpressionDefinition, etc.) are now treated as
compile-time construction factories and exempt from WF010 — needed for the
fluent-helper inliner to cover real-world plugin patterns.
- Tests: AnalyzerTestHarness gains an additionalTexts overload (with
InMemoryAdditionalText); GeneratorStepsTests adds coverage for the new
handlers and inliners; NonTrustedCallTests inverts
CallingHelperThatHasImperative to assert the new WF010 exemption for helpers
returning trusted workflow types.
Verified: 51/51 analyzer tests pass (net10.0).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
37 of the 179 Bulstrad workflow start-request types carry
[WorkflowBusinessId] / [WorkflowBusinessReferencePart] attributes
which the runtime compiler surfaces as the top-level `businessReference`
canonical JSON field. Until now the generator omitted this field and
would have broken byte-parity on every one of those workflows during
the split.
Generator changes:
* BuildBusinessReference(INamedTypeSymbol) walks TStartRequest's
public instance properties collecting attributes:
Ablera.Serdica.Workflow.Abstractions.WorkflowBusinessIdAttribute
StellaOps.Workflow.Abstractions.WorkflowBusinessIdAttribute
Ablera.Serdica.Workflow.Abstractions.WorkflowBusinessReferencePartAttribute
StellaOps.Workflow.Abstractions.WorkflowBusinessReferencePartAttribute
* Emits null when neither attribute is present (matches the runtime's
`SingleOrDefault => null` path). Only emits when at least one is found.
* KeyExpression = WorkflowExpr.Path("start.{camelCased-or-JsonPropertyName}")
from the property tagged with [WorkflowBusinessId].
* Parts[] = one NamedExpr per [WorkflowBusinessReferencePart] property,
Name = attribute's partName arg or property name if null,
Expression = WorkflowExpr.Path("start.{camelCased-or-JsonPropertyName}").
* ResolveJsonPropertyName mirrors the runtime: [JsonPropertyName] wins,
else camelCase of the CLR property name.
Test fixture BusinessKeyedRequest + BusinessReferenceWorkflow
exercises the full path. Added as a third byte-parity test alongside
PureExpressionWorkflow + StartWithDecisionWorkflow. All 3 pass.
Other risk flagged in the migration manifest (nameof() in WorkflowName
/ WorkflowVersion / DisplayName): a grep across the Bulstrad plugin
returned zero matches — no workflow builds its name via nameof().
Namespace changes during the split cannot drift JSON. Risk #1
eliminated by evidence, no code needed.
Tests: 39/39 pass in both repos.
New net10.0 library that reads bundled canonical workflows out of
plugin assemblies (via the source generator's
_BundledCanonicalWorkflowRegistry) and POSTs each one to the workflow
service's /Orchestration/definition-import endpoint on microservice
startup. Server handles dedup + auto +N versioning; publisher just
drives the roundtrip and logs the outcome.
Public surface:
* WorkflowPublisherOptions — appsettings-bindable config
(Enabled, Endpoint, CommandPath, ServiceName, AuthToken, Timeout,
MaxRetriesPerWorkflow, RetryBaseDelay, FailOnError, DryRun)
* BundledWorkflow record
* IBundledWorkflowProvider + ReflectionBundledWorkflowProvider
(reads StellaOps.Workflow.Generated._BundledCanonicalWorkflowRegistry
from any assembly in a provided list; AppDomain.CurrentDomain
fallback at DI resolution)
* WorkflowPublisher — orchestrator with retry/backoff/summary
* WorkflowPublisherHostedService — IHostedService adapter that
runs PublishAllAsync on startup
* PublisherServiceCollectionExtensions.AddWorkflowPublisher()
Behaviour:
* Enabled=false -> no requests, summary DisabledReason set
* DryRun=true -> logs what would POST, no network
* Endpoint missing -> each workflow counted failed, DisabledReason
set, no HTTP calls; FailOnError=true throws
* Non-2xx 5xx -> exponential retry MaxRetriesPerWorkflow times
* 4xx / persistent 5xx -> FailOnError=false logs+continues,
FailOnError=true throws
* Server response's WasImported=false (hash match) -> counted as
HashSkipped
* AuthToken propagates as Authorization: Bearer
* ServiceName propagates into WorkflowDefinitionImportRequest.ImportedBy
as "{ServiceName}/publisher"
Consumer wiring example (a microservice's Program.cs):
services
.Configure<WorkflowPublisherOptions>(
builder.Configuration.GetSection(nameof(WorkflowPublisherOptions)))
.AddWorkflowPublisher();
Tests: 11/11 pass using an in-memory HttpMessageHandler fake.
Covers disabled, empty provider, missing endpoint, happy path,
hash-match skip, dry-run, transient retry, persistent failure
(FailOnError false + true), auth header, ImportedBy.
Extends WorkflowCanonicalArtifactGenerator to produce canonical JSON
byte-identical to what WorkflowCanonicalDefinitionCompiler +
WorkflowCanonicalJsonSerializer produce at runtime, so the server's
SHA-256 content-hash dedup sees generator-emitted JSON and runtime-
emitted JSON as the same content.
Drift points resolved:
* startRequest (new CanonicalStartRequest.cs) — generator walks
TStartRequest's public instance IPropertySymbols, maps CLR types
to JSON-schema types (string / boolean / number / string for
enums / array with items / string for DateTime[Offset] / object
fallback), converts property names to camelCase, unwraps
Nullable<T>, detects IEnumerable<T>, emits enum constants from
enum type members. Matches BuildStartRequestContract +
BuildPropertiesSchema exactly.
* requiredModules (new RequiredModule IR) — generator walks the
IR and seeds `workflow.dsl.core`, adds `workflow.functions.core`
on any FunctionExpr, adds transport modules on LegacyRabbit /
Microservice addresses. Sorted alphabetically. Each module has
`versionExpression = ">=1.0.0"` + `optional = false` matching
the runtime's WorkflowRequiredModuleDeclaration record defaults.
* Brace / array formatting — BeginObject/BeginArray now inline
directly after a property colon (`"prop": {`) instead of
inserting newline+indent before the `{` / `[`. Matches
System.Text.Json WriteIndented behavior.
* String escaping — AppendEscapedString now matches
JavaScriptEncoder.Default: escapes <, >, &, ', +, ` and all
non-ASCII printable (>0x7E) as uppercase \uXXXX. Runtime's
System.Text.Json uses this encoder by default; without these
escapes every `">=1.0.0"` version string differs.
* Task emission (CanonicalTask.cs) — always emits
routeExpression (defaulting to WorkflowExpr.String(route)) and
payloadExpression (defaulting to Null) since the runtime's
BuildTask applies `??= WorkflowExpr.String(...)` /
`??= WorkflowExpr.Null()`. Always emits onComplete (defaulting
to empty sequence).
* Top-level field order — startRequest slots in after displayName,
workflowRoles after, businessReference between workflowRoles and
start — matching the runtime record's property declaration order.
Tests: 38 pass, 2 parity tests previously [Explicit] now active.
Generator produces byte-identical JSON for both parity fixtures:
expression-only workflow + step/task workflow with Set +
WhenExpression + AddTask + OnComplete.
Still deferred (future commits as Bulstrad corpus surfaces them):
* businessReference emission when TStartRequest has
[WorkflowBusinessId] / [WorkflowBusinessReferencePart] attributes.
Current fixture has neither; field omits via WhenWritingNull.
* Fork / Repeat / Wait / WaitForSignal / ContinueWith /
SubWorkflow / HttpAddress / GraphqlAddress — WF020 until
extended. Each requires a small visitor addition + tests.
Wires the analyzer + source generator into the analyzer test project
so parity tests can compare the generator's bundled JSON against what
the runtime WorkflowCanonicalDefinitionCompiler produces for the same
workflow class instance.
Changes:
* WorkflowCanonicalJsonSerializer.SerializerOptions: explicitly pin
`NewLine = "\n"` so canonical-JSON bytes are identical across build
platforms. Default would be Environment.NewLine = \r\n on Windows,
\n on Unix — unstable for hash-dedup across CI runners.
* Test project now consumes StellaOps.Workflow.Analyzer via <Analyzer>
item with a pre-build MSBuild Target that builds the analyzer csproj
first, so generator output is available in-assembly.
* New Fixtures/ParityFixtureWorkflows.cs with two canonical fixtures
exercising expression-only + step/task builder chains.
* New GeneratorByteParityTests.cs with diagnostic output showing the
exact byte-offset of first drift + visible-char window around it.
Parity tests are marked [Explicit] because the runtime compiler
populates `startRequest`, `businessReference`, and `requiredModules`
via CLR reflection — fields the generator does not yet emit
(replicating them symbolically requires significant work). The drift
surface is documented in the test class doc comment.
The next architectural decision is captured in the plan file: either
(A) extend the generator to reimplement those reflection paths
symbolically, or (B) pivot to a hybrid where the generator emits
metadata + type registry and the publisher calls the runtime
compiler at startup. Option B eliminates the parity gap entirely
with publisher overhead of ~1-5 ms per workflow on first boot.
Test status: 36 passing, 2 explicit-skipped (parity).
Extends WorkflowCanonicalArtifactGenerator (ships alongside the existing
analyzer in the same Roslyn component) to walk the real-world builder
surface used by the Bulstrad corpus.
New step visitors (inside StartWith(flow => ...) lambdas and
OnComplete(flow => ...) lambdas):
* Set(key, expr) and SetIfHasValue(key, expr) -> set-state
* ActivateTask(name) -> activate-task
* Complete() -> complete
* WhenExpression(name, cond, whenTrue, whenElse?) -> decision with
nested whenTrue/whenElse step sequences (lambdas walked recursively)
* Call(stepName, address, payload?, resultKey?, whenFailure?,
whenTimeout?, timeoutSeconds?) -> call-transport
Address resolution covers:
* new LegacyRabbitAddress("cmd") and new LegacyRabbitAddress("cmd", mode)
* new Address("serviceName", "command")
* Referenced static readonly fields -- walks the field's declarator
initializer and emits the address there (matches real Bulstrad pattern
"private static readonly LegacyRabbitAddress FooAddress = new(...)").
AddTask chains:
WorkflowHumanTask.For<T>("name", "type", "route")
.WithRoles(...) / .WithTimeout(...) / .WithRoute(expr)
/ .WithPayload(expr) / .OnComplete(flow => ...)
The generator walks each segment and appends a CanonicalTask to
definition.tasks[].
IR extended:
* CanonicalSteps.cs -- SetStateStep, ActivateTaskStep, CompleteStep,
DecisionStep, TransportCallStep, AssignBusinessReferenceStep,
StepSequence
* CanonicalAddress.cs -- MicroserviceAddress, LegacyRabbitAddress
* CanonicalTask.cs -- CanonicalTask
* CanonicalDefinition now emits the full top-level shape
(workflowRoles[], start{initializeStateExpression, initialTaskName,
initialSequence}, tasks[], requiredModules[], requiredCapabilities[]).
Tests: 36/36 pass (31 existing + 5 new covering the step/task surface,
including a WF020 fallback test for the not-yet-supported Fork().
Still not supported (WF020 continues to fire): Fork, Repeat, Wait,
WaitForSignal, ContinueWith, SubWorkflow, SetBusinessReference with
object-initialiser syntax, QueryGraphql, HttpAddress / GraphqlAddress.
Follow-up commits extend these as the corpus migration surfaces them.
Archived sprint files inherit the rename without the post-move status edits
since git recorded the rename against pre-edit content. Applies the
OBSOLETE/DONE annotations directly on the archived copies so the record is
internally consistent.
Integration-detail component + spec: small polish pass. integration-hub-ui
spec: trivial assertion tweak. Playwright: refreshed live-frontdoor-auth
snapshot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds SPRINT_20260415_001_DOCS_real_service_cutover_plan tracking the doc
work needed to finalize the no-mocks / real-service migration.
Archives SPRINT_20260415_002_FE_integration_hub_truthful_status_and_button_styling
— both tasks complete (truthful integration status + button styling fix
landed in the earlier Web UI commit).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes SPRINT_20260408_002_Findings_vulnexplorer_ledger_merge via Option B:
- Phase 1 (VXPM-001..005) marked OBSOLETE. The separate vulnexplorer
schema was superseded by commit 6b15d9827 (direct merger into Findings
Ledger); there is no separate Postgres schema to build.
- Phase 2 corrections: VXLM-003/004/005 flipped to DONE. The adapter
ConcurrentDictionary pattern is accepted as the VXLM-003 closure — these
are read-side projections over Ledger events; durability comes from the
append-only event log, not from the adapter. Two follow-ups logged in
Decisions & Risks (FOLLOW-A: write-through Ledger event emission;
FOLLOW-B: /api/v1/vulnerabilities gateway route alignment).
- Deletes stale VulnExplorer project trees:
- src/Findings/StellaOps.VulnExplorer.Api/ (entire service)
- src/Findings/StellaOps.VulnExplorer.WebService/ (shell + migrated contracts)
- src/Findings/__Tests/StellaOps.VulnExplorer.Api.Tests/ (tests targeted
SampleData IDs that no longer exist under Ledger)
- src/Findings/StellaOps.Findings.Ledger.WebService/Services/
VulnExplorerRepositories.cs (33-line placeholder with a misleading
header comment; the actual Postgres path was never wired)
- Updates StellaOps.sln and Findings.sln to drop the removed project GUIDs
and their 24 configuration entries. dotnet build
src/Findings/StellaOps.Findings.sln passes 0 warnings / 0 errors.
Also archives the 4 previously-closed sprints:
- SPRINT_20260408_002 Findings VulnExplorer merger (above)
- SPRINT_20260410_001 Web runtime no-mocks (21/21 tasks done via earlier
Postgres persistence commits)
- SPRINT_20260413_002 Integrations GitLab bootstrap automation
- SPRINT_20260413_003 Web UI-driven local setup rerun
- SPRINT_20260413_004 Platform UI-only setup bootstrap closure
Active sprints reduced to 2: SPRINT_20260408_004 Timeline unified audit
sink (15-25hr breadth work) and SPRINT_20260408_005 Audit endpoint filters
deprecation (mandatory 30/90-day verification windows).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
devops/compose: docker-compose.stella-ops.legacy.yml +
docker-compose.stella-services.yml receive small service wiring updates.
Playwright: refreshed auth-state/report fixtures from the latest
integrations + setup-wizard + policy-runtime live runs. Includes a new
playwright-report-integrations/ bundle.
Docs: SPRINT_20260410_001 (runtime no-mocks) significantly expanded with
additional NOMOCK tasks reflecting the Postgres-backed work shipped across
Policy, Graph, Excititor, VexLens, Scanner, VexHub. SPRINT_20260413_004
(UI-only setup bootstrap closure) log updates.
Gitignore: narrow the earlier `output/` rule to `/output/` so the tracked
src/Web/StellaOps.Web/output/playwright fixtures continue to be picked up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
app.config: wiring updates for VEX hub statement providers + integration
hub DI.
VEX hub client: large refactor and expansion of vex-hub.client.ts (+spec)
with the shape needed by the statement detail panel and the new
noise-gating surfaces. vex-statement-detail-panel.component aligned with
the new client contract.
Integration hub component: extends the bootstrap + verification flow
(browser-backed, no mocks) and updates the spec coverage accordingly.
New tooling:
- scripts/run-policy-orchestrator-proof-e2e.mjs to drive the orchestrator
proof flow from outside the Angular test harness.
- src/tests/triage/noise-gating-api.providers.spec.ts covers the DI
providers wiring for the triage noise-gating surface.
- tests/e2e/integrations/policy-orchestrator.e2e.spec.ts exercises the
policy orchestrator UI end-to-end.
- tsconfig.spec.vex.json isolates the VEX spec compile so it does not
fight the main triage configs.
- angular.json + package.json wire the new spec/e2e targets.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Authority: StellaOpsLocalHostnameExtensions gains additional local aliases
for the IssuerDirectory service; new StellaOpsLocalHostnameExtensionsTests
cover the alias table. IssuerDirectory.WebService Program.cs wires the
IssuerDirectory host against the shared auth integration.
Scanner: WebService swaps in-memory score replay tracking for
PersistedScoreReplayRepositories (Postgres-backed) in Program.cs.
Docs: scanner architecture page updated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Excititor: new migration 003_vex_claim_store.sql and PostgresVexClaimStore
replace the in-memory claim tracking. ExcititorPersistenceExtensions wires
the store; ExcititorMigrationTests updated. Archives S001 demo seed.
VexLens: new migration 002_noise_gating_state.sql with
PostgresGatingStatisticsStore, PostgresSnapshotStore, and
PostgresNoiseGatingJson bring noise-gating state onto disk. New
VexLensRuntimeDatabaseOptions + AuthorityIssuerDirectoryAdapter +
VexHubStatementProvider provide the runtime wiring. WebService tests cover
the persistence, the issuer-directory adapter, and the statement provider.
VexHub: WebService Program, endpoints, middleware, models, and policies
tightened; VexExportCompatibilityTests exercise the Concelier↔VexHub export
contract.
Docs: excititor, vex-hub (architecture + integration guide), and vex-lens
architecture pages updated to match the new persistence and verification
paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces IGraphRuntimeRepository + PostgresGraphRuntimeRepository that back
runtime-path graph reads with real persistence. Graph.Api Program.cs wires
the new repository into the DI graph. InMemory* services get small cleanups
so they remain viable for tests and local dev.
CompatibilityEndpoints: extends the integration-test surface.
Tests: GraphPostgresRuntimeIntegrationTests,
GraphRuntimeRepositoryRegistrationTests, expanded
GraphCompatibilityEndpointsIntegrationTests.
Docs: graph architecture page updated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ignore .codex-*.mjs scratch scripts used by Codex agents, the top-level
output/ scratch dir, and accidentally-created duplicate source trees
(src/src/ and src/Web.StellaOps.Web/) so they stop appearing as untracked.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
docs/modules/workflow/analyzer.md — user-facing reference for
WF001-WF006 + WF010: one section per rule with a "bad" example and
the canonical fix. Covers activation, scope (Spec property is the
entry point; helpers walked transitively), trusted-assembly prefix
rule, cross-project WF010 indirection, and non-goals (no source
generator, no severity config, no escape hatch).
The DiagnosticDescriptors' HelpLinkUri already points at sections in
this doc (e.g., #wf005), so users who hit a build error can click
through to the exact rule explanation.
Golden tests (GoldenWorkflowShapeTests) exercise three patterns
lifted from the Bulstrad corpus:
1. static readonly LegacyRabbitAddress fields + nested
WhenExpression(Gt, Len, ...) + .Call + OnComplete with
WhenExpression(Eq, ...) + ActivateTask/Complete
2. SetBusinessReference(new WorkflowBusinessReferenceDeclaration
{ KeyExpression, Parts = new WorkflowNamedExpressionDefinition[] { ... } })
3. WorkflowExpr.Func("bulstrad.normalizeCustomer", path)
— custom runtime function dispatch
Each asserts zero WF* diagnostics. A regression that rejects these
patterns would break the entire Serdica corpus.
30/30 tests pass.