Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions ARCHITECTURE-1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ Qyl.Collector host process (self-telemetry only)

Qyl.Cli
├── Qyl.Api.Contracts ← generated API client, only path to the collector
├── Qyl.Telemetry.SemanticConventions.Incubating ← observer-emitted qyl vocabulary constants
├── Qyl.Telemetry.Hosting ← own self-telemetry only
└── (zero Qyl.Collector.* references — collector is spawned as a process, reached via API)

Expand Down
6 changes: 3 additions & 3 deletions Version.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup Label="qyl Product Version">
<!-- The NuGet trusted-publishing workflow reads this exact product version. Bump it before
any package-affecting main commit once the current immutable version has been published. -->
<QylVersion>1.1.8</QylVersion>
<QylVersion>1.2.0</QylVersion>
<Version>$(QylVersion)</Version>
</PropertyGroup>

Expand All @@ -25,8 +25,8 @@
<!-- The producer family ships from the Qyl.OpenTelemetry.AutoInstrumentation repo on one
release line (Qyl.Telemetry.* package IDs since 9.0.0), so its packages share one property. -->
<QylTelemetryVersion>9.0.1</QylTelemetryVersion>
<QylSemanticConventionsVersion>1.0.0</QylSemanticConventionsVersion>
<QylApiContractsVersion>7.0.0</QylApiContractsVersion>
<QylSemanticConventionsVersion>4.2.0</QylSemanticConventionsVersion>
<QylApiContractsVersion>7.1.0</QylApiContractsVersion>
<!-- Also read directly by qyl.instrumentation.generators to locate the Sources contentFiles. -->
<ANcpLuaRoslynUtilitiesVersion>2.2.42</ANcpLuaRoslynUtilitiesVersion>
</PropertyGroup>
Expand Down
156 changes: 151 additions & 5 deletions eng/build/BuildCliContractLoop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,147 @@ namespace Qyl.Build;
/// reported rather than assumed innocent, only known collection wrappers are unwrapped to their
/// element (any other generic is itself the wire shape and is judged whole), and finding nothing
/// at all is a failure: a gate whose scope moved out from under it must say so, not report success
/// over an empty set.
/// over an empty set. <c>System.Text.Json.JsonElement</c> is the single serialization intrinsic:
/// generated contracts use <c>object?</c> for an explicitly open JSON value, and source-generated
/// serialization needs metadata for the runtime carrier without making it a CLI-owned wire model.
/// </summary>
interface ICliContractLoop : IHazSourcePaths
{
private const string ContractNamespacePrefix = "Qyl.Api.Contracts.";
private const string OpenJsonValueCarrier = "System.Text.Json.JsonElement";

Target VerifyCliMcpToolSchemasAreGenerated => d => d
.Unlisted()
.Description("Verify Qyl.Cli MCP tools publish generated input and output schemas")
.Executes(() =>
{
var repoRoot = NukeBuild.RootDirectory;
var cliDirectory = repoRoot / "packages" / "Qyl.Cli";
var roots = cliDirectory.GlobFiles("**/*.cs")
.Where(static file => !file.ToString().Contains("/obj/", StringComparison.Ordinal)
&& !file.ToString().Contains("/bin/", StringComparison.Ordinal)
&& !file.Name.EndsWith(".g.cs", StringComparison.Ordinal)
&& !file.Name.EndsWith(".Designer.cs", StringComparison.Ordinal))
Comment thread
ANcpLua marked this conversation as resolved.
.OrderBy(static file => file.ToString(), StringComparer.Ordinal)
.Select(file => (
File: file,
Root: CSharpSyntaxTree.ParseText(File.ReadAllText(file), path: file.ToString())
.GetCompilationUnitRoot()))
.ToList();

var generatedSchemaFields = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var (_, root) in roots)
{
foreach (var variable in root.DescendantNodes().OfType<VariableDeclaratorSyntax>())
{
if (variable.Initializer?.Value is not InvocationExpressionSyntax
{
Expression: IdentifierNameSyntax { Identifier.ValueText: "ParseSchema" },
ArgumentList.Arguments.Count: 1,
} parse ||
parse.ArgumentList.Arguments[0].Expression is not MemberAccessExpressionSyntax artifact ||
!artifact.Expression.ToString().EndsWith("ToolSchemas", StringComparison.Ordinal))
{
continue;
}

generatedSchemaFields[variable.Identifier.ValueText] = artifact.Name.Identifier.ValueText;
}
}

var offenders = new List<string>();
var usedArtifacts = new HashSet<string>(StringComparer.Ordinal);
var schemaPropertyCount = 0;
foreach (var (file, root) in roots)
{
var relative = repoRoot.GetRelativePathTo(file).ToString().Replace('\\', '/');
foreach (var literal in root.DescendantNodes().OfType<LiteralExpressionSyntax>())
{
var text = literal.Token.ValueText;
if (!text.Contains("\"inputSchema\"", StringComparison.Ordinal) &&
!text.Contains("\"outputSchema\"", StringComparison.Ordinal))
{
continue;
}

var line = literal.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
offenders.Add(
$"{relative}:{line}: raw JSON declares an inputSchema/outputSchema; " +
"write a generated ToolSchemas artifact instead");
}
Comment thread
ANcpLua marked this conversation as resolved.

foreach (var invocation in root.DescendantNodes().OfType<InvocationExpressionSyntax>())
{
if (invocation.Expression is not MemberAccessExpressionSyntax propertyWrite ||
propertyWrite.Name.Identifier.ValueText is not "WritePropertyName" ||
invocation.ArgumentList.Arguments.Count is not 1 ||
invocation.ArgumentList.Arguments[0].Expression is not LiteralExpressionSyntax literal ||
literal.Token.ValueText is not ("inputSchema" or "outputSchema"))
{
continue;
}

schemaPropertyCount++;
var line = invocation.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
if (invocation.Parent is not ExpressionStatementSyntax statement ||
statement.Parent is not BlockSyntax block)
{
offenders.Add($"{relative}:{line}: {literal.Token.ValueText} is not a standalone generated-schema write");
continue;
}

var statementIndex = block.Statements.IndexOf(statement);
if (statementIndex < 0 || statementIndex + 1 >= block.Statements.Count ||
block.Statements[statementIndex + 1] is not ExpressionStatementSyntax
{
Expression: InvocationExpressionSyntax
{
Expression: MemberAccessExpressionSyntax writeTo,
ArgumentList.Arguments.Count: 1,
} generatedWrite,
} ||
writeTo.Name.Identifier.ValueText is not "WriteTo" ||
writeTo.Expression is not IdentifierNameSyntax schemaField ||
generatedWrite.ArgumentList.Arguments[0].Expression.ToString() != propertyWrite.Expression.ToString() ||
!generatedSchemaFields.TryGetValue(schemaField.Identifier.ValueText, out var artifactName))
{
offenders.Add(
$"{relative}:{line}: {literal.Token.ValueText} must be written directly from a " +
"JsonElement parsed from Qyl.Api.Contracts.Mcp.ToolSchemas");
continue;
}

usedArtifacts.Add(artifactName);
}
}

string[] requiredArtifacts =
[
"GetActiveWorkflowRunInput",
"GetActiveWorkflowRunOutput",
"RecordDiagnosticSnapshotInput",
"RecordDiagnosticSnapshotOutput",
];
foreach (var artifact in requiredArtifacts.Where(artifact => !usedArtifacts.Contains(artifact)))
offenders.Add($"generated ToolSchemas.{artifact} is not published by the Qyl.Cli MCP bridge");

if (schemaPropertyCount is 0)
offenders.Add("no MCP inputSchema/outputSchema properties were found under packages/Qyl.Cli");

if (offenders.Count > 0)
{
throw new InvalidOperationException(
"Qyl.Cli publishes a hand-maintained or missing MCP tool schema:" + Environment.NewLine +
string.Join(Environment.NewLine, offenders) + Environment.NewLine +
"Define named tool input/output models in qyl-api-schema, regenerate Qyl.Api.Contracts, " +
"and write the matching Qyl.Api.Contracts.Mcp.ToolSchemas artifact verbatim.");
}

Log.Information(
"Qyl.Cli publishes {SchemaProperties} MCP input/output schemas from {Artifacts} generated artifacts",
schemaPropertyCount,
usedArtifacts.Count);
});

