一键导入
centralized-filter-normalization
Domain-layer normalizer + JSON-stable cache key pattern for filter types with server-side defaults and case-insensitive fields.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Domain-layer normalizer + JSON-stable cache key pattern for filter types with server-side defaults and case-insensitive fields.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Investigate .NET CI failures using the hlx CLI tool via bash. USE FOR: checking Helix job status, searching build logs, downloading test results, AzDO build timeline analysis — when MCP tools aren't loaded, when the MCP server isn't configured or fails to start, or when scripting with JSON output and jq. DO NOT USE FOR: tasks where Helix/AzDO MCP tools are already available in context (prefer ci-analysis skill when MCP server is loaded). INVOKES: bash (hlx CLI commands with --json output).
{what this skill teaches agents}
Audit pattern for comparing WorkItemSummary (list API) fields against WorkItemDetails (per-item API) after a Helix.Client SDK bump
Strict unknown-parameter rejection for MCP tools: Stage A (UnmappedMemberHandling.Disallow) + Stage B (did-you-mean CallToolFilter).
Audit MCP/CLI tool parameter surface against underlying REST API capabilities to find silently-dropped params.
Test MCP CallToolFilter behavior with real CallToolRequestParams and McpServerTool invocation.
| name | centralized-filter-normalization |
| description | Domain-layer normalizer + JSON-stable cache key pattern for filter types with server-side defaults and case-insensitive fields. |
| domain | azdo-integration |
| confidence | high |
| source | earned |
Use when a filter record (e.g., AzdoBuildFilter) has optional string fields with server-side defaults
and case-insensitive server interpretation. Multiple layers (HTTP client, cache key builder, service
layer) historically re-implement the same canonicalization rules, causing algorithmic drift.
The pattern was extracted from the Issue #82 refactor of AzdoBuildFilter, which fixed four rounds
of reviewer feedback on PR #78 (each round finding the same class of normalization bug at a different
layer).
"Canonicalize at boundaries, share the algorithm."
"Normalize at every layer" (a common first instinct) leads to algorithm duplication and drift.
public static class AzdoBuildFilterDefaults
{
public const string QueryOrder = "queueTimeDescending";
public const string Outcomes = "Failed";
}
{FilterType}Normalizer.cs)public static class AzdoBuildFilterNormalizer
{
public static AzdoBuildFilter Normalize(AzdoBuildFilter filter) =>
filter with
{
PrNumber = NormalizeString(filter.PrNumber),
Branch = NormalizeString(filter.Branch),
StatusFilter = NormalizeString(filter.StatusFilter),
QueryOrder = NormalizeQueryOrder(filter.QueryOrder),
};
private static string? NormalizeString(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
private static string? NormalizeQueryOrder(string? value)
{
var trimmed = NormalizeString(value);
if (trimmed is null) return null;
if (string.Equals(trimmed, AzdoBuildFilterDefaults.QueryOrder, StringComparison.OrdinalIgnoreCase))
return null; // collapse explicit server default to null
return trimmed.ToLowerInvariant(); // lowercase for case-insensitive equivalence
}
}
Rules in order:
IsNullOrWhiteSpace → null (whitespace is equivalent to "not specified")Trim() non-null valuesnull (OrdinalIgnoreCase) — null and the default are semantically identicalwith (records are immutable)Visibility: public — downstream projects (CLI/MCP entry points) may need to call it for validation purposes without duplicating the rules.
private static readonly JsonSerializerOptions s_stableCacheKeyOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers =
{
static typeInfo =>
{
if (typeInfo.Kind != JsonTypeInfoKind.Object) return;
var sorted = typeInfo.Properties
.OrderBy(p => p.Name, StringComparer.Ordinal)
.ToList();
typeInfo.Properties.Clear();
foreach (var p in sorted)
typeInfo.Properties.Add(p);
}
}
}
};
private static string HashFilter(AzdoBuildFilter filter)
{
var normalized = AzdoBuildFilterNormalizer.Normalize(filter);
var json = JsonSerializer.Serialize(normalized, s_stableCacheKeyOptions);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return Convert.ToHexString(hash)[..12].ToLowerInvariant();
}
Why JSON + alphabetical sort?
WhenWritingNull omits null/unset fields — the "empty filter" produces {} which hashes to one stable keyTypeInfoResolver.Modifiers modifier is cached by the resolver; runtime cost is negligible// HTTP client — before URL construction
public async Task<IReadOnlyList<AzdoBuild>> ListBuildsAsync(..., AzdoBuildFilter filter, ...)
{
var f = AzdoBuildFilterNormalizer.Normalize(filter);
// use f.QueryOrder ?? AzdoBuildFilterDefaults.QueryOrder for URL
}
// Cache layer — before key derivation
private static string HashFilter(AzdoBuildFilter filter)
{
var normalized = AzdoBuildFilterNormalizer.Normalize(filter);
// serialize normalized, hash
}
Neither layer re-implements the normalization rules. If a rule changes, it changes in one place.
Changing HashFilter from hand-built strings to JSON changes all cache keys. This is a one-shot invalidation on deployment:
HashFilter that hand-codes each field's quirks — breaks when a new field is addedstring.IsNullOrEmpty instead of IsNullOrWhiteSpace — whitespace produces malformed URLs or distinct cache keysAzdoApiClient.DefaultQueryOrder) referenced by the cache layer — cross-layer couplingApply this pattern when ALL of the following are true:
For filters with only 1-2 simple fields, inline normalization is fine.
AzdoBuildFilter / AzdoBuildFilterDefaults / AzdoBuildFilterNormalizer — src/HelixTool.Core/AzDO/CachingAzdoApiClient.HashFilter — src/HelixTool.Core/AzDO/CachingAzdoApiClient.cs