Skip to content

Commit 468ff0c

Browse files
KSemenenkoclaude
andcommitted
Align query window, result-bytes and guide/graph oracles with the current server
- An offset/limit pair that cannot form a row window is a Validation refusal, not BudgetExceeded; projection, ordering and parameter maxima stay budgets. - Resource-lifetime result retention reports the same result-bytes message as the serialization counter. - Guide lifecycle oracles expect the MCP server-info _meta block. - Graph path oracles expect canonical key-sorted edge attributes. Verification: integration and unit test projects build with -warnaserror, dotnet format verify on changed files, repository governance PASS; locally GuideLifecycle, GraphPath R43, OneByteWholeEventRefusal and the unit SqlOffsetWholeFlow cases pass. AcSqlc006 still fails after a cold restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4207a08 commit 468ff0c

11 files changed

Lines changed: 41 additions & 18 deletions

File tree

‎src/KeyLoad.Core/ResourceExecution/Lifecycle/ResourceLifetimeWorkResultRetention.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ namespace KeyLoad.Core.ResourceExecution;
66
internal sealed class ResourceLifetimeWorkResultRetention : IDisposable
77
{
88
private const int Empty = 0;
9-
private const string Capacity = "The retained resource lifetimes exceed the operation result bytes.";
9+
// Retained lifetimes are part of the caller's result bytes, so the caller sees the same refusal.
10+
private const string Capacity = ResultByteCounterStream.ResultBytesExceeded;
1011
private const int Unbounded = int.MaxValue;
1112
private const string Invalid = "The resource lifetime ledger input is invalid.";
1213
private readonly Lock gate = new();

‎src/KeyLoad.Core/ResourceExecution/Serialization/ResultByteCounterStream.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace KeyLoad.Core.ResourceExecution;
22

33
internal sealed class ResultByteCounterStream(long maximumBytes, Action check) : Stream
44
{
5-
private const string ResultBytesExceeded = "The read result byte budget is exceeded.";
5+
internal const string ResultBytesExceeded = "The read result byte budget is exceeded.";
66
private long bytes;
77

88
internal static long Measure<T>(ReadExecutionBudget budget, T result)

‎src/KeyLoad.Query/QueryExecution/Validation/QuerySemantics/QueryValidation.cs‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal static class QueryValidation
1212
private const int MinimumPositiveCount = 1;
1313
private const string QueryStructureExceedsItsBudgetDetail = "The query structure exceeds its budget.";
1414
private const int RootPredicateDepth = 1;
15-
private const string QueryWindowIsNotRepresentableDetail = "The query offset and limit exceed the representable row positions.";
15+
private const string QueryWindowIsNotRepresentableDetail = "The query offset and limit do not form a valid row window.";
1616
private const string ModelQuerySourceIsUnsupportedDetail = "The model query source is unsupported.";
1717
private const string ModelQueryRequiresExplicitFullScanConsentDetail = "A model query requires explicit full-scan consent.";
1818
private const string ModelQueryContinuationIsNotSupportedDetail = "Model query continuation is not supported.";
@@ -42,13 +42,14 @@ public static AstQueryRequest Normalize(AstQueryRequest request, QueryExecutionO
4242
ValidateModelSource(request, query);
4343
if (query.Projection.IsDefault || query.Order.IsDefault
4444
|| query.Projection.Length < MinimumPositiveCount || query.Projection.Length > execution.MaximumProjection
45-
|| query.Order.Length > execution.MaximumOrdering || query.Limit < MinimumPositiveCount
46-
|| query.Offset < EmptyElementCount
45+
|| query.Order.Length > execution.MaximumOrdering
4746
|| request.Parameters?.Count > execution.MaximumParameters)
4847
{
4948
throw Errors.Fail(ErrorCode.BudgetExceeded, QueryStructureExceedsItsBudgetDetail);
5049
}
51-
if ((long)query.Offset + query.Limit > int.MaxValue)
50+
// A window needs a positive limit and a nonnegative offset that ends within a 32-bit row position.
51+
if (query.Limit < MinimumPositiveCount || query.Offset < EmptyElementCount
52+
|| (long)query.Offset + query.Limit > int.MaxValue)
5253
{
5354
throw Errors.Fail(ErrorCode.Validation, QueryWindowIsNotRepresentableDetail);
5455
}

‎tests/KeyLoad.IntegrationTests/ClientApi/Assertions/GuideLifecycle/GuideLifecyclePublicExpected.cs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Text.Json.Nodes;
2+
13
namespace KeyLoad.IntegrationTests.ClientApi;
24

35
internal static class GuideLifecyclePublicExpected
@@ -9,6 +11,11 @@ internal static class GuideLifecyclePublicExpected
911
internal const string Description = "How to discover and safely use KeyLoad operations.";
1012
internal const int MaximumBytes = 8192;
1113
internal const string CompleteResult = "complete";
14+
private const string ServerInfoKey = "io.modelcontextprotocol/serverInfo";
15+
private const string ServerName = "KeyLoad";
16+
private const string ServerVersion = "1.0.0";
17+
private const string NameKey = "name";
18+
private const string VersionKey = "version";
1219
internal const string Markdown = """
1320
# KeyLoad agent quickstart
1421
@@ -36,4 +43,10 @@ internal static class GuideLifecyclePublicExpected
3643
3744
This guide is static documentation, not a data browser or authorization grant. Confirm current tool schemas and feature support for each task.
3845
""";
46+
47+
/// <summary>MCP results carry the server identity in their metadata; each result owns its own node.</summary>
48+
internal static JsonObject ServerMeta() => new()
49+
{
50+
[ServerInfoKey] = new JsonObject { [NameKey] = ServerName, [VersionKey] = ServerVersion }
51+
};
3952
}

‎tests/KeyLoad.IntegrationTests/ClientApi/Assertions/GuideLifecycle/GuideLifecyclePublicGuidance.cs‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ private static async Task ResourcesAsync(McpOfficialClient session, Cancellation
3232
NextCursor = null,
3333
TimeToLive = TimeSpan.Zero,
3434
CacheScope = CacheScope.Private,
35-
ResultType = GuideLifecyclePublicExpected.CompleteResult
35+
ResultType = GuideLifecyclePublicExpected.CompleteResult,
36+
Meta = GuideLifecyclePublicExpected.ServerMeta()
3637
};
3738
var actual = await session.Client.ListResourcesAsync(new ListResourcesRequestParams(), token);
3839
await SqlClusterProtocol.EqualAsync(expected, actual);
@@ -49,7 +50,8 @@ await SqlClusterProtocol.EqualAsync(expected,
4950
}],
5051
TimeToLive = TimeSpan.Zero,
5152
CacheScope = CacheScope.Private,
52-
ResultType = GuideLifecyclePublicExpected.CompleteResult
53+
ResultType = GuideLifecyclePublicExpected.CompleteResult,
54+
Meta = GuideLifecyclePublicExpected.ServerMeta()
5355
};
5456
var read = await session.Client.ReadResourceAsync(Resource, cancellationToken: token);
5557
await SqlClusterProtocol.EqualAsync(expectedRead, read);
@@ -73,7 +75,8 @@ private static async Task PromptsAsync(McpOfficialClient session, CancellationTo
7375
NextCursor = null,
7476
TimeToLive = TimeSpan.Zero,
7577
CacheScope = CacheScope.Private,
76-
ResultType = GuideLifecyclePublicExpected.CompleteResult
78+
ResultType = GuideLifecyclePublicExpected.CompleteResult,
79+
Meta = GuideLifecyclePublicExpected.ServerMeta()
7780
};
7881
var actual = await session.Client.ListPromptsAsync(new ListPromptsRequestParams(), token);
7982
await SqlClusterProtocol.EqualAsync(expected, actual);
@@ -88,7 +91,8 @@ await SqlClusterProtocol.EqualAsync(expected,
8891
Role = Role.User,
8992
Content = new TextContentBlock { Text = GuideLifecyclePublicExpected.Markdown }
9093
}],
91-
ResultType = GuideLifecyclePublicExpected.CompleteResult
94+
ResultType = GuideLifecyclePublicExpected.CompleteResult,
95+
Meta = GuideLifecyclePublicExpected.ServerMeta()
9296
};
9397
var get = await session.Client.GetPromptAsync(GuideLifecyclePublicExpected.PromptName, cancellationToken: token);
9498
await SqlClusterProtocol.EqualAsync(expectedGet, get);

