Skip to content

Module identity unification: one mechanism, one ring, one grammar - #136

Merged
LeftTwixWand merged 233 commits into
masterfrom
os
Oct 6, 2026
Merged

LeftTwixWand merged 233 commits into
masterfrom
os

Conversation

@LeftTwixWand

Copy link
Copy Markdown
Contributor

What changed

The whole module system now runs on one mechanism, ratified in docs/superpowers/specs/2026-10-05-module-identity-amendment.md (amending the 2026-10-04 module self-description design):

  • Typed identity. ModuleId (Contracts) owns the id grammar; Module / Module<TOptions> abstract classes replace IModule — the id is constructor-enforced, no attribute, no reflection. ModuleIdentity, [ModuleId], [ModuleHosting], [ModuleDeployment] are deleted.
  • The catalog holds instances. OsCatalog.Modules is IReadOnlyList<Module>; the instance is the descriptor; every Activator.CreateInstance factory is gone.
  • Rings claim their module through a generic parameter. ModuleHosting<TModule> / ModuleDeployment<TModule> replace the string type-name attributes; discovery scans referenced assemblies of a known root (ModuleHostingScan, deployment Discover) instead of Assembly.Load / Type.GetType by name.
  • One configuration grammar, no module exempt. Every private root is dead: DigitalBrain:AI, :Workspace, :Apps, :ShippedApps, :CSharp, :Microsoft:GitHub, :Microsoft:Aspire, :Salesforce:OAuth, :Flutter:Hosting, :Time. All module keys derive from DigitalBrainConfiguration.ModuleSection/ModuleOptionsSection. LegacyConfigurationGuard refuses retired roots at OS startup; AI and Flutter binds defend their own roots where the OS guard cannot see them.
  • Options purity. Validate() checks only an options object's own values; cross-ring facts (model markers) resolve loudly once at bind in the hosting ring, with errors naming the value and the catalog. AIOptions.CopyFrom and the caller-less DigitalBrainModuleBuilder<AIModuleHosting> model API are deleted.
  • Permanent enforcement. ModuleKeyGrammarFacts pins module keys to DigitalBrain:Modules: (closed allowlist with reasons) and scans Dockerfiles/pubxml/launchSettings for retired env roots; the ConfigurationKeyOwners.json ratchet shrank by eight files.

Why

The attribute scheme flattened typed links into assembly-qualified strings (Assembly.Load, four-way error taxonomies, id cross-checks), the module concept was split across Contracts/Kernel by a dependency accident, and AI (plus seven other modules) carried private configuration roots beside the one grammar. All three were the same rule violated: a fact belongs to the ring that owns it, as a typed reference; strings exist only at the configuration boundary.

Reviewer notes

  • A fresh-context review already ran on the final tasks; its Critical (OS Dockerfile baked the retired DigitalBrain__ShippedApps__… env var — would crash-loop the container against the guard) and Important (legacy Flutter hosting section silently ignored in the AppHost process) findings are fixed with RED→GREEN tests.
  • Known pre-existing reds, not touched here: RepositoryLayoutFacts (.slnx grouping drift) and Kernel E2E boot in environments without Postgres. The aspire run smoke still needs one pass on a provisioned dev machine.
  • Deployments carrying old keys fail loudly at startup with the old→new mapping in the message; there is no silent fallback by design.
  • Deferred minors (ledgered): dead SetDefaultLlm/SetDefaultEmbedding plumbing in AIHostingState; no equality pin between ServiceTelemetryOptions' mirror constant and AIKeys; DigitalBrainBuilder.Register duplicate check is Ordinal while ModuleId equality is case-insensitive.

🤖 Generated with Claude Code

LeftTwixWand and others added 30 commits October 4, 2026 20:22
…r module self-description