/// <summary>
/// Generic wrappers that serialize their single element rather than themselves. Anything not
Expand Down Expand Up @@ -98,6 +234,7 @@ interface ICliContractLoop : IHazSourcePaths

var offenders = new List<string>();
var registeredCount = 0;
var intrinsicRegisteredCount = 0;
var localRegisteredCount = 0;

foreach (var (file, root) in roots)
Expand Down Expand Up @@ -149,6 +286,11 @@ interface ICliContractLoop : IHazSourcePaths
var resolved = Resolve(written, aliases);
if (resolved.StartsWith(ContractNamespacePrefix, StringComparison.Ordinal))
continue;
if (resolved == OpenJsonValueCarrier)
{
intrinsicRegisteredCount++;
continue;
}

var line = attribute.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
offenders.Add(resolved == written
Expand Down Expand Up @@ -192,7 +334,9 @@ interface ICliContractLoop : IHazSourcePaths
"The CLI is a client of the collector API and owns none of it. Map the value to a " +
"Qyl.Api.Contracts type first — QylRunnerContractMapper is where that happens — and " +
"register the contract type. If the type IS a contract type, name it so the verifier " +
"can prove it: a `using Contract... = Qyl.Api.Contracts....;` alias or a fully-qualified name.");
"can prove it: a `using Contract... = Qyl.Api.Contracts....;` alias or a fully-qualified name. " +
"JsonElement is allowed only as the runtime carrier for an open JSON member already owned " +
"by a generated contract.");
}

