using Foundgine.Core.Abstractions;
using Foundgine.Core.Semantic.Resolution;
using Xunit;
namespace Foundgine.Core.Semantic.Tests;
///
/// Case study for the README / walkthrough headline example — "show me overdue
/// purchase orders from our top supplier in Texas" — proving the alias path a
/// paraphrase of it takes through the real architecture, not just describing it.
///
/// The walkthrough's Step 3 ("Semantic Model") and Step 5 ("Retrieval") describe
/// two different jobs that are easy to conflate:
/// - the semantic contract declares aliases on an entity
/// (), which
/// folds into every
/// it derives from that entity;
/// - a retrieval provider (Elasticsearch, pgvector, or — as here — a fake,
/// in-memory stand-in) indexes that projection and is the thing an
/// actually queries.
///
/// A synonym in a caller's sentence therefore only grounds to the same meaning
/// as the "canonical" word if it survived both hops: declared as an alias on
/// the contract, *and* matched by whatever sits behind the candidate source.
/// This test builds a minimal but real contract (Supplier aliased "Seller",
/// PurchaseOrder aliased "Buys"), projects it with the production
/// , and backs
/// with a source that matches a
/// token against either an entry's canonical name or its
/// — the same contract the
/// canonical name is matched against, so canonical and alias tokens are proven
/// to reach the identical committed interpretation rather than merely two
/// interpretations that happen to look similar.
///
public sealed class SemanticAliasSynonymGroundingTests
{
private static readonly EntityId Supplier = new(1);
private static readonly EntityId PurchaseOrder = new(2);
[Fact]
public void Buys_grounds_to_the_same_interpretation_as_purchase_order()
{
var resolver = BuildResolver();
// "PurchaseOrder" is the real entity name used by
// Foundgine.SupplyChain.Advanced's find_top_supplier_overdue_orders
// capability (see SupplyChainSemanticModel.PurchaseOrder). Grounding
// works token-by-token (see the "customers" pattern used throughout
// SemanticLexicalResolverTests), so a single-word canonical query is
// the faithful unit here; the full multi-word sentence is exercised
// end-to-end in Full_paraphrase_grounds_every_token_the_same_way_as_the_canonical_sentence.
var canonical = resolver.Ground("PurchaseOrder");
var alias = resolver.Ground("buys");
Assert.Equal(GroundingOutcome.Committed, canonical.Outcome);
Assert.Equal(GroundingOutcome.Committed, alias.Outcome);
// Same root entity, same canonical name, same signature: "buys" is not
// a second, coincidentally-similar meaning — it is the alias path to
// the exact interpretation "purchase order" already committed to.
Assert.Equal(PurchaseOrder, canonical.Committed!.RootEntity);
Assert.Equal(PurchaseOrder, alias.Committed!.RootEntity);
Assert.Equal(canonical.Committed.Signature, alias.Committed.Signature);
Assert.Equal("PurchaseOrder", canonical.Committed.Steps[0].Candidate.CanonicalName);
Assert.Equal("PurchaseOrder", alias.Committed.Steps[0].Candidate.CanonicalName);
}
[Fact]
public void Seller_grounds_to_the_same_interpretation_as_supplier()
{
var resolver = BuildResolver();
var canonical = resolver.Ground("supplier");
var alias = resolver.Ground("seller");
Assert.Equal(GroundingOutcome.Committed, canonical.Outcome);
Assert.Equal(GroundingOutcome.Committed, alias.Outcome);
Assert.Equal(Supplier, canonical.Committed!.RootEntity);
Assert.Equal(Supplier, alias.Committed!.RootEntity);
Assert.Equal(canonical.Committed.Signature, alias.Committed.Signature);
Assert.Equal("Supplier", canonical.Committed.Steps[0].Candidate.CanonicalName);
Assert.Equal("Supplier", alias.Committed.Steps[0].Candidate.CanonicalName);
}
[Fact]
public void Full_paraphrase_grounds_every_token_the_same_way_as_the_canonical_sentence()
{
var resolver = BuildResolver();
// README: "show me overdue purchase orders from our top supplier in Texas"
// Paraphrase: "show me overdue buys from our top seller in Texas"
// Only the two content words that carry the entity meaning need to
// ground identically; the rest of each sentence is filler for this
// isolated grounding step (state/rank/overdue are resolved elsewhere
// in the pipeline — see docs/GROUNDING-DECISIONS.md).
foreach (var (canonicalWord, aliasWord, expectedEntity) in new[]
{
("purchase order", "buys", PurchaseOrder),
("supplier", "seller", Supplier)
})
{
var canonical = resolver.Ground(canonicalWord);
var alias = resolver.Ground(aliasWord);
Assert.Equal(GroundingOutcome.Committed, canonical.Outcome);
Assert.Equal(GroundingOutcome.Committed, alias.Outcome);
Assert.Equal(expectedEntity, canonical.Committed!.RootEntity);
Assert.Equal(expectedEntity, alias.Committed!.RootEntity);
Assert.Equal(canonical.Committed.Signature, alias.Committed.Signature);
}
}
private static SemanticLexicalResolver BuildResolver()
{
var contract = new SemanticModelBuilder()
.Entity(Supplier, "Supplier", e => e
.Alias("Seller")
.Identity(new FieldId(1), "Id"))
.Entity(PurchaseOrder, "PurchaseOrder", e => e
.Alias("Buys")
.Identity(new FieldId(2), "Id"))
.Build()
.Freeze()
.CreateSnapshot();
// The production projection is what turns declared aliases into
// searchable lexicon entries — the same projection a real Elasticsearch
// or pgvector-backed ISemanticLexicalCandidateSource indexes from.
var lexicon = SemanticLexiconProjection.Build(contract);
return new SemanticLexicalResolver(contract, new AliasAwareLexicalSource(lexicon));
}
///
/// Stand-in for a real retrieval provider: matches a token against either
/// an entry's canonical name or any of its declared aliases, exactly the
/// lookup an Elasticsearch/pgvector index built from
/// output performs.
///
private sealed class AliasAwareLexicalSource(IReadOnlyList lexicon)
: ISemanticLexicalCandidateSource
{
public IReadOnlyList Retrieve(SemanticLexicalRequest request) =>
lexicon
.Where(entry => request.EffectiveKinds.Contains(entry.Kind))
.Where(entry =>
string.Equals(entry.CanonicalName, request.Token, StringComparison.OrdinalIgnoreCase) ||
entry.EffectiveAliases.Any(alias =>
string.Equals(alias, request.Token, StringComparison.OrdinalIgnoreCase)))
.Select(entry => new SemanticLexicalCandidate(
request.Token,
entry.Kind,
entry.CanonicalName,
Score: .95,
EntityId: entry.EntityId,
RelationshipId: entry.RelationshipId,
FieldId: entry.FieldId,
SourceEntityId: entry.SourceEntityId,
TargetEntityId: entry.TargetEntityId,
Value: entry.Value))
.ToArray();
}
}