Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
80d7425
Initial commit with task details
konard Aug 29, 2026
293c00a
chore(rust): update doublets basis crate to 0.5.0
konard Aug 29, 2026
83f324e
feat(rust): reuse the doublets 0.5.0 decorator layer in DoubletsStorage
konard Aug 29, 2026
7632038
fix(rust): resolve uniqueness and cascades through the doublets decor…
konard Aug 29, 2026
2fcda42
fix(rust): keep hybrid external references out of the doublets servic…
Aug 29, 2026
66faf10
feat(rust): port the persistent transformation trigger decorator
Aug 29, 2026
dbdd193
feat(rust): expose the persistent transformation triggers on the CLI
konard Aug 29, 2026
6039c94
fix(rust): hand out addresses in the same order as the C# store
konard Aug 29, 2026
3afa78f
fix(rust): report the same changes as the C# CLI does
konard Aug 29, 2026
1cec228
docs(rust): add changelog fragments for the CLI triggers and the pari…
konard Aug 29, 2026
84560a2
test(rust): cover the trigger CLI flags end to end
konard Aug 29, 2026
85fa64d
docs: drop the stale claim that triggers are C#-only
konard Aug 29, 2026
f180a63
feat(rust): make the whole library reachable from outside the crate
konard Aug 29, 2026
db4ce7e
feat(csharp): keep every decorator open for extension
konard Aug 29, 2026
3d53362
docs(issue-100): capture the platform-data external-range overlap
konard Aug 29, 2026
3ff7f28
fix(rust): resolve unspecified substitution halves like C# resolves Any
konard Aug 29, 2026
6fde7fc
docs(issue-100): write the case study and ship the verification logs
konard Aug 29, 2026
0f21839
docs(issue-100): fix the relative link back to the case study
konard Aug 29, 2026
f9e567f
refactor(rust): split the query processor and transactions decorator
konard Aug 29, 2026
d4d01a1
docs(issue-100): record the module split in the case study
konard Aug 29, 2026
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
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ cargo install link-cli
cargo add link-cli
```

The NuGet CLI tool is the C# implementation and exposes the complete production
command surface, including persistent transformation triggers. The Rust crate
mirrors the core query engine, named references, LiNo import/export, structure
formatting, and the WebAssembly workbench API. Persistent transformation
trigger CLI options currently exist only in the C# tool.
Both implementations expose the same command surface, so a scenario written
against one runs unchanged against the other: the query engine, named
references, LiNo import/export, structure formatting, persistent
transformation triggers, transactions and version control. The Rust crate adds
the WebAssembly workbench API on top.

This tool provides all CRUD operations for links using single [substitution operation](https://en.wikipedia.org/wiki/Markov_algorithm) ([ru](https://ru.wikipedia.org/wiki/Нормальный_алгоритм)) which is turing complete.

Expand Down Expand Up @@ -565,9 +565,7 @@ clink '((1: 2 1) (2: 1 2)) ()' --changes --after

## All options and arguments

The C# NuGet tool supports every option below. The Rust CLI currently supports
the core query, storage, output, import/export, and structure options; trigger
options are C#-only for now.
Both the C# NuGet tool and the Rust CLI support every option below.

| Parameter | Type | Default Value | Aliases | Description |
|-------------------------|---------|----------------|-------------------------------------|----------------------------------------------------------------------------|
Expand Down
5 changes: 5 additions & 0 deletions csharp/.changeset/issue-100-open-for-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'Foundation.Data.Doublets.Cli': minor
---

Opened the library up for extension: every decorator (`NamedTypesDecorator`, `NamedLinksDecorator`, `SimpleLinksDecorator`, `PinnedTypesDecorator`, `TransactionsDecorator`, `VersionControlDecorator`, `PersistentTransformationDecorator`) is now unsealed with overridable members, disposable ones follow the `protected virtual void Dispose(bool)` pattern so a subclass can release resources of its own, and `PersistentTransformationDecorator.PersistentTransformationQuery` and `InternalNamePrefix` are public. A custom CLI can now subclass any layer of the stack instead of forking it.
16 changes: 8 additions & 8 deletions csharp/Foundation.Data.Doublets.Cli.Library/NamedLinks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,25 @@ public NamedLinks(
_getString = getString;
}

public TLinkAddress SetNameForExternalReference(TLinkAddress link, string name)
public virtual TLinkAddress SetNameForExternalReference(TLinkAddress link, string name)
{
var reference = new Hybrid<TLinkAddress>(link, isExternal: true);
return SetName(reference, name);
}

public TLinkAddress SetName(TLinkAddress link, string name)
public virtual TLinkAddress SetName(TLinkAddress link, string name)
{
var nameSequence = _createString(name);
return _links.GetOrCreate(link, _links.GetOrCreate(_nameType, nameSequence));
}

public string? GetNameByExternalReference(TLinkAddress link)
public virtual string? GetNameByExternalReference(TLinkAddress link)
{
var reference = new Hybrid<TLinkAddress>(link, isExternal: true);
return GetName(reference);
}

public string? GetName(TLinkAddress link)
public virtual string? GetName(TLinkAddress link)
{
var any = _links.Constants.Any;
var query = new Link<TLinkAddress>(any, link, any);
Expand All @@ -62,7 +62,7 @@ public TLinkAddress SetName(TLinkAddress link, string name)
return null;
}

public TLinkAddress GetByName(string name)
public virtual TLinkAddress GetByName(string name)
{
var nameSequence = _createString(name);
var nameLink = _links.SearchOrDefault(_nameType, nameSequence);
Expand All @@ -80,7 +80,7 @@ public TLinkAddress GetByName(string name)
return _links.GetSource(link);
}

public TLinkAddress GetExternalReferenceByName(string name)
public virtual TLinkAddress GetExternalReferenceByName(string name)
{
var nameSequence = _createString(name);
var nameLink = _links.SearchOrDefault(_nameType, nameSequence);
Expand All @@ -103,7 +103,7 @@ public TLinkAddress GetExternalReferenceByName(string name)
return _links.Constants.Null;
}

public void RemoveName(TLinkAddress link)
public virtual void RemoveName(TLinkAddress link)
{
var any = _links.Constants.Any;
var query = new Link<TLinkAddress>(any, link, any);
Expand All @@ -124,7 +124,7 @@ public void RemoveName(TLinkAddress link)
}
}

public void RemoveNameByExternalReference(TLinkAddress externalReference)
public virtual void RemoveNameByExternalReference(TLinkAddress externalReference)
{
var reference = new Hybrid<TLinkAddress>(externalReference, isExternal: true);
RemoveName(reference);
Expand Down
22 changes: 17 additions & 5 deletions csharp/Foundation.Data.Doublets.Cli.Library/NamedLinksDecorator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

namespace Foundation.Data.Doublets.Cli
{
public sealed class NamedLinksDecorator<TLinkAddress> : LinksDecoratorBase<TLinkAddress>, INamedTypesLinks<TLinkAddress>, IDisposable
public class NamedLinksDecorator<TLinkAddress> : LinksDecoratorBase<TLinkAddress>, INamedTypesLinks<TLinkAddress>, IDisposable
where TLinkAddress : struct,
IUnsignedNumber<TLinkAddress>,
IComparisonOperators<TLinkAddress, TLinkAddress, bool>,
Expand Down Expand Up @@ -69,9 +69,21 @@ public NamedLinksDecorator(string databaseFilename, bool tracingEnabled = false)
/// Releases the memory-mapped file handles of both the data and the names databases.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Releases the databases this decorator owns. Derived decorators that
/// own extra resources override this and call
/// <c>base.Dispose(disposing)</c>.
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
_disposed = true;
if (!disposing) return;
LinksFacadeDisposer.Dispose(_namedLinksFacade);
LinksFacadeDisposer.Dispose(_links);
}
Expand All @@ -81,7 +93,7 @@ public void Dispose()
/// </summary>
/// <param name="link">The link address to get the name for.</param>
/// <returns>The name associated with the link, or null if no name is set.</returns>
public string? GetName(TLinkAddress link)
public virtual string? GetName(TLinkAddress link)
{
if (_tracingEnabled) Console.WriteLine($"[Trace] GetName called for link: {link}");
var result = NamedLinks.GetNameByExternalReference(link);
Expand All @@ -95,7 +107,7 @@ public void Dispose()
/// <param name="link">The link address to name.</param>
/// <param name="name">The name to assign to the link.</param>
/// <returns>The link address representing the name assignment.</returns>
public TLinkAddress SetName(TLinkAddress link, string name)
public virtual TLinkAddress SetName(TLinkAddress link, string name)
{
if (_tracingEnabled) Console.WriteLine($"[Trace] SetName called for link: {link} with name: '{name}'");
// Remove any existing name mapping before setting the new one
Expand All @@ -110,7 +122,7 @@ public TLinkAddress SetName(TLinkAddress link, string name)
/// </summary>
/// <param name="name">The name to look up.</param>
/// <returns>The link address associated with the name, or Null if not found.</returns>
public TLinkAddress GetByName(string name)
public virtual TLinkAddress GetByName(string name)
{
if (_tracingEnabled) Console.WriteLine($"[Trace] GetByName called for name: '{name}'");
var result = NamedLinks.GetExternalReferenceByName(name);
Expand All @@ -122,7 +134,7 @@ public TLinkAddress GetByName(string name)
/// Removes the name association for the specified link address.
/// </summary>
/// <param name="link">The link address whose name should be removed.</param>
public void RemoveName(TLinkAddress link)
public virtual void RemoveName(TLinkAddress link)
{
if (_tracingEnabled) Console.WriteLine($"[Trace] RemoveName called for link: {link}");
NamedLinks.RemoveNameByExternalReference(link);
Expand Down
26 changes: 19 additions & 7 deletions csharp/Foundation.Data.Doublets.Cli.Library/NamedTypesDecorator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

namespace Foundation.Data.Doublets.Cli
{
public sealed class NamedTypesDecorator<TLinkAddress> : LinksDecoratorBase<TLinkAddress>, INamedTypesLinks<TLinkAddress>, IPinnedTypes<TLinkAddress>, IDisposable
public class NamedTypesDecorator<TLinkAddress> : LinksDecoratorBase<TLinkAddress>, INamedTypesLinks<TLinkAddress>, IPinnedTypes<TLinkAddress>, IDisposable
where TLinkAddress : struct,
IUnsignedNumber<TLinkAddress>,
IComparisonOperators<TLinkAddress, TLinkAddress, bool>,
Expand Down Expand Up @@ -73,14 +73,26 @@ public NamedTypesDecorator(string databaseFilename, bool tracingEnabled = false)
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Releases the names database this decorator owns. Derived decorators
/// that own extra resources override this and call
/// <c>base.Dispose(disposing)</c>.
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
_disposed = true;
if (!disposing) return;
LinksFacadeDisposer.Dispose(_namedLinksFacade);
LinksFacadeDisposer.Dispose(PinnedTypesDecorator);
}

public IEnumerator<TLinkAddress> GetEnumerator()
public virtual IEnumerator<TLinkAddress> GetEnumerator()
{
return PinnedTypesDecorator.GetEnumerator();
}
Expand All @@ -90,20 +102,20 @@ IEnumerator IEnumerable.GetEnumerator()
return GetEnumerator();
}

public void Deconstruct(out TLinkAddress type1, out TLinkAddress type2, out TLinkAddress type3)
public virtual void Deconstruct(out TLinkAddress type1, out TLinkAddress type2, out TLinkAddress type3)
{
PinnedTypesDecorator.Deconstruct(out type1, out type2, out type3);
}

public string? GetName(TLinkAddress link)
public virtual string? GetName(TLinkAddress link)
{
if (_tracingEnabled) Console.WriteLine($"[Trace] GetName called for link: {link}");
var result = NamedLinks.GetNameByExternalReference(link);
if (_tracingEnabled) Console.WriteLine($"[Trace] GetName result: {result}");
return result;
}

public TLinkAddress SetName(TLinkAddress link, string name)
public virtual TLinkAddress SetName(TLinkAddress link, string name)
{
if (_tracingEnabled) Console.WriteLine($"[Trace] SetName called for link: {link} with name: '{name}'");
var existingLinkWithName = NamedLinks.GetExternalReferenceByName(name);
Expand All @@ -117,15 +129,15 @@ public TLinkAddress SetName(TLinkAddress link, string name)
return result;
}

public TLinkAddress GetByName(string name)
public virtual TLinkAddress GetByName(string name)
{
if (_tracingEnabled) Console.WriteLine($"[Trace] GetByName called for name: '{name}'");
var result = NamedLinks.GetExternalReferenceByName(name);
if (_tracingEnabled) Console.WriteLine($"[Trace] GetByName result: {result}");
return result;
}

public void RemoveName(TLinkAddress link)
public virtual void RemoveName(TLinkAddress link)
{
if (_tracingEnabled) Console.WriteLine($"[Trace] RemoveName called for link: {link}");
NamedLinks.RemoveNameByExternalReference(link);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ public sealed record PersistentTransformation(
public string Query => $"({Condition} {Substitution})";
}

public sealed class PersistentTransformationDecorator : LinksDecoratorBase<uint>, INamedTypesLinks<uint>
public class PersistentTransformationDecorator : LinksDecoratorBase<uint>, INamedTypesLinks<uint>
{
private const string InternalNamePrefix = "__persistent_transformation:";
/// <summary>
/// The prefix every name this decorator stores carries, so a custom
/// front end can recognise (and skip) the trigger schema.
/// </summary>
public const string InternalNamePrefix = "__persistent_transformation:";

private readonly INamedTypesLinks<uint> _namedLinks;
private readonly INamedTypesLinks<uint> _triggerLinks;
Expand Down Expand Up @@ -55,7 +59,7 @@ public static string MakeTriggersDatabaseFilename(string databaseFilename)
return Path.Combine(directory ?? string.Empty, $"{filenameWithoutExtension}.triggers.links");
}

public uint StoreTrigger(PersistentTransformationKind kind, string query)
public virtual uint StoreTrigger(PersistentTransformationKind kind, string query)
{
var parsed = PersistentTransformationQuery.Parse(query);
return WithoutTriggerApplication(() =>
Expand All @@ -73,7 +77,7 @@ public uint StoreTrigger(PersistentTransformationKind kind, string query)
});
}

public int RemoveTriggers(string query)
public virtual int RemoveTriggers(string query)
{
var parsed = PersistentTransformationQuery.Parse(query);
return WithoutTriggerApplication(() =>
Expand All @@ -91,7 +95,7 @@ public int RemoveTriggers(string query)
});
}

public IReadOnlyList<PersistentTransformation> GetTriggers()
public virtual IReadOnlyList<PersistentTransformation> GetTriggers()
{
if (!TryGetSchema(out var schema))
{
Expand Down Expand Up @@ -155,22 +159,22 @@ public override uint Each(IList<uint>? restriction, ReadHandler<uint>? handler)
return _links.Each(restriction, handler);
}

public string? GetName(uint link)
public virtual string? GetName(uint link)
{
return _namedLinks.GetName(link);
}

public uint SetName(uint link, string name)
public virtual uint SetName(uint link, string name)
{
return _namedLinks.SetName(link, name);
}

public uint GetByName(string name)
public virtual uint GetByName(string name)
{
return _namedLinks.GetByName(name);
}

public void RemoveName(uint link)
public virtual void RemoveName(uint link)
{
_namedLinks.RemoveName(link);
}
Expand Down Expand Up @@ -340,7 +344,10 @@ private void Trace(string message)

private readonly record struct TriggerSchema(uint Type, uint Trigger, uint Once, uint Always, uint Condition, uint Substitution);

private sealed record PersistentTransformationQuery(string Condition, string Substitution)
/// <summary>
/// The two halves a trigger query parses into.
/// </summary>
public sealed record PersistentTransformationQuery(string Condition, string Substitution)
{
public string Query => $"({Condition} {Substitution})";

Expand Down
4 changes: 2 additions & 2 deletions csharp/Foundation.Data.Doublets.Cli.Library/PinnedTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public PinnedTypes(ILinks<TLinkAddress> links)
_links = links;
}

public IEnumerator<TLinkAddress> GetEnumerator()
public virtual IEnumerator<TLinkAddress> GetEnumerator()
{
return new PinnedTypesEnumerator(_links);
}
Expand Down Expand Up @@ -101,7 +101,7 @@ public void Dispose()
}
}

public void Deconstruct(out TLinkAddress type1, out TLinkAddress type2, out TLinkAddress type3)
public virtual void Deconstruct(out TLinkAddress type1, out TLinkAddress type2, out TLinkAddress type3)
{
using var enumerator = GetEnumerator();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public PinnedTypesDecorator(ILinks<TLinkAddress> links) : base(links)
_pinnedTypes = new PinnedTypes<TLinkAddress>(links);
}

public IEnumerator<TLinkAddress> GetEnumerator()
public virtual IEnumerator<TLinkAddress> GetEnumerator()
{
return _pinnedTypes.GetEnumerator();
}
Expand All @@ -32,7 +32,7 @@ IEnumerator IEnumerable.GetEnumerator()
return GetEnumerator();
}

public void Deconstruct(out TLinkAddress type1, out TLinkAddress type2, out TLinkAddress type3)
public virtual void Deconstruct(out TLinkAddress type1, out TLinkAddress type2, out TLinkAddress type3)
{
_pinnedTypes.Deconstruct(out type1, out type2, out type3);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

namespace Foundation.Data.Doublets.Cli
{
public sealed class SimpleLinksDecorator<TLinkAddress> : LinksDecoratorBase<TLinkAddress>, IDisposable
public class SimpleLinksDecorator<TLinkAddress> : LinksDecoratorBase<TLinkAddress>, IDisposable
where TLinkAddress : struct,
IUnsignedNumber<TLinkAddress>,
IComparisonOperators<TLinkAddress, TLinkAddress, bool>,
Expand Down Expand Up @@ -67,9 +67,21 @@ public SimpleLinksDecorator(string databaseFilename, bool tracingEnabled = false
/// Releases the memory-mapped file handles of both the data and the names databases.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Releases the databases this decorator owns. Derived decorators that
/// own extra resources override this and call
/// <c>base.Dispose(disposing)</c>.
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
_disposed = true;
if (!disposing) return;
LinksFacadeDisposer.Dispose(_namedLinksFacade);
LinksFacadeDisposer.Dispose(_links);
}
Expand Down
Loading
Loading