if (contextNames.Count is 0 || registeredCount is 0)
Expand All @@ -203,9 +347,11 @@ interface ICliContractLoop : IHazSourcePaths
}

Log.Information(
"Qyl.Cli collector boundaries serialize contract types only: {Registered} contract and " +
"{LocalRegistered} local-state registrations across {Contexts} JSON context(s)",
registeredCount,
"Qyl.Cli collector boundaries serialize contract types only: {ContractRegistered} contract, " +
"{IntrinsicRegistered} open-JSON intrinsic, and {LocalRegistered} local-state registrations " +
"across {Contexts} JSON context(s)",
registeredCount - intrinsicRegisteredCount,
intrinsicRegisteredCount,
localRegisteredCount,
contextNames.Count);

Expand Down
6 changes: 4 additions & 2 deletions eng/build/BuildDependencyEdges.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ interface IDependencyEdges : IHazSourcePaths
"Qyl.Telemetry.Hosting", "Qyl.Api.Contracts",
"Qyl.Telemetry.SemanticConventions", "Qyl.Telemetry.SemanticConventions.Incubating",
],
// G11: the CLI is a client of the collector API — generated contracts only.
["packages/Qyl.Cli/Qyl.Cli.csproj"] = ["Qyl.Api.Contracts"],
// G11: the CLI is a client of the collector API. Its observer also emits qyl-owned
// telemetry using the published incubating vocabulary rather than local string copies.
["packages/Qyl.Cli/Qyl.Cli.csproj"] =
["Qyl.Api.Contracts", "Qyl.Telemetry.SemanticConventions.Incubating"],
["packages/Qyl.Run.Workload/Qyl.Run.Workload.csproj"] =
["Qyl.Telemetry.SemanticConventions.SourceGeneration"],
// Collector product function: generated contracts it serves. The producer stack
Expand Down
4 changes: 4 additions & 0 deletions eng/build/BuildVerify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2562,6 +2562,7 @@
// VerifyBackend, not Verify, so a gate hung only on Verify never executes in CI.
.DependsOn<IDependencyEdges>(static x => x.VerifyDependencyEdges)
.DependsOn<ICliContractLoop>(static x => x.VerifyCliSerializesContractsOnly)
.DependsOn<ICliContractLoop>(static x => x.VerifyCliMcpToolSchemasAreGenerated)
// Same reasoning, applied to the gate that polices this exact asymmetry: hung only on Ci,
// the CI-coverage check would never run in the CI it describes.
.DependsOn<ICiCoverage>(static x => x.VerifyCiTargetCoversWorkflow);
Expand All @@ -2571,6 +2572,7 @@
.DependsOn(VerifyBackend)
.DependsOn<IDependencyEdges>(static x => x.VerifyDependencyEdges)
.DependsOn<ICliContractLoop>(static x => x.VerifyCliSerializesContractsOnly)
.DependsOn<ICliContractLoop>(static x => x.VerifyCliMcpToolSchemasAreGenerated)
.DependsOn(VerifyFrontendApiTypes)
.DependsOn(VerifyFrontendTypes)
.Executes(() =>
Expand Down Expand Up @@ -2612,6 +2614,8 @@
Log.Information(" Collector log storage writes are replay-idempotent");
Log.Information(" README configuration matches every QYL_* code binding");
Log.Information(" Removed local build surfaces stayed removed");
Log.Information(" Qyl.Cli serialization roots are generated API contracts");
Log.Information(" Qyl.Cli MCP tool schemas are generated contract artifacts");
Log.Information("═══════════════════════════════════════════════════════════════");
});