IntoChat.AppHost moves to src/DigitalBrain/AppHost as DigitalBrain.AppHost; the
DigitalBrain.OS library and DigitalBrain.OS.Host merge into one DigitalBrain.OS
executable. Spec and plan for the next step: ids/hosting/deployment declared on the
module type, OsCatalog as types, enabled-by-default composition in the image.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Module identity attributes (ModuleId, ModuleHosting, ModuleDeployment) and
ModuleIdentity move to DigitalBrain.Contracts so the AppHost-side hosting
package can read them without referencing the kernel ring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…moved

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ables them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
String WithModule composition calls are gone; the silo composes the OS catalog
itself. AI sensitive-data telemetry moves to AppHost configuration. The product
composition fact now checks the reference composition against OsCatalog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…figuration

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le hosting attributes fail loudly; module ids match configuration casing

- HostingModuleOptions.Flatten omits leaves equal to the type's defaults, so an
  AppHost's untouched option values cannot override the silo's own configuration
  (the shell title regression the review caught).
- ModuleHostingResolution distinguishes an absent assembly (null: composition
  without that infrastructure) from a present assembly missing the named type
  (loud error); CompositionFacts gates every catalog [ModuleHosting] string.
- SelectModules matches module ids case-insensitively, like configuration keys.
- BrandingFacts pins the shell title in host configuration instead of Program.cs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…est structure

- DigitalBrain.OS references modules by glob (every runtime module in the repo),
  with OsCatalog as the compiler-checked semantic list; vestigial Orleans.Client,
  redundant Kernel reference and repo-covered DisableMSBuildAssemblyCopyCheck gone.
- DigitalBrain.Aspire.Hosting's catch-all Brain/ folder becomes Composition/,
  Resources/ and Identity/ plus root hosting entry points.
- DigitalBrain.Aspire.Server: DigitalBrainRuntimeHostingExtensions renames to
  ServerHostingExtensions; the one-file Configuration/ folder flattens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IntoChat.ServiceDefaults moves to src/Aspire/DigitalBrain.ServiceDefaults (it was
never product-specific) and stops being a host concern: AddDigitalBrainServer and
AddDigitalBrainClient apply the process defaults themselves, MapDigitalBrainModules
maps /health and /alive, and the two duplicate copies die — the Client's private
ClientHostingDefaults (with its five duplicated OpenTelemetry package references)
and the Testing ring's ReferenceTelemetry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DigitalBrain:ShippedApps:{publisher}:Path points the host at a directory of app
folders (DirectoryShippedAppSource, same shape and line-ending normalization as
the embedded source). IntoChat.Apps is deleted: the AppHost hands the OS the
repo's src/IntoChat/Apps path, the docker image copies the folder and sets the
same key. The OS's own apps remain embedded — they are the OS's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The product host tests move to DigitalBrain.OS.Tests.{Unit,E2E}: CompositionFacts →
ProductCompositionFacts, ShippedAppFacts → VendorAppFacts, the E2E builder/fixture →
ProductE2E/ProductHostFixture alongside the reference fixture. Trimmed in passing:
IdentityHostOptionsFacts loses the case that tested configuration precedence rather
than our code, and PathTruthFacts' one surviving fact becomes a stronger catalog-wide
closure check in OsCatalogFacts (every catalog module's assembly ships with the
runtime). src/IntoChat is now only the Apps content folder; the intochat CI suite
folds into the os suite with a budget that covers both fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The Orleans dashboard is gone end to end (WithDashboard, MapDigitalBrainDashboard,
  two Dashboard properties, the hosting URL annotation, the package reference):
  dashboards are not DigitalBrain's responsibility.
- AddDigitalBrain(name, options): persistentStorage, dataVolume and serviceId fold
  into DigitalBrainHostingOptions, deleting the duplicate-serviceId contradiction.
- Dead API deleted: DigitalBrainHostingNames.ForModule and GetOrAddModuleNode(Type).
- WithModule(Type) refuses a type without [ModuleId] instead of minting an id.
- DigitalBrainModuleBuilder.GetOrAddProjection replaces the GetOrAddState/out added/
  AddProjection dance in nine module hosting packages.
- StandaloneOptions inlined to the one config read; SiloHosts error message no longer
  names a Testing-ring option.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…yment

