Economy command: L-productive is one fault per unproductive component; a held-open catalog is a named refusal at the first read too

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
soul 2026-10-23 07:15:59 +02:00
commit 6befccce74
3 changed files with 63 additions and 14 deletions

View file

@ -304,11 +304,11 @@ public static class EconomyChecks
}
var final = Build(pick, 0);
var (rho, component) = Radius(final, n);
var faults = new List<EconomyFault>();
if (rho >= 1 - Margin)
string Name(int i) => i == labour ? "labour" : names[i];
foreach (var (rho, component) in Radii(final, n))
{
string Name(int i) => i == labour ? "labour" : names[i];
if (rho < 1 - Margin) continue;
var order = CycleOrder(final, n, component);
faults.Add(new EconomyFault(LProductive, Name(order[0]),
$"The goods cannot sustain themselves: spectral radius {rho:0.######} is not below 1, carried by {string.Join(" -> ", order.Select(Name))}.",
@ -336,9 +336,9 @@ public static class EconomyChecks
return u;
}
// The spectral radius of a nonnegative matrix: the largest over its strongly connected components that hold a cycle, each by
// power iteration on A + I (primitive when irreducible). An edge j -> i is a nonzero A[i, j]. Also the component carrying it.
private static (double Rho, List<int> Component) Radius(double[,] a, int n)
// The spectral radius of every strongly connected component of a nonnegative matrix, each by power iteration on A + I
// (primitive when irreducible). An edge j -> i is a nonzero A[i, j]. A component with no cycle has radius 0.
private static List<(double Rho, List<int> Component)> Radii(double[,] a, int n)
{
var reach = new bool[n, n];
for (var i = 0; i < n; i++)
@ -350,8 +350,7 @@ public static class EconomyChecks
for (var j = 0; j < n; j++)
if (reach[k, j]) reach[i, j] = true;
var done = new bool[n];
var best = 0.0;
var bestComponent = new List<int>();
var radii = new List<(double, List<int>)>();
for (var start = 0; start < n; start++)
{
if (done[start]) continue;
@ -359,9 +358,9 @@ public static class EconomyChecks
foreach (var i in component) done[i] = true;
var rho = Irreducible(a, component);
if (double.IsNaN(rho)) rho = double.PositiveInfinity;
if (rho > best) { best = rho; bestComponent = component; }
radii.Add((rho, component));
}
return (best, bestComponent);
return radii;
}
private static double Irreducible(double[,] a, List<int> nodes)

View file

@ -416,6 +416,28 @@ public sealed class EconomyCommandTests
Assert.Equal(bytes, File.ReadAllBytes(f.CatalogPath));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ANewUnproductiveCycleBelowTheHeldRadiusIsRefusedAloneOrBesideALoweringOfTheHeldCycle(bool alsoLowerHeld)
{
using var f = EconomyFixture.Healthy();
f.Good("loopa", inputs: new[] { ("loopb", 2) });
f.Good("loopb", inputs: new[] { ("loopa", 4) });
Flush(f);
var bytes = File.ReadAllBytes(f.CatalogPath);
var code = Author(f, true, out var output, a =>
{
if (alsoLowerHeld) Good(f, a, "loopb", inputs: new[] { ("loopa", 2) });
Good(f, a, "loopc", inputs: new[] { ("loopd", 1) });
Good(f, a, "loopd", inputs: new[] { ("loope", 1) });
Good(f, a, "loope", inputs: new[] { ("loopc", 2) });
});
Assert.Equal(1, code);
Assert.Contains("L-productive", output);
Assert.Equal(bytes, File.ReadAllBytes(f.CatalogPath));
}
[Fact]
public void AHeldCycleWhoseRadiusFallsStillApplies()
{
@ -520,6 +542,23 @@ public sealed class EconomyCommandTests
Assert.Empty(Directory.GetFiles(Path.GetDirectoryName(f.CatalogPath), "*.economy-*"));
}
[Fact]
public void ACatalogHeldOpenForWritingIsANamedRefusalAndStaysIntact()
{
if (!OperatingSystem.IsWindows()) return;
using var f = EconomyFixture.Healthy();
Flush(f);
var bytes = File.ReadAllBytes(f.CatalogPath);
using (new FileStream(f.CatalogPath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
{
var code = Author(f, true, out var output, a => Good(f, a, "coal"));
Assert.Equal(1, code);
Assert.Contains("close Unity", output);
Assert.DoesNotContain(" at ", output);
}
Assert.Equal(bytes, File.ReadAllBytes(f.CatalogPath));
}
[Fact]
public void TheSwapKeepsTheCatalogsFileMode()
{

View file

@ -121,19 +121,26 @@ public static class EconomyCommand
try
{
var trial = Path.Combine(scratch, "Aetheria.cc");
var fingerprint = Fingerprint(catalogPath);
File.Copy(catalogPath, trial);
new FileInfo(trial).IsReadOnly = false;
string fingerprint;
Dictionary<string, EconomyFault> held;
HashSet<string> recipesHeld;
Dictionary<string, byte[]> original;
using (var cache = AetheriaStores.Open(catalogPath, registry: registry))
try
{
fingerprint = Fingerprint(catalogPath);
File.Copy(catalogPath, trial);
new FileInfo(trial).IsReadOnly = false;
using var cache = AetheriaStores.Open(catalogPath, registry: registry);
held = new Dictionary<string, EconomyFault>(StringComparer.Ordinal);
foreach (var fault in EconomyChecks.Check(cache).Faults) held[Identity(fault)] = fault;
recipesHeld = new HashSet<string>(cache.GetAll<RecipeData>().Select(r => cache.RefOf(r).Key.Value), StringComparer.Ordinal);
original = Records(cache);
}
catch (Exception e) when (e is UnauthorizedAccessException || e is IOException)
{
output.WriteLine(Held("read", e));
return 1;
}
var touched = new List<string>();
int written;
using (var cache = AetheriaStores.Open(trial, catalogWritable: true, registry: registry, kindlessStation: warning => output.WriteLine("warning: " + warning)))
@ -214,6 +221,10 @@ public static class EconomyCommand
}
}
// Something holds Aetheria.cc open: a refusal that names the holder to close, never an exception.
private static string Held(string verb, Exception e) =>
$"The batches are refused: Aetheria.cc could not be {verb} ({e.Message}). Something holds the file: close Unity and any other editor or tool that has it open, then run again. The catalog is unchanged.";
// A fault is the same fault by rule, record and message, so fixing one fault on a record cannot hide a new one there. L-productive
// is one fault per cycle: its identity is the cycle's member set, and the same cycle with a higher radius is a worse fault.
private static string Identity(EconomyFault fault) =>