Expand Down Expand Up @@ -3333,7 +3337,7 @@

private static string NormalizeSqlFragment(string sql)
{
var normalized = string.Join(

Check warning on line 3340 in eng/build/BuildVerify.cs

View workflow job for this annotation

GitHub Actions / Backend (.NET)

In method 'NormalizeSqlFragment', replace the call to 'ToLowerInvariant' with 'ToUpperInvariant' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1308)
' ',
sql.Replace("\"", "", StringComparison.Ordinal)
.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries))
Expand Down
16 changes: 15 additions & 1 deletion eng/config/collector-semantic-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@
},
"developmentAttributeAllowList": {
"span": [
"qyl.agent.diagnostic.check.count",
"qyl.agent.diagnostic.check.failed_count",
"qyl.agent.diagnostic.extension.id",
"qyl.agent.diagnostic.format.version",
"qyl.agent.diagnostic.outcome",
"qyl.agent.diagnostic.phase",
"qyl.agent.diagnostic.probe.id",
"qyl.agent.diagnostic.snapshot.id",
"qyl.agent.diagnostic.variable.count",
"qyl.exception.source",
"qyl.instrumentation.domain",
"qyl.mcp.evaluation_run.id",
Expand All @@ -54,7 +63,12 @@
"qyl.mcp.sdk.tier",
"qyl.mcp.server.id",
"qyl.mcp.test_case.id",
"qyl.mcp.tool.name"
"qyl.mcp.tool.name",
"qyl.workflow.agent.id",
"qyl.workflow.attempt.id",
"qyl.workflow.event.id",
"qyl.workflow.run.id",
"qyl.workflow.tool_call.id"
],
"log": [
"browser.device_memory",
Expand Down
3 changes: 3 additions & 0 deletions packages/Qyl.Cli/Codex/ActiveWorkflowRunStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ internal sealed class ActiveWorkflowRunStore
public ActiveWorkflowRunStore(string root)
{
Directory.CreateDirectory(root);
Root = root;
_activePath = Path.Combine(root, ActiveFileName);
_lockPath = Path.Combine(root, LockFileName);
}

public string Root { get; }

public FileStream Acquire()
{
try
Expand Down
Loading
Loading