MindAttic.Ideas
A single-deployment Blazor CMS for the MindAttic ecosystem.
MindAttic.Ideas
A single-deployment Blazor CMS for the MindAttic ecosystem. One Azure App Service, one app
pool, one database hosts many pages — so a project like MindAttic.Frontpage or
MindAttic.Legion.Frontend no longer needs a whole web app just to serve essentially one page.
You ship capability by uploading or CLI'ing a .idea file (a plain zip). The CMS reads whether
it's a Page, Plugin, Component, or Theme, registers it, and it's live — no redeploy,
no app-pool restart. Plugins, Components, and Themes are globally available, so any Page composes
them by dropping a tag:
{{ Theme.Cyberspace }}
{{ Plugin.Tooltip }} <!-- no version = latest -->
{{ Component.Textbox label="Email" }}
or, from a compiled page/theme, the equivalent <CmsInclude Ref="…"/> primitive (see
Composing citizens).
Status: Foundation built and verified end-to-end (0 build errors, 224 NUnit tests green as of the last recorded run — see
docs/BIBLE.md §6). This README is a practical engineering tour;docs/BIBLE.md+docs/AMENDMENTS.mdare the canonical source of truth for what's true today — an amendment always wins over prose here or in the bible.docs/USER_STORIES.mdtest-cites every shipped story.A note on vocabulary. The content kinds have been renamed twice as the design settled (
Widget/Control→Plugin/Component, amendments A18/A19/A26). This README uses the current vocabulary —Page·Plugin·Theme·Component— throughout. If you seeWidgetorControlanywhere in the codebase (a stray file name, an old comment,docs/FOUNDATION_ADR.md), it is legacy vocabulary from before that split;docs/BIBLE.md §9is the authoritative glossary.
Table of contents
- The one mental model
- The tag convention
- Composing citizens: CmsInclude / tags
- Versioning & lifecycle
- The Abstractions SDK — public API
- The
.ideapackage format - Directory layout
- Worked example: authoring a Page from the template
- Worked example: a Plugin from the first-party library
- The first-party library
- CSS cascade
- Trust & security
- Ecosystem integration
- Build, test, run
- The
ma-ideaCLI - End-to-end tests
- Feature checklist
- Stack
- Canon & further reading
The one mental model
A Page is free-form. A Theme wraps it. Plugins and Components drop into it. Inline JS/CSS/HTML is yours.
There are no zones, panes, slots, or grids — DotNetNuke's clunky fixed-layout model is explicitly
rejected. You author a page however you like and place content exactly where you want it in your markup.
The unit you install is a .idea zip; the thing it contains is one of four content kinds, all
deriving from a shared root, IdeaBase (src/MindAttic.Ideas.Abstractions/Bases.cs):
Kind (ContentKind) |
Ordinal | What it is | Base type | Example reference |
|---|---|---|---|---|
| Page | 0 |
A free-form or compiled page — content, resolved by (SiteId, Slug) |
PageBase |
MindAttic.Ideas.Page.HelloWorld.V1 |
| Plugin | 1 |
A site-wide capability activator (loads css/js across the whole page; picked in Page Properties or {{Plugin.X}}) |
PluginBase |
MindAttic.Ideas.Plugin.Tooltip.V1 |
| Theme | 2 |
Layout chrome + one @Body hole + a CSS bundle |
ThemeBase |
MindAttic.Ideas.Theme.Cyberspace.V4 |
| (ordinal 3 — removed) | 3 |
Control was deleted pre-1.0 (A19); atomic UI is authored as a Component. Never reused. |
— | — |
| Component | 4 |
An inline-placed UI unit, rendered at the exact {{Component.X}} token position; can nest other Components |
ComponentBase |
MindAttic.Ideas.Component.Textbox.V1 |
"Idea" names the shared base and the .idea package format (and the /_ideas/... asset route) — it is
never a content kind itself. New kinds can be appended to the enum later (new ordinals only,
never renumbered) without breaking anything.
Plugin vs Component. A Plugin is a site-wide capability activator — dropping
{{Plugin.Tooltip}}loads the tooltip engine so that thereafter any element withdata-tooltip/data-ttshows a tooltip on hover; by default it renders no markup of its own (seePluginBase.BuildRenderTree, which just emits<link>/<script>tags for its declaredStylesheetUrls/ScriptUrls). A Component is inline-placed at a specific{{Component.X}}token position and renders actual markup (e.g.Component.Textboxrenders an<input>). Both can nest other citizens via[Uses]/<CmsInclude Ref="…"/>.
Two ways to author a Page — one render path
- Data page (zero deploy): free-form
BodyHtml/PageCss/PageJsstored in the DB. Interactivity comes from your inline JS.{{Kind.Name}}tags are expanded into live content at render time by theIncludeExpander. This is the primary path. - Code page (compiled): a
PageBasesubclass — a.razorcomponent shipped as a.idea— for when you genuinely need Blazor C# interactivity. Deploys once per type, never per page instance.
Both are first-class Page rows resolved by (SiteId, Slug) and rendered through the same primitive
(PageHost → DynamicComponent / the built-in free-form renderer). A page can graduate Data ↔ Code
as a row edit — never a schema change.
The tag convention (locked)
In a Data page (stored BodyHtml), the include grammar is:
{{ <Kind>.<Name>[.V<n>|.Latest] [attr=value …] }}
<Kind>—Theme·Plugin·Component(first token, case-insensitively matched against theContentKindenum member names).<Name>— the content's key (Cyberspace,Tooltip,Textbox, …).[.V<n>|.Latest]— optional: omitted or.Latest→ latest enabled version;.V3→ pins version 3.- A missing/disabled reference degrades to a clickable placeholder (opens the admin uploader prefilled with the missing reference) — never a crash.
In a compiled Page/Theme/Component (a .razor that compiles against only Abstractions), the same
identity is expressed as the fully-qualified string "MindAttic.Ideas.{Kind}.{Name}.{Version}" passed to
the <CmsInclude Ref="…"/> primitive — see the next section. Both forms resolve through the same
catalog, the same Missing/Disabled degradation, and the same Admin Inbox alerting.
Identity is inferred by convention — Kind from the base type, Key from the namespace tail, Version
from the V{n} class name — so no attributes are needed in the normal case. An optional
[Idea(key:…, version:…, scope:Global)] attribute overrides the convention when a name can't follow it.
Composing citizens: CmsInclude / tags
A compiled Page/Theme/Component references another citizen by string id, with zero
compile-time reference to that citizen's package — the MindAttic analog of Orchard's @Display /
<zone>. This is CmsInclude, defined once in src/MindAttic.Ideas.Abstractions/CmsInclude.cs:
<CmsInclude Ref="MindAttic.Ideas.Plugin.Tooltip.V1" />
<CmsInclude Ref="MindAttic.Ideas.Component.Textbox.V1" placeholder="Name" />
<CmsInclude Ref="MindAttic.Ideas.Component.Accordion" /> @* no version = float to latest *@
CmsInclude pulls the cascaded IRenderContext, resolves an IIncludeRenderer host feature from it,
and delegates rendering — if no host feature is present (e.g. Blazor design time) it silently renders
nothing rather than throwing. Unmatched attributes on the tag flow straight through to the resolved
citizen.
Declare what a compiled citizen depends on with the repeatable [Uses(ContentKind, key, version)]
class attribute (version: 0 = float to latest). This is what a Page never compile-referencing another
package's assembly actually means in practice — it names what it uses instead:
[Uses(ContentKind.Plugin, "tooltip", 1)]
[Uses(ContentKind.Component, "textbox", 1)]
public sealed class V1 : PageBase { … }
[Uses] feeds the manifest's uses[] array, which drives four things at once: (1) <head> asset
hoisting for the referenced citizen's css/js, (2) an install-time "missing dependency" warning, (3) the
delete reference-guard, and (4) the pre-upload compose-graph check (ma-idea verify).
Themes are not placed with CmsInclude — a page selects its theme via the ThemeKey/ThemeVersion
Page Properties (or the {{Theme.X}} inline override token), and the host wraps the body in it.
Versioning & lifecycle
Versions are whole numbers only (V1, V2, V3) — never SemVer like 1.5.11. Same scheme for
every kind. This is the heart of "never change, only enhance":
- You never mutate
Cyberspace.V1. You shipCyberspace.V2alongside it; versions coexist. - A reference may pin (
.V1) when a page cares, or float to latest (no version /.Latest) when it doesn't.
Lifecycle rules (data model shipped; full admin enforcement is part of the Phase-2 admin UI, itself
shipped per docs/BIBLE.md §6):
- A page must never be invalid. At render, a missing/disabled reference degrades to a visible placeholder + fires an Admin Inbox alert — never a crash.
- Disabled = a version that exists but can't be used until re-enabled (Page, Plugin, Component, Theme).
- Delete is version-specific and reference-guarded: you can't delete
Tooltip.V11while any page pins it. ShippingV12doesn't freeV11— each page must first be migrated (.V11→.V12) until nothing referencesV11. A floating (latest) reference is fine as long as some enabled version remains. - Wiki-like history via SQL Server temporal (system-versioned) tables: every Page version records which Plugin/Component/Theme versions it carried, so you can inspect — and roll back to — any prior state.
The Abstractions SDK — public API
Everything an author compiles against lives in one frozen project,
src/MindAttic.Ideas.Abstractions. It references only Microsoft.AspNetCore.Components +
System.Text.Json — nothing host-specific — and its public surface is append-only forever (MAJOR
pinned at 1, Sdk.Version constant): new members may be added, nothing is ever removed, renamed, or
made abstract.
| File | What it defines |
|---|---|
Bases.cs |
IdeaBase (shared root: cascaded IRenderContext, SafeUrl/IsUnsafeUrl XSS guards) and the four kind bases: PageBase (+ generic PageBase<TSettings>), ThemeBase (Body render fragment hole, GlobalCssUrls/ThemeCssUrls/ScriptUrls, BodyPreludeHtml), PluginBase (StylesheetUrls/ScriptUrls, default render emits <link>/<script> only), ComponentBase (same asset surface, meant to render real markup — aliases Blazor's ComponentBase as BlazorComponentBase internally so MindAttic's ComponentBase wins the bare name). |
Enums.cs |
ContentKind (Page=0,Plugin=1,Theme=2,Component=4 — ordinal 3 retired, never reused), PageKind (Data/Code), CmsRenderMode (Static/InteractiveServer — WebAssembly intentionally excluded, a hard .NET ALC boundary), ContentMode (View/Edit/Preview), ContentOrigin (Compiled/Package), RenderStrategy (ClrType/RawMarkup), PlacementScope (Placeable/Global), ContentTrust (Untrusted/Author). |
Attributes.cs |
[Idea(key:, version:, displayName:, category:, scope:, renderMode:)] — override the naming convention. [Uses(ContentKind, key, version)] — repeatable, declares a runtime string-id dependency (see above). [IdeaSdkVersionAttribute] — assembly-level, stamped by the packer. Sdk.Version — the frozen SDK version constant (1). |
Contexts.cs |
IRenderContext (cascaded to every citizen: InstanceId, Mode, RenderMode, Page/Site contexts, scoped Services, RawSettingsJson/GetSettings<T>(), and the additive-forever TryGetFeature<T>() escape hatch). IPageContext, ISiteContext, IInlineMarkup (a Data page's Html/Css/Js + Trusted flag). Optional host features resolved via TryGetFeature: IIncludeRenderer (renders a string-id reference — what CmsInclude delegates to), IComponentMetadataStore (per-instance metadata persistence), IPageTree (a page's children/descendants, e.g. for a TableOfContents component). |
Discovery.cs |
The seams shared by compiled discovery and the runtime .idea loader: ContentDescriptor (the uniform record every source yields — identity, Strategy, RenderMode, AssetMount, etc.), ICmsContentSource (a registration source), ITypeResolver (descriptor → Type, ALC-aware in the host), IContentCatalog (Find/FindLatest/ResolveTag), ContentResolution (Resolved/Missing/Disabled), IRenderAlertSink (fire-and-forget Admin Inbox alerting), IRawContentGate (the sole MarkupString chokepoint), SharedContracts.DeferToDefaultPrefixes (the ALC unification allow-list: MindAttic.Ideas.Abstractions/.Core, Microsoft.*, System.*, …). |
CmsInclude.cs |
The <CmsInclude Ref="…"/> component itself. |
Extension points, summarized:
| To build a… | Derive from | Override |
|---|---|---|
| Page | PageBase (or PageBase<TSettings> for typed settings) |
Razor markup; [Uses(...)] for string-id deps |
| Theme | ThemeBase |
Body (render fragment hole), GlobalCssUrls/ThemeCssUrls/ScriptUrls, BodyPreludeHtml |
| Plugin | PluginBase |
StylesheetUrls/ScriptUrls (default render just emits <link>/<script>); override BuildRenderTree for markup |
| Component | ComponentBase |
StylesheetUrls/ScriptUrls, BuildRenderTree for its markup; declare typed [Parameter]s, unmatched attrs land in Attributes |
The .idea package format
A .idea is a plain zip. Its only required member is idea.json. The manifest kernel is defined
in src/MindAttic.Ideas.Packaging/IdeaManifest.cs; the six required fields never change:
{
"manifestVersion": 1, // schema of this file (host-gated integer)
"category": "Plugin", // Page | Plugin | Component | Theme (WHAT it is)
"kind": "data", // data | code (HOW it renders)
"key": "tooltip", // stable identity, never the CLR type name
"version": 1, // whole-number content version (pins + asset URL segment)
"displayName": "Tooltip"
// optional, append-only: sdk, entryType, renderMode, css[], scripts[], assets, uses[], uiux[]
}
tooltip.idea (a zip)
├─ idea.json # required
├─ wwwroot/ # css/js/assets → served at /_ideas/{category}/{key}/{version}/...
├─ bin/ # kind=code ONLY: the compiled assembly + non-host deps
├─ data/ # optional idempotent seed
└─ icon.png README LICENSE # never parsed
Unknown fields/folders are ignored (forward-compatible). Host-provided assemblies
(MindAttic.Ideas.Abstractions, Microsoft.*, System.*) are forbidden in bin/ — the
ManifestValidator audits for this. Data content carries no bin/ and installs with zero build and
zero recycle. MindAttic.Ideas.Packaging (pure, IO-free, NUnit-tested) is the whole wire contract:
manifest kernel + reflection-only Packer + zip-slip-guarded IdeaArchiveReader + ManifestValidator
Sha256Hasher+PackageVersionResolver.
Directory layout
MindAttic.Ideas.slnx # CMS engine solution
├─ src/
│ ├─ MindAttic.Ideas.Abstractions # the frozen SDK — see "The Abstractions SDK" above
│ ├─ MindAttic.Ideas.Core # EF entities, CmsDbContext (SQL Server, temporal Pages),
│ │ # convention discovery, catalog, raw-content gate,
│ │ # FreeFormPage/include expander, ALC loader, auth, seed
│ ├─ MindAttic.Ideas.Packaging # pure .idea wire contract: manifest kernel, packer, reader,
│ │ # validator, SHA-256, version resolver
│ ├─ MindAttic.Ideas.Rendering # small rendering-support library (CmsHead.razor, PageHost.razor)
│ ├─ MindAttic.Ideas.Sdk # the `ma-idea` CLI: pack / inspect / list / verify / install / upgrade
│ ├─ MindAttic.Ideas.Blazor # the Blazor Web App host (global InteractiveServer): PageHost
│ │ # catch-all, CmsHead cascade, /admin, /_ideas asset route,
│ │ # Vault + Legion + MindAttic.Authentication wiring
│ └─ MindAttic.Ideas.Tests # NUnit suite
│
├─ library/ # first-party Theme/Plugin/Component library — see below
│ ├─ Themes/ Plugins/ Components/ # one small csproj per citizen; Directory.Build.props holds
│ │ # the shared settings + the one allowed reference (Abstractions)
│ └─ dist/ # packed *.idea output
│
├─ samples/MindAttic.Ideas.Page.HelloWorld # the reference modular Page (not part of the host solution)
├─ templates/maidea-page # `dotnet new` template that scaffolds a Page like the sample
├─ dist/ # packed .idea artifacts at the repo root (ad hoc / import staging)
├─ e2e/ # Cypress end-to-end suite (upload → reference → render)
├─ docs/ # Codex canon: BIBLE.md, AMENDMENTS.md, USER_STORIES.md, rfc/, ADRs
└─ tools/ # codex.ps1 (docs doctor/digest), import-frontpage.ps1, install-library.ps1
Known drift:
MindAttic.Ideas.slnxcurrently references a project atsrc/MindAttic.Ideas.Web/MindAttic.Ideas.Web.csproj, but the host project directory on disk issrc/MindAttic.Ideas.Blazor(csprojMindAttic.Ideas.Blazor.csproj, root namespaceMindAttic.Ideas.Blazor).dotnet build MindAttic.Ideas.slnxcurrently fails with MSB3202 (project file not found) until the.slnxis updated to point at the renamed project — see Build, test, run.
Worked example: authoring a Page from the template
The fastest way to build a new Page citizen is dotnet new from templates/maidea-page, which
scaffolds exactly what samples/MindAttic.Ideas.Page.HelloWorld
already shows working end-to-end:
# Install the template once, from the repo root
dotnet new install ./templates/maidea-page
# Scaffold a new Page (run from samples/ so the relative Abstractions path resolves)
cd samples
dotnet new maidea-page -n MyPage --slug my-page --theme cyberspace
# -> samples/MyPage/ with MindAttic.Ideas.Page.MyPage.csproj, namespace MindAttic.Ideas.Page.MyPage, class V1
| Template parameter | Default | What it does |
|---|---|---|
-n / --name |
(required) | Short name — becomes MindAttic.Ideas.Page.<Name> |
--slug |
hello-world |
Route the page is served at after install |
--theme |
cyberspace |
Theme key this page wears (referenced by string, never bundled) |
The generated project (and the HelloWorld sample it mirrors) is a Razor Class Library that
compiles against only MindAttic.Ideas.Abstractions:
<!-- MindAttic.Ideas.Page.HelloWorld.csproj — the whole thing, save comments -->
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Version>1.0.0</Version>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<!-- Private=false + ExcludeAssets=runtime keep Abstractions out of the packed bin/ -->
<ProjectReference Include="..\..\src\MindAttic.Ideas.Abstractions\MindAttic.Ideas.Abstractions.csproj"
Private="false" ExcludeAssets="runtime" />
</ItemGroup>
</Project>
@* V1.razor — identity by convention: namespace tail "HelloWorld" -> key "helloworld", class "V1" -> version 1 *@
@namespace MindAttic.Ideas.Page.HelloWorld
@inherits PageBase
@attribute [Uses(ContentKind.Plugin, "tooltip", 1)]
@attribute [Uses(ContentKind.Component, "textbox", 1)]
<section class="hello">
<h1>Hello, world.</h1>
<CmsInclude Ref="MindAttic.Ideas.Plugin.Tooltip.V1" />
<p><button type="button" data-tooltip="Resolved at runtime by string id.">Hover me</button></p>
<CmsInclude Ref="MindAttic.Ideas.Component.Textbox.V1" placeholder="Type here…" />
</section>
// data/page.json — the initial Page row seed
{
"slug": "hello-world",
"title": "Hello World",
"themeKey": "cyberspace",
"themeVersion": 1,
"published": true
}
Build, pack, inspect, and upload it (full detail in docs/AUTHORING.md):
dotnet build -c Release samples/MyPage
dotnet run --project src/MindAttic.Ideas.Sdk -- pack `
--assembly samples/MyPage/bin/Release/net10.0/MindAttic.Ideas.Page.MyPage.dll `
--out ./dist `
--refs src/MindAttic.Ideas.Abstractions/bin/Debug/net10.0
dotnet run --project src/MindAttic.Ideas.Sdk -- inspect ./dist/MindAttic.Ideas.Page.MyPage.V1.idea
Then, in the CMS admin at /admin/upload, drop the .idea. The host validates it (the same gate as
ma-idea install), registers the type, extracts its wwwroot/, and it is immediately live at its slug.
Worked example: a Plugin from the first-party library
A Plugin is even smaller when it's pure asset-activation with no custom markup. This is the actual,
shipped library/Plugins/Tooltip/V1.cs in full:
using MindAttic.Ideas.Abstractions;
namespace MindAttic.Ideas.Plugin.Tooltip;
public sealed class V1 : PluginBase
{
private const string Mount = "/_ideas/Plugin/tooltip/1";
public override IReadOnlyList<string> StylesheetUrls { get; } = new[] { Mount + "/tooltip.css" };
public override IReadOnlyList<string> ScriptUrls { get; } = new[] { Mount + "/tooltip.js" };
}
Its assets live in a plain assets/ folder next to the code (not wwwroot/, to avoid the Razor
static-web-asset collision):
library/Plugins/Tooltip/
├─ MindAttic.Ideas.Plugin.Tooltip.csproj # ~3 lines; settings inherited from Directory.Build.props
├─ V1.cs
└─ assets/
├─ tooltip.css
└─ tooltip.js
That same assets/ bundle serves three consumers with zero duplication: a raw .html demo page links
assets/*.css/*.js directly; a standalone Blazor app references the RCL or the same assets/; and the
CMS uploads the packed .idea whose wwwroot/ is that folder, served at
/_ideas/Plugin/tooltip/1/….
The first-party library
library/ (MindAttic.Ideas.Library.slnx) is the first-party home for every official Theme,
Plugin, and Component — merged into this repo from a former sibling repo. It is build-independent of
the CMS: it references only Abstractions, and the CMS never compile-references it, only installs
its packed output as optional content. Present on disk today:
| Folder | Citizens present |
|---|---|
Themes/ (8) |
Autumn, Cyberspace, Dark, Hardware, Light, Spring, Summer, Winter |
Plugins/ (12) |
AtticFont, BackHomeM, BackToTop, Breadcrumbs, Cyberspace, Footer, Header, NavMenu, OutfitFont, PinFooter, SacredGeometry, SocialLinks |
Components/ (27) |
Accordion, Callout, Card, Carousel, ChiMesh, Claudia, CodeBlock, ContactForm, FromHtml, FromMd, Frontpage, Gallery, HardwareHero, HelloWorld, Hero, IdeasBrochure, IdeasFrontpage, LegionPersonas, MediaImage, MediaLink, MindAtticFrontpage, ModalPopup, TabBoard, TableOfContents, Tabs, Textbox, VideoEmbed, WebSnapshot |
(library/README.md and library/docs/BIBLE.md currently cite 43 total citizens / 23 Components as of
amendment MAIL-A6; the counts above are a fresh directory listing at the time this README was written —
several Components (FromHtml, FromMd, Header, IdeasFrontpage, MediaImage, MediaLink) exist on
disk but aren't yet reflected in that older count, so treat library/docs/ as the tie-breaker for exact
numbers.)
# Build one citizen
dotnet build -c Release library/Plugins/Tooltip
# Build everything in the library
dotnet build -c Release library/MindAttic.Ideas.Library.slnx
# Pack + verify (from the repo root)
dotnet run --project src/MindAttic.Ideas.Sdk -- pack `
--assembly library/Plugins/Tooltip/bin/Release/net10.0/MindAttic.Ideas.Plugin.Tooltip.dll `
--out library/dist --wwwroot library/Plugins/Tooltip/assets `
--refs src/MindAttic.Ideas.Abstractions/bin/Debug/net10.0
dotnet run --project src/MindAttic.Ideas.Sdk -- verify library/dist
The library has its own Codex canon at library/docs/BIBLE.md / AMENDMENTS.md / USER_STORIES.md /
docs/data/components.json (a machine-readable catalog of every shipped .idea) — see
library/CLAUDE.md for its working rules.
CSS cascade — fixed order
Locked, enforced in exactly one place (CmsHead, src/MindAttic.Ideas.Rendering/CmsHead.razor), never
reordered:
GLOBAL stylesheet → THEME stylesheet (e.g. Cyberspace) → PAGE-level stylesheet → inline style=""
(Host setting) (mirrors UiUx deps.json) (Page.PageCss) (by DOM nature)
A per-page tweak is either inline CSS in the Page definition, or an uploaded .idea.
Trust & security
Sign-in is delegated to the MindAttic.Authentication
package (Argon2id+pepper, Vault-backed, hardened sessions) — src/MindAttic.Ideas.Blazor/Program.cs
wires it via AddMindAtticAuthentication<CmsDbContext>(...) with AppName = "Ideas" (a hard per-app
trust boundary — no cross-app SSO), plus a Cms.AuthorRawMarkup policy claim. What stays Ideas-owned is
the raw-content trust gate:
- On save, a page is stamped
ContentTrust.Authoriff the writer holds theCms.AuthorRawMarkupclaim (Admin role); otherwiseUntrusted. - At render, a single gate (
IRawContentGate) emits markup: Author → raw passthrough (your inline JS runs); Untrusted → sanitized (HtmlSanitizer strips script/style/event-handlers/javascript:;{{tags}}survive). - Demoting an author is a deliberate policy action (an
AuthorTrustVersionepoch bump), never a silent re-render of live pages.
You intentionally author inline JavaScript in trusted pages — that's a feature, not a leak. The trust boundary is author identity at write time, not content inspection at read time.
Ecosystem integration
MindAttic.Ideas reuses the ecosystem's shared infrastructure (all wired in
src/MindAttic.Ideas.Blazor/Program.cs):
- MindAttic.Vault — all credentials (DB connection
strings, API keys, admin bootstrap) via
AddMindAtticVaultFiles(...)+AddMindAtticVault(...). Same code locally (%APPDATA%\MindAttic\…) and on Azure (App Settings / Key Vault via Managed Identity). No User Secrets. - MindAttic.Legion — LLM calls + multi-model
voting/consensus/scoring, wired via
AddLegionClient(). - MindAttic.Authentication — the canonical
auth engine, wired via
AddMindAtticAuthentication<CmsDbContext>(...)(see Trust & security above). Note:docs/BIBLE.mdcurrently marks this integration📋 plannedpending an "interim BCrypt" stack, butProgram.csas it stands on disk already callsAddMindAtticAuthentication— treat the code as ahead of that doc line anddocs/AMENDMENTS.md(A16) as the place that should eventually be updated to match. - MindAttic.Media — disk-backed media storage, pointed at
{ContentRoot}/media. - MindAttic.UiUx — the canonical upstream source for
official Plugins/Components/Themes' raw js/css/html, from which the
library/Blazor wrappers are authored.
Build, test, run
# Build the CMS engine solution
dotnet build MindAttic.Ideas.slnx -c Debug
# Run the NUnit suite
dotnet test src/MindAttic.Ideas.Tests/MindAttic.Ideas.Tests.csproj
# Run the Blazor host directly (bypassing the stale .slnx — see the drift note above)
dotnet run --project src/MindAttic.Ideas.Blazor
As of this writing,
dotnet build MindAttic.Ideas.slnxfails immediately with MSB3202 because the.slnxstill points atsrc/MindAttic.Ideas.Web/MindAttic.Ideas.Web.csproj, a path that no longer exists (the project directory issrc/MindAttic.Ideas.Blazor, csprojMindAttic.Ideas.Blazor.csproj). Building/running the individual projects (Abstractions,Core,Packaging,Rendering,Sdk,Tests,Blazor) directly works around this; the.slnxitself needs its project path corrected.
Codex docs tooling (see Canon & further reading):
powershell -File tools/codex.ps1 digest # regenerate docs/BIBLE.digest.md
powershell -File tools/codex.ps1 doctor # validate the canon (must pass)
The ma-idea CLI
src/MindAttic.Ideas.Sdk builds a CLI (ma-idea) over the pure MindAttic.Ideas.Packaging library.
Every read verb is offline and never touches a database:
| Verb | What it does |
|---|---|
pack --assembly <dll> --out <dir> [--wwwroot <dir>] [--data <dir>] [--icon <file>] [--version <n>] [--refs <a;b>] |
Packs a built Page/Theme/Plugin/Component RCL into a .idea (reflection-only; identity read from namespace + Vn class by convention). |
inspect <file.idea> |
Prints the manifest + bin//wwwroot//data/ file counts. |
list [dir] |
Lists every .idea in a directory (key, version, category, kind). |
verify [dir] |
Checks every package's uses[] resolves against the .idea files in that directory — the compose-graph check. |
install <file.idea> [--allow-override] |
Offline validation only — does not install; that is a host operation (PackageInstallService). |
upgrade <file.idea> |
Validates + previews the install action (InstallAction) against the .idea files beside it. |
disable |
Refuses — disabling a live package is a host database operation, not reachable offline. |
dotnet run --project src/MindAttic.Ideas.Sdk -- pack --assembly bin/Release/net10.0/MyPage.dll --out ./dist
dotnet run --project src/MindAttic.Ideas.Sdk -- inspect ./dist/MindAttic.Ideas.Page.MyPage.V1.idea
dotnet run --project src/MindAttic.Ideas.Sdk -- verify ./dist
End-to-end tests
e2e/ is a Cypress suite covering the core admin flow end to end: admin logs in → uploads a compiled
.idea → creates a page referencing it by {{tag}} → the page renders the content with no missing-content
placeholder (e2e/cypress/e2e/admin-widget-flow.cy.js, fixture
e2e/cypress/fixtures/MindAttic.Ideas.Plugin.Tooltip.V1.idea). It expects a running instance of the host
(MindAttic.Ideas.Blazor) — it does not start the app itself. See e2e/README.md for the full
environment-variable table and run instructions:
# 1) start the CMS (from the repo root, separate dev DB)
$env:ASPNETCORE_ENVIRONMENT = "Development"
$env:ASPNETCORE_URLS = "https://localhost:7207"
dotnet run --project src/MindAttic.Ideas.Blazor
# 2) in a second shell, from e2e/
npm install
$env:CYPRESS_BASE_URL = "https://localhost:7207"
$env:CYPRESS_ADMIN_PASSWORD = "<bootstrap admin password>"
npm run cy:run
Feature checklist
The full, kept-in-lockstep checklist (with per-item ✅/🔨/📋 status and the test/story that proves each
✅) lives in docs/BIBLE.md §6 and docs/USER_STORIES.md —
this README does not duplicate it. In short, per the bible's own "verified state" section: the
Abstractions SDK, Core EF model + convention discovery + catalog, the free-form page renderer + include
expander, the fixed CSS cascade, the raw-content trust gate, disable/enable + reference-guarded delete,
the Admin Inbox, the full .idea packaging pipeline (pack/inspect/list/verify/install), host-side
install + collectible-ALC runtime load, the asset-hoisting <head> pipeline, the /_ideas/... asset
route, and the first-party library (43 citizens per library/docs/) are all reported ✅, each citing
its NUnit test in docs/USER_STORIES.md.
Stack
.NET 10 · Blazor Web App (global InteractiveServer) · EF Core + SQL Server (temporal tables) ·
Azure Blob · IDbContextFactory · MindAttic.Vault · MindAttic.Legion · MindAttic.Authentication ·
MindAttic.Media.
Canon & further reading
| File | Layer | What it is |
|---|---|---|
docs/BIBLE.md |
L0 | Source of truth — what the project IS/is NOT, architecture, the Laws ({#MAI-LAW-n}). |
docs/AMENDMENTS.md |
L1 | Append-only change log (MAI-A1..A26+). An amendment wins over the bible. |
docs/USER_STORIES.md |
L2 | Test-cited stories (MAI-US-<Epic><n>); every ✅ names its NUnit test. |
docs/AUTHORING.md |
guide | The full authoring walkthrough: Pages (admin UI) vs Plugins/Components (.idea packages), asset bundle rules, composing/nesting, build/pack/verify/upload. |
docs/rfc/0001-unified-page-plan.md |
rfc | The unified page-grammar plan (status: implemented). |
docs/FOUNDATION_ADR.md |
historical | The original Legion deliberation that produced the foundation — vocabulary superseded by the amendments. |
docs/FOUNDATION_AMENDMENTS.md |
historical | Preserved for existing links; current truth is BIBLE.md + AMENDMENTS.md. |
docs/IMPLEMENTATION_PLAN.md |
historical | The original brief. |
docs/BIBLE.digest.md |
generated | Produced by tools/codex.ps1 digest; injected at session start. Never hand-edit. |
library/README.md / library/CLAUDE.md |
— | The first-party Theme/Plugin/Component library's own docs and Codex canon (library/docs/). |
CLAUDE.md |
— | How to work in this repo (Codex rules of engagement, build/test commands). |