Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,36 @@ Two things make that an honest answer rather than a shrug.

`Error` rather than `Warning` is deliberate and costs nothing extra: both levels ship to Loki and the path is exceptional, so the only thing that changes is that it trips error-rate alerting. That is the right classification — nothing retries the write, the loss is permanent, and the visible consequence is a cell that tells a human it has never run. "Degraded but self-correcting" is what `Warning` claims, and none of it holds here.

What is still **not** covered: a viewer looking at a cell after a reload cannot tell `NeverRun` from "ran, not recorded". Closing that needs a surface that finds a cell's runs by `ActivityLog.HubPath` rather than by the stamp — a read that does not depend on the stamp, not another write. See [issue #3301](https://github.com/Systemorph/MeshWeaver/issues/3301).
#### Recovering the run for the READER — follow the edge, do not write another marker

The three candidates above all fail for the same reason: they are **writes**. What the reader needs is a **read**, and the edge to read along already exists. `CodeRunHistory.ResolveOutputCurrency` (`MeshWeaver.Mesh.Contract`, beside the currency rule it extends) walks it:

```csharp
// The verdict a cell can substantiate when its own stamp cannot supply one.
// Reactive like every other read — one verdict, then completion.
code.ResolveOutputCurrency(cellPath, viewerHome, meshService)
.Subscribe(verdict => { /* NeverRun · Unverified · Stale · Current */ });
```

| Step | What happens | Cost |
|---|---|---|
| 1 | `OutputCurrency()` — the stamp. Anything but `NeverRun` is returned unchanged. | **no query** |
| 2 | Only on `NeverRun`: one listing for an Activity naming this cell on `HubPath`. A hit ⇒ `Unverified`. | one row |
| 3 | No hit ⇒ `NeverRun`, exactly as today. | — |

Three properties are what make this the right shape rather than merely a working one.

**It is a listing by predicate, not a point read.** `Query` is eventually consistent, so it may never decide what a *specific known node* contains — that stays `GetMeshNodeStream`'s job. Here a stale negative is harmless by construction: the worst outcome is the answer the cell already gives today.

**A recovered run is `Unverified`, never `Current`.** The Activity records *that* the cell ran, never *what* it ran — no fingerprint travels with it. That is the same evidence a stamp which landed without its hash leaves, so it gets the same fail-closed verdict, for the same reason.

**It runs only where it can change the outcome.** A notebook of cells that ran normally costs zero queries, because step 1 answers first. This is the whole reason the lookup hangs off `NeverRun` rather than being a general "find this cell's runs" call every view makes.

**Where it looks.** The run lands under `{activityParent}/_Activity`, and `CodeNodeType.ResolveActivityParent` picks that parent in three layers: the Code node's own `CodeConfiguration.ActivityParentPath`, then the partition's `PartitionDefinition.DefaultActivityParentPath`, then the partition root — with the `{viewer}` sentinel at either configured layer expanding to the calling user's home. `CodeRunHistory.ActivityNamespaces` reconstructs the layers it can see from the cell alone — the node's own `ActivityParentPath` with `{viewer}` expanded, the partition root, and the reading viewer's home — deduplicated into a single union query, usually one namespace. A partition-level `DefaultActivityParentPath` pointing somewhere else is **not** covered: that lookup is a live query on the partition registry, which `MeshWeaver.Mesh.Contract` sits below. Where it applies the listing finds nothing and the verdict falls back to `NeverRun` — the answer the cell gives today, so the gap costs nothing that was already working. A caller that has already resolved the parent (the dispatcher has, via `CodeNodeType.ResolveActivityParent`) skips the derivation and calls `CodeRunHistory.RunsQuery` with the namespace it resolved.

🚨 **Two cross-assembly agreements hold this together, and neither is visible to a compiler.** The lookup filters on `nodeType:Activity` and expands the `{viewer}` sentinel — both declared in assemblies it sits *below* (`GraphNodeTypeNames.Activity`, `CodeNodeType.ResolveActivityParent`). A rename on either side would leave the query looking for something nothing writes: zero rows, every recovered cell silently back to `NeverRun`, no compiler error anywhere. `CodeRunHistoryTest` pins both — the node-type name by equality, the sentinel by *driving the dispatcher's own resolver* and requiring the answer the lookup assumes.

**What remains uncovered.** The lookup answers *whether* the cell ran, not *what it produced*: it deliberately does not re-point the output pane at the recovered activity. And an operator still gets the `Error` under event id `3249` — the stamp failing is a real fault, and a reader who can no longer see the consequence is not a reason to stop reporting it.

### 2. From application code — `SubmitCodeRequest` directly

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
Name: A cell that ran no longer claims it never did
Category: Fix
Description: When the bookkeeping write that records a script run failed, the cell reported itself as never run after the next page load — even though the run had happened and its log was still there. The cell now finds the run itself, and says its output is unverified rather than pretending there is none.
Icon: History
Order: -20260905
---

# A cell that ran no longer claims it never did

Press **Run** on a code cell and two separate things are written. The run itself gets its own
record — the activity, with its log, its status and everything the script printed. Separately, a
short note is written back onto the cell: *this cell was run, at this time, by this person, and
here is where the log lives*.

That second note is what the cell reads on your next visit. It is one small write, and very
occasionally it does not land — a busy moment at startup, a refused write, content the platform
cannot read back. When that happened, the note was simply absent, and the cell had nothing left to
tell you with.

So it told you the only thing it could see: **nothing had ever run here.**

That was wrong, and it was wrong in the least helpful direction. The run had happened. Its log was
still sitting in your activity list, complete. The only thing missing was the cell's pointer to it —
and the cell reported that missing pointer as a missing run.

## What happens now

The cell looks for its own runs instead of relying on the note. Every run writes down which cell it
came from, before it starts, so the trail back exists whether or not the note was ever written.

- A cell whose note is intact behaves exactly as before — nothing extra happens, and nothing got
slower.
- A cell whose note went missing now finds the run and says so, marking its output **unverified**:
the run is real, but nothing recorded which version of the code it ran, so the cell will not
claim the output matches what you are looking at.
- A cell nobody has ever run still says nothing at all. An unrun cell has no output to be wrong
about, and it should not carry a warning.

"Unverified" rather than "up to date" is deliberate. The recovered run proves *that* the cell ran;
it does not prove *what* it ran. Telling you the output is current when nothing substantiates that
would be a wrong claim, not merely a missing one — so the cell tells you what it actually knows and
lets you decide whether to run it again.
7 changes: 5 additions & 2 deletions src/MeshWeaver.Graph/Configuration/CodeNodeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,11 @@ public static IObservable<string> ResolveActivityParent(
return unresolved switch
{
null => partitionRoot,
"{viewer}" when !string.IsNullOrEmpty(viewerHome) => viewerHome!,
"{viewer}" => partitionRoot,
// Spelled through the constant, not the literal: the #3301 lookup that finds a
// cell's runs when the stamp did not land has to expand the sentinel THE SAME
// WAY, and two literals in two assemblies are free to drift silently.
CodeRunHistory.ViewerHomeSentinel when !string.IsNullOrEmpty(viewerHome) => viewerHome!,
CodeRunHistory.ViewerHomeSentinel => partitionRoot,
var p => p
};
});
Expand Down
166 changes: 166 additions & 0 deletions src/MeshWeaver.Mesh.Contract/CodeOutputCurrency.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
using System.Collections.Immutable;
using System.Reactive.Linq;
using MeshWeaver.Mesh.Services;

namespace MeshWeaver.Mesh;

/// <summary>
Expand Down Expand Up @@ -125,3 +129,165 @@ public static CodeOutputCurrency OutputCurrency(this CodeConfiguration? code)
public static bool ProvesOutputIsCurrent(this CodeConfiguration? code) =>
code.OutputCurrency() is CodeOutputCurrency.Current;
}

/// <summary>
/// The half of the verdict that does NOT depend on the last-execution stamp (#3301).
///
/// <para><b>The gap this closes.</b> A cell reads its own run history from ONE denormalised
/// pointer — the stamp that writes <see cref="CodeConfiguration.LastExecutedAt"/>,
/// <see cref="CodeConfiguration.LastExecutedBy"/>, <see cref="CodeConfiguration.LastActivityPath"/>
/// and <see cref="CodeConfiguration.LastExecutedCodeHash"/> together. When that write does not land
/// (#3249: the workspace not ready, a refused partition write, content that cannot be read as a
/// <see cref="CodeConfiguration"/>) the pointer is simply absent, and after a page reload the cell
/// is indistinguishable from one nobody has ever run. <see cref="CodeOutputCurrencyExtensions.OutputCurrency"/>
/// answers <see cref="CodeOutputCurrency.NeverRun"/> — correctly, from the evidence it has — and the
/// reader is told the run never happened.</para>
///
/// <para><b>Why a READ is the fix and a write is not.</b> The run is not lost. The dispatcher
/// creates the Activity node BEFORE it dispatches and stamps the originating cell onto it
/// (<c>ActivityLog.HubPath</c> = the Code node's path), so the cell → run edge is durable; it is
/// only missing in the direction the view reads it. Every scheme that recovers it by putting a
/// marker on the CELL is a second write to the node whose first write just failed — recovery in the
/// same failure domain — which is why <c>Doc/Architecture/ScriptExecution</c> → "When the stamp does
/// not land" rules all three out. Following the edge backwards needs no write at all.</para>
///
/// <para><b>Why this is a legitimate query.</b> It is a LISTING BY PREDICATE, where a stale negative
/// is harmless: the worst outcome is the answer the cell already gives today. It is emphatically not
/// the forbidden shape — reading one known node's content by path — which stays
/// <c>GetMeshNodeStream</c>'s job.</para>
///
/// <para><b>And why it is not run on every cell.</b> <see cref="ResolveOutputCurrency"/> consults
/// the mesh ONLY where the stamp answers <see cref="CodeOutputCurrency.NeverRun"/> — the one state
/// where the lookup can change the outcome. A notebook of cells that ran normally costs zero
/// queries.</para>
/// </summary>
public static class CodeRunHistory
{
/// <summary>
/// The <c>nodeType</c> a run's Activity node carries. Spelled as a literal because
/// <c>MeshWeaver.Mesh.Contract</c> sits BELOW <c>MeshWeaver.Graph.Contract</c>, where
/// <c>GraphNodeTypeNames.Activity</c> declares it — the two are pinned equal by
/// <c>CodeRunHistoryTest.TheActivityNodeTypeNameMatchesTheGraphVocabulary</c> rather than left to
/// agree by luck.
/// </summary>
public const string ActivityNodeTypeName = "Activity";

/// <summary>
/// The sentinel a Code node (or a <c>PartitionDefinition</c>) may put in
/// <see cref="CodeConfiguration.ActivityParentPath"/> to mean "the VIEWER's home partition, not
/// mine" — expanded by <c>CodeNodeType.ResolveActivityParent</c> at dispatch, and expanded the
/// same way here so the lookup searches where the run actually landed.
/// </summary>
public const string ViewerHomeSentinel = "{viewer}";

/// <summary>
/// The one spelling of "this cell's own runs": Activity satellites in
/// <paramref name="activityNamespace"/> whose <c>ActivityLog.HubPath</c> is
/// <paramref name="cellPath"/>.
///
/// <para>Ordered newest-first and capped at one row: the question is EXISTENCE, so listing every
/// run of a cell that has run a thousand times would pay for nine hundred and ninety-nine rows
/// nobody reads.</para>
/// </summary>
/// <param name="cellPath">Path of the Code node — the value the dispatcher wrote to <c>HubPath</c>.</param>
/// <param name="activityNamespace">The <c>{parent}/_Activity</c> namespace to look in.</param>
public static string RunsQuery(string cellPath, string activityNamespace) =>
$"namespace:{activityNamespace} nodeType:{ActivityNodeTypeName} "
+ $"content.hubPath:{cellPath} sort:LastModified-desc limit:1";
Comment on lines +194 to +196

/// <summary>
/// The <c>_Activity</c> namespaces a cell's runs can land in, derived from the data at hand —
/// the cell's own <see cref="CodeConfiguration.ActivityParentPath"/> (with
/// <see cref="ViewerHomeSentinel"/> expanded), its partition root (the default), and the
/// viewer's home. Deduplicated, so the common case is a single namespace.
///
/// <para>🚨 <b>What this deliberately does NOT cover, stated rather than hidden.</b> A
/// <c>PartitionDefinition.DefaultActivityParentPath</c> pointing at a FOURTH place is invisible
/// here: that lookup is a live workspace query on the partition registry, which this assembly
/// sits below. Where it applies, the lookup finds nothing and the verdict falls back to
/// <see cref="CodeOutputCurrency.NeverRun"/> — exactly what the cell says today, so the gap
/// costs nothing that was already working. A caller that has ALREADY resolved the parent (the
/// dispatcher does, through <c>CodeNodeType.ResolveActivityParent</c>) skips this derivation and
/// calls <see cref="RunsQuery"/> with the namespace it resolved.</para>
/// </summary>
/// <param name="cellPath">Path of the Code node.</param>
/// <param name="code">The cell's configuration, for its activity-parent override.</param>
/// <param name="viewerHome">The reading viewer's home partition, if known.</param>
public static ImmutableArray<string> ActivityNamespaces(
string? cellPath, CodeConfiguration? code, string? viewerHome)
{
if (string.IsNullOrEmpty(cellPath))
return [];

var partitionRoot = cellPath.Split('/', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
if (string.IsNullOrEmpty(partitionRoot))
return [];

var home = string.IsNullOrEmpty(viewerHome) ? null : viewerHome;
var configured = code?.ActivityParentPath switch
{
null or "" => null,
ViewerHomeSentinel => home ?? partitionRoot,
var p => p,
};

return new[] { configured, partitionRoot, home }
.Where(parent => !string.IsNullOrEmpty(parent))
.Select(parent => $"{parent}/{ActivityNodeGuard.ActivitySegment}")
.Distinct(StringComparer.Ordinal)
.ToImmutableArray();
}

/// <summary>
/// The cell's currency verdict, recovered from the mesh when its own stamp cannot supply one.
///
/// <list type="number">
/// <item><see cref="CodeOutputCurrencyExtensions.OutputCurrency"/> first. Anything but
/// <see cref="CodeOutputCurrency.NeverRun"/> is returned unchanged and costs NO query — the
/// stamp is the fast path and stays it.</item>
/// <item>Only on <see cref="CodeOutputCurrency.NeverRun"/>: one listing over the cell's
/// candidate <c>_Activity</c> namespaces for a run that names this cell. A hit means a run
/// happened; nothing about that run says WHAT it ran, so the honest verdict is
/// <see cref="CodeOutputCurrency.Unverified"/> — the same fail-closed answer as a stamp that
/// landed without its fingerprint, and for the same reason.</item>
/// <item>No hit ⇒ <see cref="CodeOutputCurrency.NeverRun"/>, unchanged. A cell nobody has run
/// still says nothing, and a viewer who cannot SEE the activity (the query is access-filtered
/// like every other) gets the same silence rather than a claim about data they may not read.</item>
/// </list>
///
/// <para><b>One-shot by construction.</b> The result is the query's <c>Initial</c> snapshot and
/// then completion — this is a read, not a binding. A live subscription per never-run cell is
/// the cost the issue's own analysis rules out, and a stale negative here is harmless by the
/// same argument that makes the listing legitimate at all. A view keeps its liveness from the
/// node stream it is already bound to: when a later run stamps the cell, the node emits and the
/// caller re-resolves.</para>
/// </summary>
/// <param name="code">The cell's configuration.</param>
/// <param name="cellPath">Path of the Code node.</param>
/// <param name="viewerHome">The reading viewer's home partition, if known.</param>
/// <param name="meshService">The mesh's query surface.</param>
/// <returns>Exactly one verdict, then completion.</returns>
public static IObservable<CodeOutputCurrency> ResolveOutputCurrency(
this CodeConfiguration? code,
string? cellPath,
string? viewerHome,
IMeshService meshService)
{
var stamped = code.OutputCurrency();
if (stamped is not CodeOutputCurrency.NeverRun)
return Observable.Return(stamped);

var namespaces = ActivityNamespaces(cellPath, code, viewerHome);
if (namespaces.Length == 0)
return Observable.Return(CodeOutputCurrency.NeverRun);

return meshService
.Query<MeshNode>(MeshQueryRequest.FromQueries(
namespaces.Select(ns => RunsQuery(cellPath!, ns))))
.Where(change => change.ChangeType is QueryChangeType.Initial)
.Take(1)
.Select(change => change.Items.Count > 0
? CodeOutputCurrency.Unverified
: CodeOutputCurrency.NeverRun);
}
}
Loading
Loading