‎tests/KeyLoad.IntegrationTests/GraphTraversal/Assertions/Paths/GraphPath/GraphPathFieldUseR43Current.cs‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ namespace KeyLoad.IntegrationTests.GraphTraversal;
66

77
internal static class GraphPathFieldUseR43Current
88
{
9-
private const string CanonicalFullAttributes = "{\"public\":\"visible\",\"secret\":\"graph-path-private-marker\"}";
10-
119
internal static async Task ObserveAsync(RequestCqrsClusterCallers admin, GraphPathFieldUseR43State state,
1210
CancellationToken token)
1311
{
@@ -20,7 +18,7 @@ await SqlClusterProtocol.EqualAsync(expected, (await PublicReadAssertions.Docume
2018
}
2119
var request = new TraverseRequest(state.Seed.Partition, GraphPathClusterScenario.Graph, state.Source, 2, 3, 2);
2220
var expectedGraph = state.Seed.StoredTraversal(new([state.Middle, state.Source, state.Target],
23-
[.. state.Edges.Select(edge => edge with { AttributesJson = CanonicalFullAttributes })]));
21+
[.. state.Edges.Select(edge => edge with { AttributesJson = GraphPathClusterScenario.CanonicalAttributes })]));
2422
await SqlClusterProtocol.EqualAsync(expectedGraph, await McpCallerAssertions.SdkSuccessAsync(
2523
await admin.Sdk.TraverseAsync(request, token)));
2624
await SqlClusterProtocol.EqualAsync(expectedGraph, (await McpCallerAssertions.SuccessAsync<global::KeyLoad.GraphTraversal>(

‎tests/KeyLoad.IntegrationTests/GraphTraversal/Assertions/Paths/GraphPath/GraphPathRawReadR43Current.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ await SqlClusterProtocol.EqualAsync(expected, (await PublicReadAssertions.Docume
1717
await admin.Mcp.CallAsync(McpCallerTools.DocumentsGet, new GetDocumentRequest(vertex), token))).Value);
1818
}
1919
var request = new TraverseRequest(state.Seed.Partition, GraphPathClusterScenario.Graph, state.Source, 2, 3, 2);
20-
var expectedGraph = state.Seed.StoredTraversal(new([state.Middle, state.Source, state.Target], [.. state.Edges]));
20+
var expectedGraph = state.Seed.StoredTraversal(new([state.Middle, state.Source, state.Target],
21+
[.. state.Edges.Select(edge => edge with { AttributesJson = GraphPathClusterScenario.CanonicalAttributes })]));
2122
await SqlClusterProtocol.EqualAsync(expectedGraph, await McpCallerAssertions.SdkSuccessAsync(
2223
await admin.Sdk.TraverseAsync(request, token)));
2324
await SqlClusterProtocol.EqualAsync(expectedGraph, (await McpCallerAssertions.SuccessAsync<global::KeyLoad.GraphTraversal>(

‎tests/KeyLoad.IntegrationTests/GraphTraversal/Models/GraphPathRawReadR43State.cs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ internal bool Allowed
2626
new("r43-raw-0", Source, Middle, GraphPathClusterScenario.Label, "{\"public\":\"visible\"}", 1L),
2727
new("r43-raw-1", Middle, Target, GraphPathClusterScenario.Label, "{\"public\":\"visible\"}", 1L)
2828
];
29-
internal EdgeRecord[] ProjectedEdges => Allowed ? Edges : RedactedEdges;
29+
internal EdgeRecord[] ProjectedEdges => Allowed
30+
? [.. Edges.Select(edge => edge with { AttributesJson = GraphPathClusterScenario.CanonicalAttributes })]
31+
: RedactedEdges;
3032
internal EdgeRecord[] StoredProjectedEdges => [.. ProjectedEdges.Select(Seed.StoredEdge)];
3133
internal CommandRequest Command { get; set; } = null!;
3234
internal CommitReceipt Receipt { get; set; } = null!;

‎tests/KeyLoad.IntegrationTests/GraphTraversal/Scenarios/Paths/GraphPathClusterScenario.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ internal static class GraphPathClusterScenario
2929
internal const string IsolatedId = "isolated";
3030
internal const string SecretOwner = "different-row-owner";
3131
internal const string PublicAttributes = "{\"secret\":\"graph-path-private-marker\",\"public\":\"visible\"}";
32+
/// <summary>Stored edge attributes are canonical JSON, so a full read returns them with sorted keys.</summary>
33+
internal const string CanonicalAttributes = "{\"public\":\"visible\",\"secret\":\"graph-path-private-marker\"}";
3234
internal const string EmptyAttributes = "{}";
3335

3436
internal static async Task<GraphPathClusterSeed> CreateAsync(ClusterFixture fixture,

‎tests/KeyLoad.IntegrationTests/QueryExecution/Assertions/Pagination/SqlOffsetClusterRefusals.cs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ await McpCallerAssertions.ErrorAsync(await callers.Mcp.CallAsync(McpCallerTools.
4343
Offset = offset
4444
}
4545
};
46+
// A negative offset or an offset whose window passes the 32-bit row position is invalid input, as in SQL.
4647
var failure = await callers.Sdk.QueryAstAsync(bad, token);
4748
await Assert.That(failure.IsSuccess).IsFalse();
48-
await Assert.That(failure.Problem?.ErrorCode).IsEqualTo(ErrorCode.BudgetExceeded.ToString());
49+
await Assert.That(failure.Problem?.ErrorCode).IsEqualTo(ErrorCode.Validation.ToString());
4950
await McpCallerAssertions.ErrorAsync(await callers.Mcp.CallAsync(McpCallerTools.QueryAst, bad, token),
50-
ErrorCode.BudgetExceeded, dispatched: true);
51+
ErrorCode.Validation, dispatched: true);
5152
}
5253
}
5354
internal static async Task DeniedAsync(RequestCqrsClusterCallers callers, SqlOffsetClusterState state, CancellationToken token)

0 commit comments

Comments
 (0)