WithHttpIdentity() reads DigitalBrain:Identity from the AppHost's own configuration
and projects it; the code literals, the IntoChatConfiguration constant and the
Compile-Include hack that smuggled it into the AppHost are gone. The image's
appsettings remains the standalone deployment's statement, and a unit fact pins the
two descriptors to agree while both live in this repo (the AppHost's env projection
would otherwise shadow a drift). The unread IntoChat__Hosted__Enabled flag leaves
the Dockerfile and publish profile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o magic keys

IdentityHostOptions and AuthOptions (with IdentityPosture) move to the contracts
ring so every writer shares the reader's vocabulary: the hosting ring's HttpIdentity
twin is deleted (WithHttpIdentity binds and projects the one type), AuthPosture
stops re-spelling the posture key, and tests write configuration through
IdentityHostOptions.Key/AuthOptions.Key + nameof instead of string paths. The
module test host's process side-channel is owned by one ModuleHostChannel type
(keys, serialization and resolution in a single place).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ProductSurfaceResources dies: resource names are the two literals they always were,
and the UI/MCP ports are AppHost configuration (DigitalBrain:Http:UiPort/McpPort)
with their current defaults. EnvironmentKeys.For is the one config-key-to-env-var
conversion (six hand-rolled Replace sites fold into it). DigitalBrainHostingOptions
owns its external override keys (Orleans:ServiceId, Orleans:ClusterId,
DigitalBrain:Hosting:PersistentStorage), leaving DigitalBrainHostingNames with
names only. Gmail and Salesforce hosting errors stop naming a deployment port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Orleans resource suffix and the Kernel module-node name become private
constants of the hosting extension that uses them; the master key parameter name
moves onto MasterKey. A miscellaneous-constants bucket is where dead code and
misfiled keys accumulated twice; nothing replaces it. EnvironmentKeys stays as
the one process-boundary adapter: options bind inside a process, and env vars
are how the AppHost writes what another process's configuration reads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The contract that declares an integration's fields now also owns their
configuration home: IntegrationDefinition.SectionName and Key(id, field).
The seeder, the AI/Gmail/GitHub hosting projections and every test fixture
spell the path through the contract instead of seventeen scattered strings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d by architecture

DigitalBrainConfiguration (Contracts) is the single grammar: EnvironmentName is the
one ':'-to-'__' conversion, Flatten the one JSON walk (full fidelity in-process,
omit-defaults cross-process), and the module sections (Modules, per-module Options,
brain-scoped overlay) are named once. EnvironmentKeys and the Kernel's duplicate
walker are gone.

Every raw DigitalBrain:* key literal in the tree now lives in the type that declares
that vocabulary — 91 files carried literals, 27 declaration owners remain (options
and contract types such as AIOptions, GitHubModuleOptions, FlutterHostOptions,
KernelCorsOptions now in Platform.Contracts, SalesforceOAuthOptions and
PostgresModuleOptions in their Contracts, ScriptEdgeProtocol, ModuleHostChannel) —
and ConfigurationKeyFacts pins the registry exactly in both directions: a new
literal fails, a cured file must leave the list.

The test tiers ratify the new shapes: <Module>.Tests.Hosting is the model-only
Aspire composition tier (it must not boot), DigitalBrain.OS.Tests.Unit is product
scope, a boot is a StartAsync, and the AppHost nests per the layout law at
src/DigitalBrain/AppHost/DigitalBrain.AppHost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The credential-suffix blacklist is gone: a name heuristic cannot carry the design
rule that secrets belong to the integrations registration, and it put product
vocabulary into the contracts ring. What remains is the structural contract — a
writable property the serializer skips would silently drop configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Its last check guarded a mistake the codebase has never made (a publicly writable
property the serializer skips); the two real JsonIgnore uses are the sanctioned
pattern — internal-set resolved values that deliberately do not flatten. The
secrets rule lives in the module contract and the integrations registration, not
in reflection on every compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The catalog lists module instances too (Task 4 shares this commit: the OS
build and its suites interlock with the instance-based registration).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LeftTwixWand and others added 28 commits October 6, 2026 17:42
The client project moved from src/DigitalBrain/Client to src/DigitalBrain.Client, but the
sandbox's ClientProject default still named the old path, so every script failed to restore.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The renderer and AppUiEndpoints learned table and chart nodes, but CompositionValidation
still refused them, so DataVisualizer's show failed with "Unsupported composition child kind".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…un loop

