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
153 changes: 143 additions & 10 deletions src/MeshWeaver.Compiler.Pipeline/NodeTypeCompilationHelpers.cs

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/MeshWeaver.Compiler.Pipeline/NodeTypeCompileState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ public record NodeTypeCompileState
/// beside it.</summary>
public System.Collections.Immutable.ImmutableList<string>? AdoptedSourcePaths { get; init; }

/// <summary>See <see cref="NodeTypeDefinition.AdoptedSourceIncludes"/> (#4280).</summary>
public System.Collections.Immutable.ImmutableList<string>? AdoptedSourceIncludes { get; init; }

/// <summary>See <see cref="NodeTypeDefinition.CurrentSourceIncludes"/> (#4280).</summary>
public System.Collections.Immutable.ImmutableList<string>? CurrentSourceIncludes { get; init; }

/// <summary>See <see cref="NodeTypeDefinition.CurrentModuleVersion"/> — the module version of
/// the source this mesh HOLDS. Mirrored for the same reason as the pair beside it.</summary>
public string? CurrentModuleVersion { get; init; }
Expand Down Expand Up @@ -184,6 +190,8 @@ definition is null
FailedSourceQueries = definition.FailedSourceQueries,
AdoptedSourceFingerprint = definition.AdoptedSourceFingerprint,
AdoptedSourcePaths = definition.AdoptedSourcePaths,
AdoptedSourceIncludes = definition.AdoptedSourceIncludes,
CurrentSourceIncludes = definition.CurrentSourceIncludes,
AdoptedModuleVersion = definition.AdoptedModuleVersion,
CurrentModuleVersion = definition.CurrentModuleVersion,
CurrentSourceFingerprint = definition.CurrentSourceFingerprint,
Expand All @@ -205,6 +213,7 @@ CompilationStatus is null && CompilationError is null && CompilationDiagnostics
&& CompiledFrameworkVersion is null && FailedBuildInputs is null
&& FailedSourceQueries is null
&& AdoptedSourceFingerprint is null && AdoptedSourcePaths is null
&& AdoptedSourceIncludes is null && CurrentSourceIncludes is null
&& AdoptedModuleVersion is null
&& CurrentModuleVersion is null
&& CurrentSourceFingerprint is null
Expand Down
75 changes: 69 additions & 6 deletions src/MeshWeaver.Compiler.Pipeline/PrebuiltAssemblySeeder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,28 @@ public static IObservable<bool> Seed(
string? sourceFingerprint,
string? moduleVersion,
IReadOnlyList<string>? sourcePaths)
=> Seed(hub, nodeTypePath, assemblyBytes, pdbBytes, frameworkMvid, logger, dependencies,
sourceFingerprint, moduleVersion, sourcePaths, sourceIncludes: null);

/// <summary>
/// <see cref="Seed(IMessageHub, string, byte[], byte[], string, ILogger, IReadOnlyDictionary{string, string}, string, string, IReadOnlyList{string})"/>
/// carrying the bundle's <c>@@</c>-include paths as well (MeshWeaver#4280) — see the
/// <c>SeedDetailed</c> overload of the same arity.
/// </summary>
public static IObservable<bool> Seed(
IMessageHub hub,
string nodeTypePath,
byte[] assemblyBytes,
byte[]? pdbBytes,
string? frameworkMvid,
ILogger? logger,
IReadOnlyDictionary<string, string>? dependencies,
string? sourceFingerprint,
string? moduleVersion,
IReadOnlyList<string>? sourcePaths,
IReadOnlyList<string>? sourceIncludes)
=> SeedDetailed(hub, nodeTypePath, assemblyBytes, pdbBytes, frameworkMvid, logger, dependencies,
sourceFingerprint, moduleVersion, sourcePaths)
sourceFingerprint, moduleVersion, sourcePaths, sourceIncludes)
.Select(outcome => outcome is SeedOutcome.Adopted or SeedOutcome.AdoptedStale);

/// <summary>What <see cref="DecideAfterStaleDecline"/> concluded for one declined entry.</summary>
Expand Down Expand Up @@ -613,6 +633,28 @@ public static IObservable<SeedOutcome> SeedDetailed(
string? sourceFingerprint,
string? moduleVersion,
IReadOnlyList<string>? sourcePaths)
=> SeedDetailed(hub, nodeTypePath, assemblyBytes, pdbBytes, frameworkMvid, logger, dependencies,
sourceFingerprint, moduleVersion, sourcePaths, sourceIncludes: null);

/// <summary>
/// <see cref="SeedDetailed(IMessageHub, string, byte[], byte[], string, ILogger, IReadOnlyDictionary{string, string}, string, string, IReadOnlyList{string})"/>
/// carrying the bundle's <c>@@</c>-include paths as well (MeshWeaver#4280, second finding on
/// #4293) — the manifest's <c>sourceIncludes</c>, stamped as
/// <see cref="NodeTypeDefinition.AdoptedSourceIncludes"/> and read by the pre-write decline
/// beside the query-resolved paths. Null from a producer that recorded none.
/// </summary>
public static IObservable<SeedOutcome> SeedDetailed(
IMessageHub hub,
string nodeTypePath,
byte[] assemblyBytes,
byte[]? pdbBytes,
string? frameworkMvid,
ILogger? logger,
IReadOnlyDictionary<string, string>? dependencies,
string? sourceFingerprint,
string? moduleVersion,
IReadOnlyList<string>? sourcePaths,
IReadOnlyList<string>? sourceIncludes)
{
// 🚨 THE GATE. FrameworkVersion is the resolved framework build identity — a content/
// surface identity, not a version string — and the assembly-store key carries the first
Expand Down Expand Up @@ -704,7 +746,7 @@ public static IObservable<SeedOutcome> SeedDetailed(
.Take(1)
.SelectMany(node => SeedObserved(
hub, workspace, node!, nodeTypePath, assemblyBytes, pdbBytes, logger,
dependencies, sourceFingerprint, moduleVersion, sourcePaths)));
dependencies, sourceFingerprint, moduleVersion, sourcePaths, sourceIncludes)));
});
}

Expand All @@ -721,7 +763,8 @@ private static IObservable<SeedOutcome> SeedObserved(
IReadOnlyDictionary<string, string>? dependencies,
string? sourceFingerprint,
string? moduleVersion,
IReadOnlyList<string>? sourcePaths)
IReadOnlyList<string>? sourcePaths,
IReadOnlyList<string>? sourceIncludes)
{
var observed = node.ContentAs<NodeTypeDefinition>(hub.JsonSerializerOptions);
if (observed is null)
Expand Down Expand Up @@ -780,7 +823,8 @@ private static IObservable<SeedOutcome> SeedObserved(
if (sourceFingerprint is { Length: > 0 } producerFingerprint
&& observed.CurrentSourceFingerprint is { Length: > 0 } liveFingerprint
&& NodeTypeCompilationHelpers.CanJudgeAdoption(
producerFingerprint, liveFingerprint, observed.CurrentSourceVersions, sourcePaths)
producerFingerprint, liveFingerprint, observed.CurrentSourceVersions, sourcePaths,
sourceIncludes, observed.CurrentSourceIncludes)
&& !string.Equals(producerFingerprint, liveFingerprint, StringComparison.Ordinal))
{
var verdict = ModuleVersionCompatibility.Classify(moduleVersion, observed.CurrentModuleVersion);
Expand Down Expand Up @@ -819,12 +863,26 @@ private static IObservable<SeedOutcome> SeedObserved(
if (sourceFingerprint is { Length: > 0 } deferred
&& observed.CurrentSourceFingerprint is { Length: > 0 } unjudged
&& !NodeTypeCompilationHelpers.CanJudgeAdoption(
deferred, unjudged, observed.CurrentSourceVersions, sourcePaths)
deferred, unjudged, observed.CurrentSourceVersions, sourcePaths,
sourceIncludes, observed.CurrentSourceIncludes)
&& !string.Equals(deferred, unjudged, StringComparison.Ordinal))
{
var notYetLive = NodeTypeCompilationHelpers.SourcePathsNotYetLive(
sourcePaths, observed.CurrentSourceVersions);
if (observed.CurrentSourceVersions is { Count: > 0 } partial && notYetLive.Count > 0)
var includesNotYetLive = NodeTypeCompilationHelpers.SourceIncludesNotYetLive(
sourceIncludes, observed.CurrentSourceIncludes);
if (observed.CurrentSourceVersions is { Count: > 0 } && notYetLive.Count == 0
&& includesNotYetLive.Count > 0)
logger?.LogWarning(
"Prebuilt assembly for {NodeTypePath}: the bundle records source fingerprint "
+ "{Producer} and this mesh's live fingerprint is {Live}, but {Missing} of the "
+ "{Declared} @@-include(s) the bundle was compiled with are not resolved as "
+ "present here yet (#4280): {Paths}. An absent include is an arrival, not a "
+ "move, so the bundle is ADOPTED and the owner defers its judgement until "
+ "they land.",
nodeTypePath, deferred, unjudged, includesNotYetLive.Count, sourceIncludes!.Count,
string.Join(", ", includesNotYetLive.Take(6)) + (includesNotYetLive.Count > 6 ? ", …" : ""));
else if (observed.CurrentSourceVersions is { Count: > 0 } partial && notYetLive.Count > 0)
logger?.LogWarning(
"Prebuilt assembly for {NodeTypePath}: the bundle records source fingerprint "
+ "{Producer} and this mesh's live fingerprint is {Live}, but the live set holds "
Expand Down Expand Up @@ -943,6 +1001,11 @@ IObservable<SeedOutcome> Write(SeedOutcome onStamped)
AdoptedSourcePaths = sourcePaths is { Count: > 0 }
? sourcePaths.ToImmutableList()
: null,
// #4280 — and WHICH includes, so an included node still landing is an
// arrival to the owner and not a move.
AdoptedSourceIncludes = sourceIncludes is { Count: > 0 }
? sourceIncludes.ToImmutableList()
: null,
// #3583 — the module version these bytes were released at, for the
// compatibility rule the owner applies when the source later moves.
// Null from a producer that recorded none is carried as null: the
Expand Down
30 changes: 29 additions & 1 deletion src/MeshWeaver.Compiler/NodeTypeSourceFingerprint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,29 @@ public static IObservable<string> Compute(
string nodeTypePath,
Func<string, string?, IObservable<(MeshNode? Node, string Path)>> readInclude,
ILogger? logger = null)
=> ComputeWithIncludes(sourceNodes, nodeTypePath, readInclude, logger)
.Select(computed => computed.Fingerprint);

/// <summary>
/// <see cref="Compute(IEnumerable{MeshNode}, string, Func{string, string, IObservable{ValueTuple{MeshNode, string}}}, ILogger)"/>
/// that also hands back WHICH <c>@@</c>-include targets the closure resolved as PRESENT — sorted
/// ordinal, the resolved mesh paths (MeshWeaver#4280, second finding on #4293).
///
/// <para>🚨 An ABSENT include is an answer (it contributes nothing to the bytes, so it is not in
/// the closure), and that is exactly why the paths matter as a separate fact: a producer that
/// baked WITH an include and a consumer whose copy of that include has not landed yet fold to
/// DIFFERENT fingerprints, and nothing in the hash says whether the difference is a move or an
/// arrival. The producer records the include paths beside the query-resolved
/// <c>sourceVersions</c>; the owner publishes this list as
/// <c>NodeTypeDefinition.CurrentSourceIncludes</c>; and
/// <c>NodeTypeCompilationHelpers.CanJudgeAdoption</c> defers while any recorded include is not
/// yet among the present ones — the same witness the query-resolved paths already get.</para>
/// </summary>
public static IObservable<(string Fingerprint, ImmutableList<string> Includes)> ComputeWithIncludes(
IEnumerable<MeshNode> sourceNodes,
string nodeTypePath,
Func<string, string?, IObservable<(MeshNode? Node, string Path)>> readInclude,
ILogger? logger = null)
{
ArgumentNullException.ThrowIfNull(sourceNodes);
ArgumentException.ThrowIfNullOrEmpty(nodeTypePath);
Expand All @@ -159,9 +182,14 @@ public static IObservable<string> Compute(

return NodeCompileShaping
.CollectIncludeClosure(sources, nodeTypePath, readInclude, log)
.Select(closure => Fold(sources, paths, closure));
.Select(closure => (Fold(sources, paths, closure), IncludePathsOf(closure)));
}

/// <summary>The include closure's PATHS, sorted ordinal — the shape both the bundle manifest
/// (<c>sourceIncludes</c>) and <c>NodeTypeDefinition.CurrentSourceIncludes</c> carry.</summary>
public static ImmutableList<string> IncludePathsOf(IReadOnlyDictionary<string, string> resolvedIncludes)
=> resolvedIncludes.Keys.OrderBy(k => k, StringComparer.Ordinal).ToImmutableList();

/// <summary>
/// The fingerprint for a caller that has ALREADY resolved the include closure — the tree bake,
/// whose compile substituted the includes anyway and hands the collected set back on
Expand Down
22 changes: 22 additions & 0 deletions src/MeshWeaver.Graph.Contract/NodeTypeDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,28 @@ public record NodeTypeDefinition
/// </summary>
public ImmutableList<string>? AdoptedSourcePaths { get; init; }

/// <summary>
/// The <c>@@</c>-include paths the PRODUCER compiled the adopted bytes with — the bundle
/// manifest's <c>sourceIncludes</c> (MeshWeaver#4280, second finding on #4293). The other half
/// of the completeness witness beside <see cref="AdoptedSourcePaths"/>: an include is matched
/// by no source query, lands as its own node, and until it does the owner's include reader
/// answers ABSENT — an answer, not a stall — so the live fold is shorter and the fingerprints
/// differ for a reason that is an arrival, not a move. Judged against
/// <see cref="CurrentSourceIncludes"/>. Null for a locally-compiled build and for a bundle
/// whose producer recorded none. Operational, never authored.
/// </summary>
public ImmutableList<string>? AdoptedSourceIncludes { get; init; }

/// <summary>
/// The <c>@@</c>-include paths the sources watcher resolved as PRESENT when it last computed
/// <see cref="CurrentSourceFingerprint"/> — published in the SAME write (MeshWeaver#4280). Null
/// until the first fingerprint computation, and left at its previous value on an inconclusive
/// emission exactly as the fingerprint is; an empty list means "computed, and this type's
/// sources include nothing". Compared with <see cref="AdoptedSourceIncludes"/> by
/// <c>NodeTypeCompilationHelpers.CanJudgeAdoption</c>.
/// </summary>
public ImmutableList<string>? CurrentSourceIncludes { get; init; }

/// <summary>
/// Content fingerprint of the LIVE source set — the same
/// <see cref="Mesh.PartitionSourceFingerprint"/> shape as
Expand Down
7 changes: 4 additions & 3 deletions src/MeshWeaver.Hosting/ShippedPrebuiltBundles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1344,9 +1344,10 @@ private static IObservable<SeedTally> SeedPayloads(
a.SourceFingerprint,
// #3583 — the manifest's released SemVer, for the owner's compatibility rule.
moduleVersion,
// #4280 — the paths the bytes were built from, for the owner's
// arriving-vs-moved distinction.
a.SourcePaths)
// #4280 — the paths (and @@-includes) the bytes were built from, for
// the owner's arriving-vs-moved distinction.
a.SourcePaths,
a.SourceIncludes)
.Take(1)
.Timeout(SeedBudget)
.Do(outcome =>
Expand Down
4 changes: 4 additions & 0 deletions src/MeshWeaver.Mesh.Contract/NodeTypeOperationalContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ public static bool IsNodeTypeNode(MeshNode? node) =>
// live fingerprint a measurement rather than an install in flight. Operational for the
// same reason the fingerprint is: an authored list would hold an adoption unjudged.
"adoptedSourcePaths",
// #4280 — the include halves of the same witness: what the bundle was compiled with, and
// what the sources watcher resolved as present. Operational for the same reason.
"adoptedSourceIncludes",
"currentSourceIncludes",
// #3583 — the two module versions the compatibility rule compares. Operational for the
// same reason the fingerprints are: an authored adopted version would let a repo file
// declare its own build compatible.
Expand Down
17 changes: 17 additions & 0 deletions src/MeshWeaver.Plugin.Packaging/BundleReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ public sealed record AssemblyRef(
/// reason as <see cref="SourceFingerprint"/>.
/// </summary>
public IReadOnlyDictionary<string, long>? SourceVersions { get; init; }

/// <summary>The <c>@@</c>-include closure's resolved paths, sorted ordinal —
/// <c>BundleWriter.AssemblyEntry.SourceIncludes</c> (MeshWeaver#4280). Null for a producer
/// that recorded none.</summary>
public IReadOnlyList<string>? SourceIncludes { get; init; }
}

/// <summary>The bundle's compiled-module declaration.</summary>
Expand Down Expand Up @@ -197,6 +202,15 @@ public sealed record Payload(
/// alone, exactly as before.
/// </summary>
public IReadOnlyList<string>? SourcePaths { get; init; }

/// <summary>
/// The <c>@@</c>-include paths the bytes were compiled with, sorted ordinal (MeshWeaver#4280)
/// — the other half of the completeness witness. <see cref="SourcePaths"/> covers what the
/// type's source QUERIES resolve; an include is matched by no query, lands as its own node,
/// and is an ABSENT include (an answer) until it does — so the owner defers on this list
/// exactly as on the paths. Null for a legacy bundle: the owner then judges without it.
/// </summary>
public IReadOnlyList<string>? SourceIncludes { get; init; }
}

/// <summary>
Expand Down Expand Up @@ -278,6 +292,9 @@ public static (Manifest? Manifest, IReadOnlyList<Payload> Assemblies) Read(
SourcePaths = reference.SourceVersions is { Count: > 0 } versions
? versions.Keys.OrderBy(k => k, StringComparer.Ordinal).ToList()
: null,
SourceIncludes = reference.SourceIncludes is { Count: > 0 } includes
? includes.OrderBy(k => k, StringComparer.Ordinal).ToList()
: null,
});
}

Expand Down
15 changes: 15 additions & 0 deletions src/MeshWeaver.Plugin.Packaging/BundleWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ public sealed record AssemblyEntry(
/// right to refuse it) and would take down a mixed fleet on the roll.</para>
/// </summary>
public string? SourceFingerprint { get; init; }

/// <summary>
/// The <c>@@</c>-include closure these bytes were compiled with — the resolved mesh paths,
/// sorted ordinal (MeshWeaver#4280). Inside <see cref="SourceFingerprint"/> already, but the
/// hash cannot say WHICH paths, and the consumer needs them: an included Code node that has
/// not landed on the consumer yet is an ABSENT include there (an answer, not a stall), so
/// the live fold is shorter, the fingerprints differ, and without this list the owner reads
/// an arrival as a move. Beside <see cref="SourceVersions"/>, which is the query-resolved
/// half of the same question. Null from a producer that recorded none; init property for
/// the binary-break reason above.
/// </summary>
public IReadOnlyList<string>? SourceIncludes { get; init; }
}

/// <summary>
Expand Down Expand Up @@ -147,6 +159,9 @@ public static void Write(
// by BundleReader and carried to PrebuiltAssemblySeeder — it is the value the
// consumer's refusal is decided on.
sourceFingerprint = a.SourceFingerprint,
// #4280 — the include paths the fingerprint folded over, so the consumer's
// completeness witness covers includes as well as query-resolved sources.
sourceIncludes = a.SourceIncludes,
})
.ToList(),
// DECLARED, never left to be discovered by enumerating the folder — the same rule the
Expand Down
Loading
Loading