Brokkr/surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrCommandLedger.cs
Meta Vens d498ee37ac Add BrokkrCommandLedger, the pending-intent rule, with its test project
The rule that decides which Unity command intents still await execution
(an id is answered by any receipt for it; oldest stored-at then key first)
was inline in the mirror and untestable outside Unity. It is now a
stateless ledger over a CultCache in Runtime/, compiled by
brokkr-contracts and pinned by brokkr-contracts.Tests, which also carries
the Imagination probe as an editor-without-Unity scenario harness.

The host snapshot gains agentCommandsEnabled (Key 18; 14-17 are held for
the play-state fields). Nothing in Unity uses the ledger yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:03:41 +02:00

52 lines
2.3 KiB
C#

#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GameCult.Caching;
namespace GameCult.Brokkr
{
// Decides which Unity command intents still await execution. A command id is single-use: any receipt that
// answers an id retires it for good, whatever is later written under that id. The ledger holds no state of its
// own; the answer is derived from the cache on every call, so there is no second copy that could drift.
public sealed class BrokkrCommandLedger
{
public const string CommandKeyPrefix = "unity/commands/";
public const string ReceiptKeyPrefix = "unity/receipts/";
private readonly CultCache cache;
public BrokkrCommandLedger(CultCache cache)
{
this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
}
public static CultRecordKey CommandKey(string commandId) => new CultRecordKey(CommandKeyPrefix + commandId);
public static CultRecordKey ReceiptKey(string commandId) => new CultRecordKey(ReceiptKeyPrefix + commandId);
// Loads writes that other processes have landed in the backing store.
public Task PullAsync() => cache.PullAllBackingStoresAsync();
// The oldest unanswered intent: stored-at order, then key order.
public bool TryNextPending(out BrokkrUnityCommand command)
{
var stored = cache.AllStoredDocuments.ToArray();
var answered = new HashSet<string>(
stored.Select(entry => entry.Document).OfType<BrokkrUnityCommandReceipt>()
.Select(receipt => receipt.commandId)
.Where(id => !string.IsNullOrWhiteSpace(id)),
StringComparer.Ordinal);
command = stored
.Where(entry => entry.Document is BrokkrUnityCommand)
.OrderBy(entry => entry.StoredAt, StringComparer.Ordinal)
.ThenBy(entry => entry.Key.Value, StringComparer.Ordinal)
.Select(entry => (BrokkrUnityCommand)entry.Document)
.FirstOrDefault(candidate => !string.IsNullOrWhiteSpace(candidate.commandId)
&& !answered.Contains(candidate.commandId));
return command != null;
}
}
}