A TaskCanceledException from an HTTP timeout escaped RunLoop's per-run catch and its outer
catch, ending the view's loop for good; both now only stop on the view's own cancellation.
A pump also waits a second after its stream ends before resubscribing, not only after a
failed resubscribe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The workflows and .gitignore still named src/Modules after the module folder moved.

0c190ed's subject is wrong: OS apps are VerifiedByBuild; the bump only clears the
differs-without-bump error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… behaviors, no app tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A restarted run-on-change script (reaped, or redeployed by binding the db slot)
left views stale until the next registry write. The retry delay now follows
every stream end, so a stream that completes cleanly cannot spin. Version 1.0.3.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
First-party apps are verified by their build, and app tests left the app
model (DataVisualizer now declares no scenario). ByBuild still demanded at
least one scenario heading, so os/datavisualizer 1.0.2+ stayed out at startup
with an empty failure list and the Marketplace kept serving 1.0.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…w is Inspect

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…brain signals live in the ring

Every grain a program resolves through the brain is a neuron now, including the
platform identity actors, the identity directory and the shell state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Read returns the word's snapshot; Inspect is the rich view. The bound-app guard
covers the members ISynapse now declares.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Shipped app versions bumped so the renamed bundles publish; deployed program
keys are computed through AppProgramKey, which the end-to-end fact probes by
generation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review fix pass: disposing a compiler iterator while a read is in flight threw
and leaked the watch from AppInvocations and the workspace events endpoint.
Also: shell state access whitelists Read/Save, the program key helper is
internal, and every identity neuron contract must be platform-only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
IBrain is the entrypoint and the first neuron: Create(Owner), Read, Neuron<T>(NeuronId),
On<T>(NeuronId). It owns its address space; nothing else builds or parses keys.
NeuronId and SignalId replace bare strings. Synapse is the durable connection as a
record (Publisher, Signal), not a grain; execution is a neuron of its own kind.
States are states, not snapshots. IDigitalBrain, ISynapse and the snapshot records
are gone. Every file carries XML documentation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PlatformOnly and PlatformAssembly are enforcement, not vocabulary. They move to
DigitalBrain.Framework.Enforcement. ICSharpAppBinding drops the attribute: BindApp is
already refused at runtime for any caller but the owning Apps grain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resolving is addressing, not a call: IDigitalBrain.Get<T>(NeuronId) is synchronous like a
grain factory, On<T>(INeuron) streams a neuron's signals, Self is the brain's own neuron.
IKernel : INeuron holds Create(Owner) and Read(); programs never create brains. NeuronId
converts implicitly from a string. "Kernel" now names the first neuron; the runtime is
the runtime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
KernelNeuron persists the ring State, Create(Owner) publishes Created, runs the
ICreationHandlers and publishes Activated; every wake publishes Activated through
KernelActivation. Neuron seals OnActivateAsync and runs IActivationHandler<TNeuron>
handlers registered by modules; CSharpFileNeuron and AgentNeuron move their activation
work into handlers. IDigitalBrains opens a brain, resolves raw grains and streams signals;
CurrentBrain is the silo's IDigitalBrain; the Orleans client and the script connection
implement the new entrypoint. Brain ids are flat: the kernel's key, no hash. ISynapse's
members return to ICSharpFile with CSharpFileChanged as its change signal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@LeftTwixWand
LeftTwixWand merged commit 621ce6b into master Oct 6, 2026
0 of 5 checks passed
@LeftTwixWand
LeftTwixWand deleted the os branch October 6, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant