Node Page Provenance
The page a node lands on now carries "who wrote this, and when" by default. Declining it is a sentence somebody wrote down, not an absence somebody has to interpret.
The provenance line is the standard header's second row — Type · Created · Updated, built by
MeshNodeLayoutAreas.BuildMetaRow from the node's own CreatedDate / CreatedBy /
LastModified / LastModifiedBy, rendered in the viewer's time zone with every label localized.
The line is platform controls, never markup: a wrapping horizontal Stack of three fixed segments,
each a Label and its value — the node type as a Link to its Configuration area, the creation
stamp and the last change as Labels. The segments are projected once (MetaLine.Of, pure, the
viewer's language passed in) and then either fed into /data and bound by pointer (the standard
header) or passed as literals (BuildMetaRow); both go through the same
MeshNodeLayoutAreas.MetaRow, so the two shapes cannot drift. A segment the node has no value for
is hidden through its bound style rather than removed, so the bound row keeps one shape.
What went wrong
The line rode on ONE renderer: the framework's Overview, registered by
AddDefaultLayoutAreas, which composes it through BuildDetailsContent → BuildHeader. Any node
type that wanted a designed landing page registered its own renderer for that area — and
LayoutDefinition.WithNamedRenderer is last-writer-wins, so the framework's renderer went away and
the provenance line went with it.
Nothing recorded that. The state on the node was intact the whole time: CustomerJourneys carried
createdBy: rsalzmann, createdDate: 2026-09-15T06:53:47Z, lastModifiedBy: rsalzmann,
lastModified: 2026-09-15T07:17:01Z. Only the header omitted it.
The second-order problem is the one that mattered more. Some omissions were weighed: an
Activity page reports Compilation · Succeeded · started 09/16 11:56 · ended —, which answers the
question better than Created/Updated would; a Thread answers it per message, in the
transcript. Others were nobody's decision at all. From outside, those are the same observation —
an absence. A reviewer could not tell a considered page from a forgotten one, and neither could
the next author.
The census, measured 2026-09-16
Landing pages that REPLACE the framework renderer, and what they do about provenance:
| Repository | Landing pages that replace the renderer | Carry the line | Do not |
|---|---|---|---|
MeshWeaver (src/) |
7 | 3 | 4 |
| MeshWeaver.Plugins (compiled + in-mesh) | 92 | 10 | 82 |
| Total | 99 | 13 | 86 |
Core's other 61 node types never replace it and were never at risk — they land on the shared
Overview and get the line for free. The four in core that had lost it were PluginCatalog,
GlobalSettings, User and Partition; the last of those is the quietest of the lot, because
PartitionNodeType mentions no page at all — it repoints the default area to Search with
WithDefaultArea, and the landing page moves off the framework renderer as a side effect.
The reported floor was "at least three". The real ratio is 87% of pages that opt out of the framework renderer, and most of them in a repository no core reviewer reads. That number is what decided the shape of the fix: at 87%, patching call sites leaves the next page to be forgotten.
The shape: the default carries it — in the breadcrumb
MeshNodeLayoutAreas.WithNodePage replaces the bare WithView for a landing area, and its
NO-ARGUMENT form leaves the provenance line to the portal shell, which shows it as the
tooltip of the breadcrumb's current crumb — on every page, landing or not, one hover away.
Changed 2026-10-26 (maintainer): the default used to compose a
Type · Created · Updatedstrip ABOVE the page. A technical strip over a designed page — a Store cover above all — looked bad, so the line moved into the breadcrumb tooltip (BreadcrumbProvenancein the Blazor portal, MeshWeaver.Plugins), built from the sameBuildMetaEntriessegments, localized and in the viewer's zone. The framework composes nothing into the page any more; the verdict is still recorded. The standard header's own provenance row (BuildHeader) andBuildMetaRoware unchanged.
// The page renders as it is; the shell's breadcrumb tooltip carries the line. This is what you
// write when you have no opinion about provenance — and it produces a correct page.
layout.WithNodePage(MeshNodeLayoutAreas.OverviewArea, MyPage)
// The page already draws the standard header (it calls BuildHeader), provenance row included.
layout.WithNodePage(OverviewArea, MyPage, NodePageProvenance.RenderedByThePage)
// The page carries none, deliberately. The reason is MANDATORY — Declined("") throws.
layout.WithNodePage(OverviewArea, MyPage, NodePageProvenance.Declined(
"an Activity reports the run's own start and end, which is the question this page is asked"))
Three properties make this more than a rename of "opt-in" to "opt-out":
- Forgetting produces a correct page. The line is in the breadcrumb tooltip whatever the page draws.
- A decline is a sentence.
NodePageProvenance.Declinedrefuses a blank reason, so the record cannot say "somebody decided" without saying what. - The verdict cannot go stale. It lives on the
LayoutDefinitionbeside the renderer it describes, andWithNamedRenderer— which everyWithViewoverload funnels through — REMOVES it. A node type taking over a landing page is exactly the move that loses the line; if the framework's own verdict survived that takeover, every check downstream would pass having measured a renderer that is no longer registered.
LayoutDefinition.GetNodePageProvenance(area) reads it back, and null — "nobody decided" — stays
distinguishable from Declined — "decided no".
What core decided for its own four
| Node type | Verdict | Why |
|---|---|---|
PluginCatalog |
framework-supplied | a catalog node is declared, published and re-pointed by people; "who set this source up, and when" is a question its page is asked |
GlobalSettings |
framework-supplied | "who last changed the platform's settings" is the first thing an administrator on that page wants |
Partition |
framework-supplied | an administrative record whose first question is who declared the space, and when |
User |
declined | the subject is a PERSON, and the node's stamps describe the ROW: LastModified moves when a preference is flipped and CreatedBy is the sign-in flow, so "Updated … by system-security" across somebody's home states a database fact to a reader asking about a colleague. The real question — "member since" — belongs in the profile the page already renders. |
The three compile/refusal overlays (NodeTypeEnrichmentHelpers) also decline: they are not the
node's page but what stands in its place while the page cannot be built, and the node's own stamps
describe the INSTANCE while the broken thing is its TYPE.
Measuring it on a rendered page
The standard header's provenance row renders into an area with a stable id,
MeshNodeLayoutAreas.NodeMetaArea (NodeMeta). Nested containers render into
{parentArea}/{childId}, so its depth varies by page but its store key always ends in /NodeMeta.
A default WithNodePage landing page carries no NodeMeta area: its line is in the breadcrumb.
That id exists because the previous way to answer "does this page carry provenance?" was to read
the rendered HTML for the English word Created: — an assertion that a German reader sees an
English page, and one that would keep passing after somebody translated it.
What the guard covers, and the two halves it cannot
NodePageProvenanceGuard scans src/ for a renderer registered on a landing area through the bare
WithView / WithNamedRenderer, and reds naming the file. Exceptions are reasoned lines in
test/NodePagesWithoutProvenance.allow, which shrinks only — a stale entry reds, so the reason
beside it is re-read when it stops being true.
It deliberately over-flags: an area name is a landing page on one hub and an ordinary tab on
another (Search is Partition's landing page and every other node's search tab), and a scanner
cannot tell which hub a registration lands on. Over-flagging costs a reasoned line; under-flagging
costs a page nobody notices is missing something.
Two halves are outside its reach, and saying so is part of the design rather than an omission:
- In-mesh source. Most of the fleet's landing pages are
Source/*.csnode content and NodeTypeconfigurationlambdas that compile at RUNTIME in the portal — invisible to every compiler and every scan in this repository. Those 82 pages are reached by the changed DEFAULT, not by the guard: a page that moves toWithNodePagegains the line, one that stays onWithViewstays as it is. Widening the guard's roots would produce a guard that scans a tree the defect does not live in. - The composed page is not the only shape. A page can render
BuildMetaRowitself, anywhere in its own tree, and declareRenderedByThePage. The guard takes that declaration at its word; it does not verify the call. Verifying it would mean rendering every node type's landing page, which needs an instance of each type, and the declaration is the thing a reviewer reads anyway.
Related
- User Interface — how a layout area is composed
- Localization — why the strip's labels are keys and its timestamps are zoned
- Reading CI Signals — why a guard that cannot fail is not a guard