Files
lserp_cs_6.0/插件库/Lskj.AgentBridge/WorkflowWriteCaseObservation.cs
T
2026-08-14 14:28:28 +08:00

1547 lines
69 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Lskj.CommandKernel;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Lskj.AgentBridge
{
/// <summary>
/// 把本地保存的 CLI 计划/执行响应投影成不含业务原值的单用例验收记录。
/// 原始响应仅作为输入读取;输出只保留稳定码、布尔/计数和不可逆 SHA-256。
/// </summary>
public static class WorkflowWriteCaseObservationProjector
{
private const int MaximumInputBytes = 4 * 1024 * 1024;
private const int MaximumOutputBytes = 128 * 1024;
private static readonly Regex SafeCaseCode = new Regex(
"^[a-z0-9_.-]{1,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeCommandName = new Regex(
"^[A-Za-z0-9_.:-]{1,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeCommandVersion = new Regex(
"^[0-9]{1,4}\\.[0-9]{1,4}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeRuntimeCliVersion = new Regex(
"^[0-9]{1,4}\\.[0-9]{1,4}\\.[0-9]{1,4}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SignerThumbprintPattern = new Regex(
"^[A-F0-9]{40}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeModuleCode = new Regex(
"^[A-Za-z0-9_.:-]{1,64}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeCorrelationId = new Regex(
"^[A-Za-z0-9_.:-]{8,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeCode = new Regex(
"^[a-z0-9_.-]{1,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafePlanId = new Regex(
"^[A-Fa-f0-9]{32}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeIdempotencyKey = new Regex(
"^[A-Za-z0-9_.:-]{8,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex Sha256Pattern = new Regex(
"^[a-f0-9]{64}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeJsonFileName = new Regex(
"^[A-Za-z0-9][A-Za-z0-9_.-]{0,175}\\.json$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex MultiDayLeaveExpression = new Regex(
@"^(?:从)?(?<start>\d{4}-\d{1,2}-\d{1,2})(?:上午|下午|全天|全日|一天)(?:到|至)(?<end>\d{4}-\d{1,2}-\d{1,2})(?:上午|下午|全天|全日|一天)$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex LeaveResolutionProof = new Regex(
@"^lrp1\.[0-9]{1,19}\.[a-f0-9]{32}\.[a-f0-9]{64}\.[A-Za-z0-9_-]{40,64}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex PurchaseResolutionProof = new Regex(
@"^rp1\.[0-9]{1,19}\.[a-f0-9]{32}\.[a-f0-9]{64}\.[A-Za-z0-9_-]{40,64}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
public static JObject ProjectFile(string inputPath, DateTime nowUtc)
{
return Project(LoadStrict(inputPath), nowUtc);
}
public static JObject VerifyProjectedCaseFile(
string inputPath,
DateTime nowUtc)
{
JObject projected = LoadStrict(inputPath);
WorkflowWriteIntegrationEvidenceVerifier.ValidateProjectedCase(
projected,
nowUtc);
return projected;
}
public static JObject ProjectReferencedFiles(
string indexPath,
DateTime nowUtc)
{
JObject index = LoadStrict(indexPath);
EnsureExact(
index,
"schemaVersion", "caseCode", "commandName", "commandInputFile",
"uatAuthorizationSourceSha256", "uatAuthorizationContentSha256",
"uatAuthorizationIdSha256", "uatAuthorizationIssuedAtUtc",
"uatAuthorizationExpiresAtUtc", "uatExecutionCaseCode",
"uatTokenSha256", "runtimeCliVersion", "runtimeCliSha256",
"runtimeCliSignerThumbprint",
"contextCliResponseFile", "planCliResponseFile",
"executeCliResponseFile", "idempotencyKey",
"businessMutationCount", "nativeConfirmationObserved",
"auditEventCount", "sourceDocumentWritePayloadBound",
"sourceDocumentAuditCount", "observedAtUtc");
if (RequiredString(index, "schemaVersion", 1, 16) != "1.3")
throw Error(
"write_case_file_index_schema_invalid",
"写用例文件索引版本无效。");
string fullIndex = Path.GetFullPath(indexPath);
string directory = Path.GetDirectoryName(fullIndex);
string commandInputFile = RequiredSiblingJsonFileName(
index,
"commandInputFile");
string contextFile = RequiredSiblingJsonFileName(
index,
"contextCliResponseFile");
string planFile = NullableSiblingJsonFileName(
index,
"planCliResponseFile");
string executeFile = NullableSiblingJsonFileName(
index,
"executeCliResponseFile");
if (planFile == null && executeFile == null)
throw Error(
"write_case_observation_response_required",
"写用例文件索引至少需要一份 CLI 响应。");
HashSet<string> references = new HashSet<string>(
StringComparer.OrdinalIgnoreCase);
foreach (string fileName in new[]
{
commandInputFile,
contextFile,
planFile,
executeFile
})
{
if (fileName != null && !references.Add(fileName))
throw Error(
"write_case_file_index_path_invalid",
"写用例文件索引不能重复引用同一文件。");
}
JObject source = new JObject
{
{ "schemaVersion", "1.3" },
{ "caseCode", index["caseCode"].DeepClone() },
{ "uatAuthorizationSourceSha256", index[
"uatAuthorizationSourceSha256"].DeepClone() },
{ "uatAuthorizationContentSha256", index[
"uatAuthorizationContentSha256"].DeepClone() },
{ "uatAuthorizationIdSha256", index[
"uatAuthorizationIdSha256"].DeepClone() },
{ "uatAuthorizationIssuedAtUtc", index[
"uatAuthorizationIssuedAtUtc"].DeepClone() },
{ "uatAuthorizationExpiresAtUtc", index[
"uatAuthorizationExpiresAtUtc"].DeepClone() },
{ "uatExecutionCaseCode", index[
"uatExecutionCaseCode"].DeepClone() },
{ "uatTokenSha256", index["uatTokenSha256"].DeepClone() },
{ "runtimeCliVersion", index[
"runtimeCliVersion"].DeepClone() },
{ "runtimeCliSha256", index[
"runtimeCliSha256"].DeepClone() },
{ "runtimeCliSignerThumbprint", index[
"runtimeCliSignerThumbprint"].DeepClone() },
{ "commandName", index["commandName"].DeepClone() },
{ "commandInput", LoadStrict(Path.Combine(
directory,
commandInputFile)) },
{ "contextCliResponse", LoadStrict(Path.Combine(
directory,
contextFile)) },
{ "planCliResponse", planFile == null
? JValue.CreateNull()
: (JToken)LoadStrict(Path.Combine(directory, planFile)) },
{ "executeCliResponse", executeFile == null
? JValue.CreateNull()
: (JToken)LoadStrict(Path.Combine(directory, executeFile)) },
{ "idempotencyKey", index["idempotencyKey"].DeepClone() },
{ "businessMutationCount", index["businessMutationCount"].DeepClone() },
{ "nativeConfirmationObserved", index["nativeConfirmationObserved"].DeepClone() },
{ "auditEventCount", index["auditEventCount"].DeepClone() },
{ "sourceDocumentWritePayloadBound", index[
"sourceDocumentWritePayloadBound"].DeepClone() },
{ "sourceDocumentAuditCount", index[
"sourceDocumentAuditCount"].DeepClone() },
{ "observedAtUtc", index["observedAtUtc"].DeepClone() }
};
return Project(source, nowUtc);
}
public static JArray AssembleCaseSetFile(
string indexPath,
DateTime nowUtc,
out string workflow)
{
JObject index = LoadStrict(indexPath);
EnsureExact(index, "schemaVersion", "workflow", "caseFiles");
if (RequiredString(index, "schemaVersion", 1, 16) != "1.0")
throw Error("write_case_index_schema_invalid", "写用例索引版本无效。");
workflow = RequiredString(index, "workflow", 1, 32);
string[] required =
WorkflowWriteIntegrationEvidenceVerifier
.RequiredCaseCodesForWorkflow(workflow);
JArray files = index["caseFiles"] as JArray;
if (files == null || files.Count != required.Length)
throw Error("write_case_index_coverage_incomplete", "写用例索引没有覆盖全部必测用例。");
string indexDirectory = Path.GetDirectoryName(
Path.GetFullPath(indexPath));
Dictionary<string, JObject> observed =
new Dictionary<string, JObject>(StringComparer.Ordinal);
HashSet<string> fileNames = new HashSet<string>(
StringComparer.OrdinalIgnoreCase);
foreach (JToken token in files)
{
if (token == null || token.Type != JTokenType.String)
throw Error("write_case_index_schema_invalid", "写用例索引文件名必须是字符串。");
string fileName = ((string)token).Trim();
if (!SafeJsonFileName.IsMatch(fileName)
|| !fileNames.Add(fileName)
|| !string.Equals(
Path.GetFileName(fileName),
fileName,
StringComparison.Ordinal))
throw Error("write_case_index_path_invalid", "写用例索引只能引用同目录下的不重复 JSON 文件。");
JObject item = LoadStrict(Path.Combine(indexDirectory, fileName));
WorkflowWriteIntegrationEvidenceVerifier.ValidateProjectedCase(
item,
nowUtc);
string caseCode = RequiredString(item, "caseCode", 1, 128);
if (!required.Contains(caseCode, StringComparer.Ordinal)
|| observed.ContainsKey(caseCode))
throw Error("write_case_index_coverage_incomplete", "写用例索引包含重复或错误工作流用例。");
observed.Add(caseCode, item);
}
if (observed.Count != required.Length)
throw Error("write_case_index_coverage_incomplete", "写用例索引没有覆盖全部必测用例。");
JArray ordered = new JArray();
foreach (string caseCode in required)
ordered.Add(observed[caseCode].DeepClone());
WorkflowWriteIntegrationEvidenceVerifier.ValidateProjectedCaseSet(
workflow,
ordered,
nowUtc);
return ordered;
}
internal static JObject Project(JObject source, DateTime nowUtc)
{
EnsureExact(
source,
"schemaVersion", "caseCode", "commandName", "commandInput",
"uatAuthorizationSourceSha256", "uatAuthorizationContentSha256",
"uatAuthorizationIdSha256", "uatAuthorizationIssuedAtUtc",
"uatAuthorizationExpiresAtUtc", "uatExecutionCaseCode",
"uatTokenSha256", "runtimeCliVersion", "runtimeCliSha256",
"runtimeCliSignerThumbprint",
"contextCliResponse", "planCliResponse", "executeCliResponse",
"idempotencyKey",
"businessMutationCount", "nativeConfirmationObserved",
"auditEventCount", "sourceDocumentWritePayloadBound",
"sourceDocumentAuditCount", "observedAtUtc");
if (RequiredString(source, "schemaVersion", 1, 16) != "1.3")
throw Error("write_case_observation_schema_invalid", "写用例观察清单版本无效。");
string caseCode = RequiredString(source, "caseCode", 1, 128);
string commandName = RequiredString(source, "commandName", 1, 128);
if (!SafeCaseCode.IsMatch(caseCode)
|| WorkflowWriteIntegrationEvidenceVerifier.ExpectedResultCodeForCase(
caseCode) == null)
throw Error("write_case_observation_case_invalid", "写用例观察清单包含未知用例。");
if (!SafeCommandName.IsMatch(commandName))
throw Error("write_case_observation_command_invalid", "写用例观察清单命令名无效。");
string uatSourceHash = RequiredHash(
source,
"uatAuthorizationSourceSha256");
string uatContentHash = RequiredHash(
source,
"uatAuthorizationContentSha256");
string uatAuthorizationIdHash = RequiredHash(
source,
"uatAuthorizationIdSha256");
string uatTokenHash = RequiredHash(source, "uatTokenSha256");
string runtimeCliVersion = RequiredString(
source,
"runtimeCliVersion",
5,
14);
string runtimeCliHash = RequiredHash(source, "runtimeCliSha256");
string runtimeCliSigner = RequiredString(
source,
"runtimeCliSignerThumbprint",
40,
40);
if (!SafeRuntimeCliVersion.IsMatch(runtimeCliVersion)
|| !SignerThumbprintPattern.IsMatch(runtimeCliSigner))
throw Error(
"write_case_observation_runtime_cli_invalid",
"写用例没有绑定有效的运行 CLI 版本、哈希和签名。" );
string uatExecutionCaseCode = RequiredString(
source,
"uatExecutionCaseCode",
1,
128);
DateTime uatIssuedAt = RequiredUtc(
source,
"uatAuthorizationIssuedAtUtc");
DateTime uatExpiresAt = RequiredUtc(
source,
"uatAuthorizationExpiresAtUtc");
DateTime observedAt = RequiredUtc(source, "observedAtUtc");
if (!UatExecutionCaseMatches(caseCode, uatExecutionCaseCode)
|| uatExpiresAt <= uatIssuedAt
|| uatExpiresAt - uatIssuedAt
> WorkflowUatAuthorizationVerifier.MaximumLifetime
|| observedAt > uatExpiresAt.AddMinutes(5)
|| observedAt < uatIssuedAt.AddMinutes(-5))
throw Error(
"write_case_observation_uat_binding_invalid",
"写用例没有绑定同一短时 UAT 授权、令牌与有效期。");
JObject commandInput = RequiredObject(source, "commandInput");
ValidateCaseCommandInput(caseCode, commandName, commandInput);
string inputFingerprint = CommandInputFingerprint.Create(
commandName,
commandInput.ToObject<Dictionary<string, object>>());
ContextProjection context = ParseContext(
source["contextCliResponse"]);
CliObservation planResponse = ParseNullableCliResponse(
source["planCliResponse"],
"plan");
CliObservation executeResponse = ParseNullableCliResponse(
source["executeCliResponse"],
"execute");
if (planResponse == null && executeResponse == null)
throw Error("write_case_observation_response_required", "写用例观察清单至少需要一份 CLI 响应。");
string operationCorrelation = executeResponse != null
? executeResponse.CorrelationId
: planResponse.CorrelationId;
if (!string.Equals(
context.CorrelationId,
operationCorrelation,
StringComparison.Ordinal))
throw Error(
"write_case_observation_context_mismatch",
"ERP 上下文、计划和执行响应必须使用同一个关联 ID。");
if (executeResponse != null
&& (planResponse == null || !planResponse.Ok))
throw Error("write_case_observation_response_invalid", "执行观察必须绑定成功返回的计划响应。");
if (executeResponse != null
&& !string.Equals(
planResponse.CorrelationId,
executeResponse.CorrelationId,
StringComparison.Ordinal))
throw Error(
"write_case_observation_response_invalid",
"计划与执行 CLI 响应必须使用同一个关联 ID。");
PlanProjection plan = null;
if (planResponse != null && planResponse.Ok)
{
plan = ParsePlan(
planResponse.Data,
commandName,
inputFingerprint,
observedAt);
ValidateCasePlan(caseCode, plan, commandInput);
}
if (executeResponse != null
&& (plan == null || !plan.Valid || !plan.ExecutionAllowed))
throw Error(
"write_case_observation_response_invalid",
"执行观察必须绑定有效且允许执行的写计划。");
if (executeResponse == null && plan != null)
{
bool successfulResolution =
caseCode == "leave_natural_language_resolution"
|| caseCode == "leave_multi_day_calendar_resolution";
if ((successfulResolution
&& (!plan.Valid || plan.ExecutionAllowed))
|| (!successfulResolution
&& (plan.Valid || plan.ExecutionAllowed)))
throw Error(
"write_case_observation_plan_invalid",
"计划有效性与固定验收场景不一致。");
}
ResultProjection execution = null;
if (executeResponse != null && executeResponse.Ok)
execution = ParseExecution(executeResponse.Data);
if (execution != null
&& IsPurchaseSourceProofCase(caseCode)
&& !execution.Success)
throw Error(
"write_case_observation_execution_invalid",
"采购成功验收用例的业务执行结果必须明确成功。");
string resultCode = executeResponse != null
? executeResponse.Ok ? execution.Code : executeResponse.Code
: planResponse.Ok ? plan.OutcomeCode : planResponse.Code;
string expectedResult =
WorkflowWriteIntegrationEvidenceVerifier.ExpectedResultCodeForCase(
caseCode);
if (!string.Equals(resultCode, expectedResult, StringComparison.Ordinal))
throw Error(
"write_case_observation_result_mismatch",
"CLI 观察结果与该固定验收用例的稳定码不一致。");
string expectedIssue =
WorkflowWriteIntegrationEvidenceVerifier.ExpectedIssueCodeForCase(
caseCode);
string issueCode = ExtractIssueCode(plan, expectedIssue);
string idempotencyKey = NullableString(
source,
"idempotencyKey",
8,
128);
if (idempotencyKey != null
&& !SafeIdempotencyKey.IsMatch(idempotencyKey))
throw Error("write_case_observation_idempotency_invalid", "写用例观察清单幂等键格式无效。");
bool sourceProofRequired = IsPurchaseSourceProofCase(caseCode);
SourceDocumentEvidence sourceEvidence =
ReadSourceDocumentEvidence(commandInput);
IList<string> sourceHashes = sourceEvidence.Hashes;
string sourceSet = null;
bool sourceInputBound = false;
if (sourceProofRequired)
{
if (sourceHashes.Count == 0
|| plan == null
|| !sourceEvidence.PreprocessContracts.Contains(
PurchaseSourceDocumentContract.PdfPreprocessContract,
StringComparer.Ordinal))
throw Error(
"write_case_observation_source_invalid",
"采购来源证明用例缺少受信任电子 PDF、处理契约或计划输入绑定。");
sourceSet = Sha256(string.Join("\n", sourceHashes.ToArray()));
sourceInputBound = true;
}
JObject projected = new JObject
{
{ "caseCode", caseCode },
{ "uatAuthorizationSourceSha256", uatSourceHash },
{ "uatAuthorizationContentSha256", uatContentHash },
{ "uatAuthorizationIdSha256", uatAuthorizationIdHash },
{ "uatTokenSha256", uatTokenHash },
{ "runtimeCliVersion", runtimeCliVersion },
{ "runtimeCliSha256", runtimeCliHash },
{ "runtimeCliSignerThumbprint", runtimeCliSigner },
{ "passed", true },
{ "correlationId", operationCorrelation },
{ "contextCorrelationBound", true },
{ "commandName", commandName },
{ "planCommandVersion", plan == null
? JValue.CreateNull()
: new JValue(plan.CommandVersion) },
{ "planModuleCode", plan == null
? JValue.CreateNull()
: new JValue(plan.ModuleCode) },
{ "planRisk", plan == null
? JValue.CreateNull()
: new JValue(plan.Risk) },
{ "accountBookSha256", Sha256(context.AccountBook) },
{ "subSystemIdSha256", Sha256(context.SubSystemId) },
{ "userIdSha256", Sha256(context.UserId) },
{ "userNameSha256", Sha256(context.UserName) },
{ "databaseScopeFingerprint",
context.DatabaseScopeFingerprint },
{ "isAdministrator", context.IsAdministrator },
{ "inputFingerprintSha256", inputFingerprint },
{ "planFingerprintSha256", plan == null
? JValue.CreateNull()
: new JValue(Sha256(plan.PlanId)) },
{ "resultCode", resultCode },
{ "issueCode", issueCode == null
? JValue.CreateNull()
: new JValue(issueCode) },
{ "recordIdSha256", HashNullable(execution == null
? null : execution.RecordId) },
{ "transactionEvidenceIdSha256", HashNullable(execution == null
? null : execution.TransactionEvidenceId) },
{ "businessAuditIdSha256", HashNullable(execution == null
? null : execution.BusinessAuditId) },
{ "idempotencyKeySha256", HashNullable(idempotencyKey) },
{ "businessMutationCount", RequiredInteger(
source, "businessMutationCount", 0, 1000) },
{ "replayed", execution != null && execution.Replayed },
{ "nativeConfirmationObserved", RequiredBoolean(
source, "nativeConfirmationObserved") },
{ "auditEventCount", RequiredInteger(
source, "auditEventCount", 1, 1000) },
{ "sourceDocumentSetSha256", sourceSet == null
? JValue.CreateNull()
: new JValue(sourceSet) },
{ "sourceDocumentPreprocessContracts", sourceProofRequired
? new JArray(sourceEvidence.PreprocessContracts)
: new JArray() },
{ "sourceDocumentInputFingerprintBound", sourceInputBound },
{ "sourceDocumentWritePayloadBound", RequiredBoolean(
source, "sourceDocumentWritePayloadBound") },
{ "sourceDocumentAuditCount", RequiredInteger(
source, "sourceDocumentAuditCount", 0, 1000) },
{ "observedAtUtc", observedAt.ToUniversalTime().ToString(
"yyyy-MM-dd'T'HH:mm:ss.fff'Z'",
CultureInfo.InvariantCulture) }
};
WorkflowWriteIntegrationEvidenceVerifier.ValidateProjectedCase(
projected,
nowUtc);
return projected;
}
private static bool UatExecutionCaseMatches(
string evidenceCaseCode,
string executionCaseCode)
{
if (string.Equals(
evidenceCaseCode,
executionCaseCode,
StringComparison.Ordinal))
return true;
return (evidenceCaseCode == "purchase_audit_correlated"
&& executionCaseCode == "purchase_unique_match_commit")
|| (evidenceCaseCode == "leave_audit_correlated"
&& executionCaseCode == "leave_create_draft_commit");
}
private static void ValidateCaseCommandInput(
string caseCode,
string commandName,
JObject commandInput)
{
string expectedCommand =
WorkflowWriteIntegrationEvidenceVerifier
.ExpectedCommandNameForCase(caseCode);
if (!string.Equals(
commandName,
expectedCommand,
StringComparison.Ordinal))
throw Error(
"write_case_observation_case_invalid",
"写验收用例调用的命令与固定场景不一致。");
if (IsPurchaseSourceProofCase(caseCode))
{
SourceDocumentEvidence sourceEvidence =
ReadSourceDocumentEvidence(commandInput);
if (sourceEvidence.Hashes.Count == 0
|| !sourceEvidence.PreprocessContracts.Contains(
PurchaseSourceDocumentContract.PdfPreprocessContract,
StringComparer.Ordinal))
throw Error(
"write_case_observation_source_invalid",
"采购成功验收用例必须绑定至少一份受信任电子 PDF。");
try
{
CommandInputSchemaValidator.Validate(
CommandInputSchemas.PurchaseInvoiceCreate(),
commandInput.ToObject<Dictionary<string, object>>());
}
catch (CommandKernelException)
{
throw Error(
"write_case_observation_case_invalid",
"采购成功验收输入不符合创建命令 Schema。");
}
string proof = commandInput.Value<string>("resolutionProof");
if (!PurchaseResolutionProof.IsMatch(proof ?? string.Empty))
throw Error(
"write_case_observation_case_invalid",
"采购成功验收输入缺少服务器短期解析凭证。");
return;
}
bool successfulResolution =
caseCode == "leave_natural_language_resolution"
|| caseCode == "leave_multi_day_calendar_resolution";
if (!successfulResolution) return;
try
{
CommandInputSchemaValidator.Validate(
CommandInputSchemas.LeaveResolve(),
commandInput.ToObject<Dictionary<string, object>>());
}
catch (CommandKernelException)
{
throw Error(
"write_case_observation_case_invalid",
"自然语言请假验收输入不符合命令 Schema。");
}
if (caseCode != "leave_multi_day_calendar_resolution") return;
DateTime startDate;
DateTime endDate;
if (!TryReadMultiDayDates(
commandInput.Value<string>("dateExpression"),
out startDate,
out endDate)
|| commandInput.Value<decimal?>("requestedHours") <= 0m)
{
throw Error(
"write_case_observation_case_invalid",
"多日请假验收必须使用两个绝对日期、两端明确时段和已知的日历总工时断言。");
}
}
private static void ValidateCasePlan(
string caseCode,
PlanProjection plan,
JObject commandInput)
{
if (IsPurchaseSourceProofCase(caseCode))
{
IList<string> sourceHashes = ReadSourceDocumentEvidence(
commandInput).Hashes;
JToken countToken = plan == null
? null
: plan.Data["sourceDocumentCount"];
int sourceCount;
string sourceSet = plan == null
? null
: RawString(plan.Data, "sourceDocumentSetSha256");
string expectedSet = Sha256(string.Join(
"\n",
sourceHashes.ToArray()));
if (plan == null
|| !plan.Valid
|| !plan.ExecutionAllowed
|| plan.OutcomeCode != "purchase_create_ready"
|| countToken == null
|| countToken.Type != JTokenType.Integer
|| !int.TryParse(
countToken.ToString(Formatting.None),
NumberStyles.None,
CultureInfo.InvariantCulture,
out sourceCount)
|| sourceCount != sourceHashes.Count
|| sourceCount <= 0
|| !string.Equals(
sourceSet,
expectedSet,
StringComparison.Ordinal))
{
throw Error(
"write_case_observation_plan_invalid",
"采购成功验收计划没有绑定完整来源附件集合。");
}
return;
}
bool multiDay = caseCode == "leave_multi_day_calendar_resolution";
if (!multiDay && caseCode != "leave_natural_language_resolution")
return;
JObject resolved = plan == null
? null
: plan.Data["resolvedInput"] as JObject;
DateTime startLocal;
DateTime endLocal;
DateTime expectedStartDate;
DateTime expectedEndDate;
decimal? expectedHours = commandInput == null
? null
: commandInput.Value<decimal?>("requestedHours");
if (plan == null
|| !plan.Valid
|| plan.ExecutionAllowed
|| plan.OutcomeCode != "leave_intent_resolved"
|| plan.Data.Value<string>("resolvedCommand") != "hr.leave.create"
|| plan.Data.Value<bool?>("requiresFollowupPlan") != true
|| resolved == null)
{
throw Error(
"write_case_observation_plan_invalid",
"自然语言请假验收没有返回唯一的创建续接输入。");
}
try
{
CommandInputSchemaValidator.Validate(
CommandInputSchemas.LeaveCreate(),
resolved.ToObject<Dictionary<string, object>>());
}
catch (CommandKernelException)
{
throw Error(
"write_case_observation_plan_invalid",
"自然语言请假续接输入不符合创建命令 Schema。");
}
string proof = resolved.Value<string>("resolutionProof");
if (!LeaveResolutionProof.IsMatch(proof ?? string.Empty))
{
throw Error(
"write_case_observation_plan_invalid",
"自然语言请假续接结果缺少服务器短期解析凭证。");
}
if (!multiDay)
{
if (!TryReadLocalDateTime(
resolved.Value<string>("startLocal"),
out startLocal)
|| !TryReadLocalDateTime(
resolved.Value<string>("endLocal"),
out endLocal)
|| startLocal.Date != endLocal.Date
|| endLocal <= startLocal
|| resolved.Value<decimal?>("requestedHours") <= 0m)
{
throw Error(
"write_case_observation_plan_invalid",
"单日自然语言请假续接结果不是有效的员工日历范围。");
}
return;
}
if (!TryReadLocalDateTime(
resolved.Value<string>("startLocal"),
out startLocal)
|| !TryReadLocalDateTime(
resolved.Value<string>("endLocal"),
out endLocal)
|| !TryReadMultiDayDates(
commandInput.Value<string>("dateExpression"),
out expectedStartDate,
out expectedEndDate)
|| startLocal.Date != expectedStartDate.Date
|| endLocal.Date != expectedEndDate.Date
|| endLocal.Date <= startLocal.Date
|| expectedHours <= 0m
|| resolved.Value<decimal?>("requestedHours") != expectedHours)
{
throw Error(
"write_case_observation_plan_invalid",
"多日请假续接结果不是与测试断言一致的跨日日历范围。");
}
}
private static bool TryReadMultiDayDates(
string expression,
out DateTime startDate,
out DateTime endDate)
{
startDate = DateTime.MinValue;
endDate = DateTime.MinValue;
string normalized = Regex.Replace(
expression ?? string.Empty,
@"[\s的]+",
string.Empty);
Match match = MultiDayLeaveExpression.Match(normalized);
return match.Success
&& DateTime.TryParseExact(
match.Groups["start"].Value,
new[] { "yyyy-M-d", "yyyy-MM-dd" },
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out startDate)
&& DateTime.TryParseExact(
match.Groups["end"].Value,
new[] { "yyyy-M-d", "yyyy-MM-dd" },
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out endDate)
&& endDate.Date > startDate.Date;
}
private static bool IsPurchaseSourceProofCase(string caseCode)
{
return caseCode == "purchase_unique_match_commit"
|| caseCode == "purchase_idempotency_replay"
|| caseCode == "purchase_audit_correlated";
}
private static bool TryReadLocalDateTime(
string value,
out DateTime result)
{
return DateTime.TryParseExact(
value,
"yyyy-MM-dd'T'HH:mm:ss",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out result)
&& result.Kind == DateTimeKind.Unspecified;
}
public static string WriteNewFile(JObject projected, string outputPath)
{
if (projected == null)
throw new ArgumentNullException("projected");
return WriteNewToken(projected, outputPath);
}
public static string WriteNewCaseSetFile(
JArray cases,
string outputPath)
{
if (cases == null) throw new ArgumentNullException("cases");
return WriteNewToken(cases, outputPath);
}
private static string WriteNewToken(JToken value, string outputPath)
{
if (string.IsNullOrWhiteSpace(outputPath))
throw Error("write_case_observation_output_required", "请提供新的写用例证据输出文件。");
try
{
string full = Path.GetFullPath(outputPath);
string directory = Path.GetDirectoryName(full);
if (string.IsNullOrWhiteSpace(directory)
|| !Directory.Exists(directory))
throw Error("write_case_observation_output_invalid", "写用例证据输出目录不存在。");
byte[] body = new UTF8Encoding(false, true).GetBytes(
value.ToString(Formatting.Indented));
if (body.Length <= 0 || body.Length > MaximumOutputBytes)
throw Error("write_case_observation_output_invalid", "写用例证据输出大小无效。");
using (FileStream stream = new FileStream(
full,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None))
{
stream.Write(body, 0, body.Length);
stream.Flush();
}
return full;
}
catch (CommandKernelException) { throw; }
catch (IOException)
{
throw Error(
"write_case_observation_output_exists",
"无法新建写用例证据;命令不会覆盖已有文件。");
}
catch (UnauthorizedAccessException)
{
throw Error("write_case_observation_output_denied", "当前用户无权创建写用例证据。");
}
catch
{
throw Error("write_case_observation_output_invalid", "写用例证据输出路径无效。");
}
}
private static ContextProjection ParseContext(JToken token)
{
CliObservation response = ParseNullableCliResponse(
token,
"context");
if (response == null || !response.Ok)
throw Error(
"write_case_observation_context_invalid",
"写用例必须绑定成功返回的 ERP 上下文响应。");
JObject data = response.Data;
EnsureExact(
data,
"userId", "userName", "accountBook", "subSystemId",
"databaseScopeFingerprint", "subSystemName",
"isAdministrator", "activeModule",
"openModuleCount", "openModulesTruncated", "openModules");
string userId = RequiredContextIdentifier(data, "userId");
string userName = RequiredContextText(data, "userName", 500);
string accountBook = RequiredContextText(
data,
"accountBook",
500);
string subSystemId = RequiredContextIdentifier(
data,
"subSystemId");
string databaseScopeFingerprint = RequiredContextText(
data,
"databaseScopeFingerprint",
64);
if (!CommandInputFingerprint.IsValid(
databaseScopeFingerprint))
throw Error(
"write_case_observation_context_invalid",
"ERP 数据库作用域指纹格式无效。");
RequiredContextText(data, "subSystemName", 500);
bool isAdministrator = RequiredBoolean(data, "isAdministrator");
int openCount = RequiredInteger(
data,
"openModuleCount",
0,
100000);
bool truncated = RequiredBoolean(data, "openModulesTruncated");
JArray openModules = data["openModules"] as JArray;
if (openModules == null
|| openModules.Count > 50
|| openCount < openModules.Count
|| truncated != (openCount > openModules.Count))
throw Error(
"write_case_observation_context_invalid",
"ERP 上下文打开模块统计不一致。");
foreach (JToken item in openModules)
ValidateContextModule(item, "openModules");
JToken active = data["activeModule"];
if (active == null)
throw Error(
"write_case_observation_context_invalid",
"ERP 上下文缺少活动模块字段。");
if (active.Type != JTokenType.Null)
{
ValidateContextModule(active, "activeModule");
if (openCount == 0
|| (!truncated && !openModules.Any(item =>
JToken.DeepEquals(item, active))))
throw Error(
"write_case_observation_context_invalid",
"ERP 活动模块与已打开模块不一致。");
}
return new ContextProjection
{
CorrelationId = response.CorrelationId,
UserId = userId,
UserName = userName,
AccountBook = accountBook,
SubSystemId = subSystemId,
DatabaseScopeFingerprint = databaseScopeFingerprint,
IsAdministrator = isAdministrator
};
}
private static void ValidateContextModule(JToken token, string name)
{
JObject module = token as JObject;
if (module == null)
throw Error(
"write_case_observation_context_invalid",
"ERP 上下文模块结构无效:" + name + "。");
EnsureExact(
module,
"moduleCode", "navigationCode", "moduleName");
RequiredContextIdentifier(module, "moduleCode");
RequiredContextIdentifier(module, "navigationCode");
RequiredContextText(module, "moduleName", 500);
}
private static string RequiredContextIdentifier(
JObject source,
string name)
{
string value = RequiredContextText(source, name, 128);
if (!SafeCommandName.IsMatch(value))
throw Error(
"write_case_observation_context_invalid",
"ERP 上下文标识格式无效。");
return value;
}
private static string RequiredContextText(
JObject source,
string name,
int maximum)
{
JToken token = source == null ? null : source[name];
if (token == null || token.Type != JTokenType.String)
throw Error(
"write_case_observation_context_invalid",
"ERP 上下文文本字段无效。");
string value = (string)token;
if (string.IsNullOrWhiteSpace(value)
|| value.Length > maximum
|| !string.Equals(value, value.Trim(), StringComparison.Ordinal)
|| value.Any(char.IsControl))
throw Error(
"write_case_observation_context_invalid",
"ERP 上下文文本字段无效。");
return value;
}
private static PlanProjection ParsePlan(
JObject data,
string expectedCommand,
string expectedInputFingerprint,
DateTime observedAtUtc)
{
EnsureExact(data, "plan");
JObject plan = RequiredObject(data, "plan");
EnsureExact(
plan,
"planId", "commandName", "commandVersion", "moduleCode", "risk",
"createdAtUtc", "expiresAtUtc", "valid", "executionAllowed",
"inputFingerprint", "outcomeCode", "title", "preview", "data",
"warnings");
string planId = RequiredString(plan, "planId", 32, 32);
string commandName = RequiredString(plan, "commandName", 1, 128);
string commandVersion = RequiredString(
plan,
"commandVersion",
3,
16);
string moduleCode = RequiredString(plan, "moduleCode", 1, 64);
string risk = RequiredString(plan, "risk", 1, 16);
DateTime createdAtUtc = RequiredUtc(plan, "createdAtUtc");
DateTime expiresAtUtc = RequiredUtc(plan, "expiresAtUtc");
string fingerprint = RequiredString(plan, "inputFingerprint", 64, 64)
.ToLowerInvariant();
string outcomeCode = RequiredString(plan, "outcomeCode", 1, 128);
string expectedVersion;
string expectedRisk;
ExpectedPlanContract(
expectedCommand,
out expectedVersion,
out expectedRisk);
if (!SafePlanId.IsMatch(planId)
|| !SafeCommandName.IsMatch(commandName)
|| !string.Equals(commandName, expectedCommand, StringComparison.Ordinal)
|| !SafeCommandVersion.IsMatch(commandVersion)
|| !string.Equals(
commandVersion,
expectedVersion,
StringComparison.Ordinal)
|| !SafeModuleCode.IsMatch(moduleCode)
|| !string.Equals(risk, expectedRisk, StringComparison.Ordinal)
|| expiresAtUtc <= createdAtUtc
|| expiresAtUtc != createdAtUtc.AddMinutes(10)
|| createdAtUtc > observedAtUtc.AddMinutes(5)
|| !Sha256Pattern.IsMatch(fingerprint)
|| !string.Equals(
fingerprint,
expectedInputFingerprint,
StringComparison.Ordinal)
|| !SafeCode.IsMatch(outcomeCode)
|| !(plan["data"] is JObject)
|| !(plan["warnings"] is JArray)
|| plan["valid"] == null
|| plan["valid"].Type != JTokenType.Boolean
|| plan["executionAllowed"] == null
|| plan["executionAllowed"].Type != JTokenType.Boolean)
throw Error("write_case_observation_plan_invalid", "CLI 计划响应与观察输入不一致。");
JObject planData = (JObject)plan["data"];
JToken dataOutcome = planData["outcomeCode"];
if (dataOutcome == null
|| dataOutcome.Type != JTokenType.String
|| !string.Equals(
(string)dataOutcome,
outcomeCode,
StringComparison.Ordinal))
throw Error("write_case_observation_plan_invalid", "CLI 计划稳定码投影不一致。");
return new PlanProjection
{
PlanId = planId.ToLowerInvariant(),
CommandVersion = commandVersion,
ModuleCode = moduleCode,
Risk = risk,
OutcomeCode = outcomeCode,
Valid = (bool)plan["valid"],
ExecutionAllowed = (bool)plan["executionAllowed"],
Data = planData
};
}
private static void ExpectedPlanContract(
string commandName,
out string version,
out string risk)
{
switch (commandName)
{
case "purchase.invoice.create":
version = "1.4";
risk = "write";
return;
case "hr.leave.resolve":
version = "1.4";
risk = "draft";
return;
case "hr.leave.create":
version = "1.2";
risk = "write";
return;
case "hr.leave.submit":
version = "1.0";
risk = "write";
return;
default:
throw Error(
"write_case_observation_command_invalid",
"写验收用例命令不属于固定工作流合同。");
}
}
private static ResultProjection ParseExecution(JObject data)
{
EnsureExact(data, "result", "followupPlan", "followupCode");
JObject result = RequiredObject(data, "result");
EnsureExact(
result,
"success", "code", "message", "recordId", "replayed",
"transactionEvidenceId", "businessAuditId", "data");
string code = RequiredString(result, "code", 1, 128);
if (!SafeCode.IsMatch(code)
|| result["success"] == null
|| result["success"].Type != JTokenType.Boolean
|| result["replayed"] == null
|| result["replayed"].Type != JTokenType.Boolean
|| !(result["data"] is JObject)
|| (data["followupPlan"].Type != JTokenType.Null
&& !(data["followupPlan"] is JObject))
|| (data["followupCode"].Type != JTokenType.Null
&& data["followupCode"].Type != JTokenType.String))
throw Error("write_case_observation_execution_invalid", "CLI 执行响应结构无效。");
return new ResultProjection
{
Success = (bool)result["success"],
Code = code,
Replayed = (bool)result["replayed"],
RecordId = NullableBoundedRaw(result, "recordId"),
TransactionEvidenceId = NullableBoundedRaw(
result,
"transactionEvidenceId"),
BusinessAuditId = NullableBoundedRaw(
result,
"businessAuditId")
};
}
private static string ExtractIssueCode(
PlanProjection plan,
string expectedIssue)
{
if (expectedIssue == null) return null;
if (plan == null)
throw Error("write_case_observation_issue_missing", "解析阻断用例缺少 CLI 计划问题码。");
JArray issues = plan.Data["issues"] as JArray;
if (issues == null)
throw Error("write_case_observation_issue_missing", "解析阻断用例缺少 CLI 计划问题码。");
int matches = 0;
foreach (JObject issue in issues.OfType<JObject>())
{
JToken code = issue["code"];
if (code != null && code.Type == JTokenType.String
&& string.Equals(
(string)code,
expectedIssue,
StringComparison.Ordinal))
matches += 1;
}
if (matches != 1)
throw Error("write_case_observation_issue_missing", "CLI 计划没有唯一记录该用例的精确问题码。");
return expectedIssue;
}
private static SourceDocumentEvidence ReadSourceDocumentEvidence(
JObject commandInput)
{
JToken token = commandInput["sourceDocuments"];
if (token == null)
return new SourceDocumentEvidence();
JArray documents = token as JArray;
if (documents == null
|| documents.Count > PurchaseSourceDocumentContract.MaximumCount)
throw Error("write_case_observation_source_invalid", "来源附件观察结构无效。");
HashSet<string> hashes = new HashSet<string>(StringComparer.Ordinal);
HashSet<string> preprocessContracts = new HashSet<string>(
StringComparer.Ordinal);
foreach (JToken item in documents)
{
JObject document = item as JObject;
EnsureExact(
document,
"kind", "filename", "sha256", "sizeBytes",
"extractionSha256", "preprocessContract");
string kind = RawString(document, "kind");
string filename = RawString(document, "filename");
string sourceSha256 = RawString(document, "sha256");
string extractionSha256 = RawString(
document,
"extractionSha256");
string preprocessContract = RawString(
document,
"preprocessContract");
JToken sizeToken = document["sizeBytes"];
long sizeBytes;
if ((kind != "image" && kind != "file")
|| string.IsNullOrWhiteSpace(filename)
|| filename.Length > 128
|| !string.Equals(
filename,
filename.Trim(),
StringComparison.Ordinal)
|| filename.Any(char.IsControl)
|| filename == "." || filename == ".."
|| filename.IndexOf('/') >= 0
|| filename.IndexOf('\\') >= 0
|| !Sha256Pattern.IsMatch(sourceSha256 ?? string.Empty)
|| !Sha256Pattern.IsMatch(
extractionSha256 ?? string.Empty)
|| sizeToken == null
|| sizeToken.Type != JTokenType.Integer
|| !long.TryParse(
sizeToken.ToString(Formatting.None),
NumberStyles.None,
CultureInfo.InvariantCulture,
out sizeBytes)
|| sizeBytes <= 0
|| sizeBytes
> PurchaseSourceDocumentContract.MaximumSizeBytes
|| !PurchaseSourceDocumentContract.Matches(
kind,
filename,
preprocessContract)
|| !hashes.Add(sourceSha256))
throw Error(
"write_case_observation_source_invalid",
"来源附件双摘要、预处理契约或元数据无效。");
preprocessContracts.Add(preprocessContract);
}
SourceDocumentEvidence result = new SourceDocumentEvidence
{
Hashes = hashes.OrderBy(
item => item,
StringComparer.Ordinal).ToList(),
PreprocessContracts = preprocessContracts.OrderBy(
item => item,
StringComparer.Ordinal).ToList()
};
return result;
}
private static string RawString(JObject source, string name)
{
JToken token = source == null ? null : source[name];
return token != null && token.Type == JTokenType.String
? (string)token
: null;
}
private static CliObservation ParseNullableCliResponse(
JToken token,
string kind)
{
if (token == null || token.Type == JTokenType.Null) return null;
JObject response = token as JObject;
if (response == null)
throw Error("write_case_observation_response_invalid", "CLI 响应必须是对象或 null。");
JToken okToken = response["ok"];
if (okToken == null || okToken.Type != JTokenType.Boolean)
throw Error("write_case_observation_response_invalid", "CLI 响应缺少 ok 布尔值。");
bool ok = (bool)okToken;
if (ok)
EnsureExact(response, "ok", "correlationId", "data");
else
EnsureExact(response, "ok", "correlationId", "error");
string correlation = RequiredString(
response,
"correlationId",
8,
128);
if (!SafeCorrelationId.IsMatch(correlation))
throw Error("write_case_observation_response_invalid", "CLI 响应关联 ID 无效。");
if (ok)
{
JObject data = RequiredObject(response, "data");
return new CliObservation
{
Ok = true,
CorrelationId = correlation,
Data = data,
Kind = kind
};
}
JObject error = RequiredObject(response, "error");
EnsureExact(error, "code", "message", "exitCode");
string code = RequiredString(error, "code", 1, 128);
string message = RequiredString(error, "message", 1, 2000);
if (!SafeCode.IsMatch(code)
|| message.Any(char.IsControl)
|| RequiredInteger(error, "exitCode", 1, 255) < 1)
throw Error("write_case_observation_response_invalid", "CLI 错误响应结构无效。");
return new CliObservation
{
Ok = false,
CorrelationId = correlation,
Code = code,
Kind = kind
};
}
private static JObject LoadStrict(string path)
{
if (string.IsNullOrWhiteSpace(path))
throw Error("write_case_observation_input_required", "请提供原始写用例观察清单。");
string full;
try { full = Path.GetFullPath(path); }
catch { throw Error("write_case_observation_input_invalid", "写用例观察清单路径无效。"); }
FileInfo file = new FileInfo(full);
if (!file.Exists)
throw Error("write_case_observation_not_found", "写用例观察清单不存在。");
if (file.Length <= 0 || file.Length > MaximumInputBytes
|| (file.Attributes & FileAttributes.ReparsePoint) != 0)
throw Error("write_case_observation_size_invalid", "写用例观察清单必须是 4 MB 内的非空普通文件。");
try
{
using (FileStream stream = new FileStream(
full,
FileMode.Open,
FileAccess.Read,
FileShare.Read))
using (StreamReader text = new StreamReader(
stream,
new UTF8Encoding(false, true),
true,
4096))
using (RejectCommentsJsonReader json =
new RejectCommentsJsonReader(text))
{
json.DateParseHandling = DateParseHandling.None;
JObject value = JObject.Load(json, new JsonLoadSettings
{
DuplicatePropertyNameHandling =
DuplicatePropertyNameHandling.Error,
CommentHandling = CommentHandling.Ignore,
LineInfoHandling = LineInfoHandling.Ignore
});
if (json.Read())
throw Error("write_case_observation_json_invalid", "写用例观察清单包含多个 JSON 根值。");
return value;
}
}
catch (CommandKernelException) { throw; }
catch
{
throw Error("write_case_observation_json_invalid", "写用例观察清单不是严格 UTF-8 JSON 对象。");
}
}
private static void EnsureExact(JObject value, params string[] names)
{
if (value == null || value.Properties().Count() != names.Length
|| names.Any(name =>
value.Property(name, StringComparison.Ordinal) == null))
throw Error("write_case_observation_schema_invalid", "写用例观察清单包含缺失或未知字段。");
}
private static JObject RequiredObject(JObject source, string name)
{
JObject value = source == null ? null : source[name] as JObject;
if (value == null)
throw Error("write_case_observation_schema_invalid", "写用例观察清单对象字段无效。");
return value;
}
private static string RequiredString(
JObject source,
string name,
int minimum,
int maximum)
{
JToken token = source == null ? null : source[name];
if (token == null || token.Type != JTokenType.String)
throw Error("write_case_observation_schema_invalid", "写用例观察清单文本字段无效。");
string value = ((string)token).Trim();
if (value.Length < minimum || value.Length > maximum)
throw Error("write_case_observation_schema_invalid", "写用例观察清单文本长度无效。");
return value;
}
private static string NullableString(
JObject source,
string name,
int minimum,
int maximum)
{
JToken token = source == null ? null : source[name];
if (token == null || token.Type == JTokenType.Null) return null;
return RequiredString(source, name, minimum, maximum);
}
private static string RequiredSiblingJsonFileName(
JObject source,
string name)
{
string value = RequiredString(source, name, 6, 181);
if (!SafeJsonFileName.IsMatch(value)
|| !string.Equals(
Path.GetFileName(value),
value,
StringComparison.Ordinal))
throw Error(
"write_case_file_index_path_invalid",
"写用例文件索引只能引用同目录下的安全 JSON 文件名。");
return value;
}
private static string NullableSiblingJsonFileName(
JObject source,
string name)
{
JToken token = source[name];
if (token == null || token.Type == JTokenType.Null) return null;
return RequiredSiblingJsonFileName(source, name);
}
private static string NullableBoundedRaw(JObject source, string name)
{
JToken token = source[name];
if (token == null || token.Type == JTokenType.Null) return null;
if (token.Type != JTokenType.String)
throw Error("write_case_observation_execution_invalid", "CLI 执行标识字段类型无效。");
string value = (string)token;
if (string.IsNullOrWhiteSpace(value)
|| value.Length > 512
|| !string.Equals(value, value.Trim(), StringComparison.Ordinal)
|| value.Any(char.IsControl))
throw Error("write_case_observation_execution_invalid", "CLI 执行标识字段格式无效。");
return value;
}
private static bool RequiredBoolean(JObject source, string name)
{
JToken token = source == null ? null : source[name];
if (token == null || token.Type != JTokenType.Boolean)
throw Error("write_case_observation_schema_invalid", "写用例观察清单布尔字段无效。");
return (bool)token;
}
private static int RequiredInteger(
JObject source,
string name,
int minimum,
int maximum)
{
JToken token = source == null ? null : source[name];
int value;
if (token == null || token.Type != JTokenType.Integer
|| !int.TryParse(
token.ToString(Formatting.None),
NumberStyles.None,
CultureInfo.InvariantCulture,
out value)
|| value < minimum || value > maximum)
throw Error("write_case_observation_schema_invalid", "写用例观察清单整数字段无效。");
return value;
}
private static DateTime RequiredUtc(JObject source, string name)
{
string text = RequiredString(source, name, 20, 40);
DateTime value;
if (!DateTime.TryParse(
text,
CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal,
out value)
|| value.Kind != DateTimeKind.Utc
|| !text.EndsWith("Z", StringComparison.OrdinalIgnoreCase))
throw Error("write_case_observation_schema_invalid", "写用例观察清单 UTC 时间无效。");
return value;
}
private static string RequiredHash(JObject source, string name)
{
string value = RequiredString(source, name, 64, 64);
if (!Sha256Pattern.IsMatch(value))
throw Error(
"write_case_observation_schema_invalid",
"写用例观察清单 SHA-256 字段无效。");
return value;
}
private static JToken HashNullable(string value)
{
return value == null
? (JToken)JValue.CreateNull()
: new JValue(Sha256(value));
}
private static string Sha256(string value)
{
using (SHA256 sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(
new UTF8Encoding(false).GetBytes(value ?? string.Empty));
return BitConverter.ToString(hash)
.Replace("-", string.Empty)
.ToLowerInvariant();
}
}
private static CommandKernelException Error(string code, string message)
{
return new CommandKernelException(code, message, 6);
}
private sealed class CliObservation
{
public bool Ok { get; set; }
public string CorrelationId { get; set; }
public string Code { get; set; }
public string Kind { get; set; }
public JObject Data { get; set; }
}
private sealed class ContextProjection
{
public string CorrelationId { get; set; }
public string UserId { get; set; }
public string UserName { get; set; }
public string AccountBook { get; set; }
public string SubSystemId { get; set; }
public string DatabaseScopeFingerprint { get; set; }
public bool IsAdministrator { get; set; }
}
private sealed class PlanProjection
{
public string PlanId { get; set; }
public string CommandVersion { get; set; }
public string ModuleCode { get; set; }
public string Risk { get; set; }
public string OutcomeCode { get; set; }
public bool Valid { get; set; }
public bool ExecutionAllowed { get; set; }
public JObject Data { get; set; }
}
private sealed class ResultProjection
{
public bool Success { get; set; }
public string Code { get; set; }
public bool Replayed { get; set; }
public string RecordId { get; set; }
public string TransactionEvidenceId { get; set; }
public string BusinessAuditId { get; set; }
}
private sealed class SourceDocumentEvidence
{
public SourceDocumentEvidence()
{
Hashes = new List<string>();
PreprocessContracts = new List<string>();
}
public IList<string> Hashes { get; set; }
public IList<string> PreprocessContracts { get; set; }
}
private sealed class RejectCommentsJsonReader : JsonTextReader
{
public RejectCommentsJsonReader(TextReader reader) : base(reader) { }
public override bool Read()
{
bool result = base.Read();
if (result && TokenType == JsonToken.Comment)
throw Error("write_case_observation_json_invalid", "写用例观察清单不允许 JSON 注释。");
return result;
}
}
}
}