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

1352 lines
67 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
{
public sealed class WorkflowWriteIntegrationEvidenceResult
{
public string ContentSha256 { get; set; }
public string Workflow { get; set; }
public string ModuleCode { get; set; }
public string AccountBook { get; set; }
public string SubSystemId { get; set; }
public string UserIdSha256 { get; set; }
public string UserNameSha256 { get; set; }
public string DatabaseScopeFingerprint { get; set; }
public bool IsAdministrator { get; set; }
public string SourceCommit { get; set; }
public string PackageSha256 { get; set; }
public string RuntimeConfigurationSha256 { get; set; }
public string RuntimeCliVersion { get; set; }
public string RuntimeCliSha256 { get; set; }
public string RuntimeCliSignerThumbprint { get; set; }
public string UatAuthorizationSourceSha256 { get; set; }
public string UatAuthorizationContentSha256 { get; set; }
public string UatAuthorizationIdSha256 { get; set; }
public string EnvironmentId { get; set; }
public DateTime TestedAtUtc { get; set; }
public string TestedBy { get; set; }
public int CaseCount { get; set; }
internal IDictionary<string, string> UatTokenSha256ByCase { get; set; }
public IDictionary<string, object> ToDictionary()
{
return new Dictionary<string, object>
{
{ "evidenceType", "workflow_write_integration" },
{ "schemaVersion", "1.6" },
{ "contentSha256", ContentSha256 },
{ "workflow", Workflow },
{ "moduleCode", ModuleCode },
{ "erpScope", new Dictionary<string, object>
{
{ "accountBook", AccountBook },
{ "subSystemId", SubSystemId },
{ "userIdSha256", UserIdSha256 },
{ "userNameSha256", UserNameSha256 },
{ "databaseScopeFingerprint",
DatabaseScopeFingerprint },
{ "isAdministrator", IsAdministrator }
}
},
{ "sourceCommit", SourceCommit },
{ "packageSha256", PackageSha256 },
{ "runtimeConfigurationSha256", RuntimeConfigurationSha256 },
{ "runtimeCli", new Dictionary<string, object>
{
{ "fileName", "lserp-agent-cli.exe" },
{ "version", RuntimeCliVersion },
{ "sha256", RuntimeCliSha256 },
{ "signerThumbprint", RuntimeCliSignerThumbprint },
{ "requiresElevation", false },
{ "bridgeOnly", true },
{ "databaseDirectAccess", false },
{ "sessionSource", "current_logged_in_erp_process" }
}
},
{ "uatAuthorizationSourceSha256", UatAuthorizationSourceSha256 },
{ "uatAuthorizationContentSha256", UatAuthorizationContentSha256 },
{ "uatAuthorizationIdSha256", UatAuthorizationIdSha256 },
{ "environmentId", EnvironmentId },
{ "testedAtUtc", TestedAtUtc },
{ "testedBy", TestedBy },
{ "caseCount", CaseCount },
{ "verified", true },
{ "registrationReady", false }
};
}
}
/// <summary>
/// 验证客户 Windows/SQL Server 写链路报告。报告只保存不可逆标识哈希,
/// 但必须逐项证明权限、确认、事务、回滚、幂等、运行时复核和审计行为。
/// </summary>
public static class WorkflowWriteIntegrationEvidenceVerifier
{
private const int MaximumBytes = 4 * 1024 * 1024;
private static readonly Regex SafeModule = new Regex(
@"^[A-Za-z0-9_.:\-]{1,64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex SafeIdentifier = new Regex(
@"^[A-Za-z0-9_.:\-]{1,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 Sha256Pattern = new Regex(
@"^[a-f0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex CommitPattern = new Regex(
@"^[a-f0-9]{40}$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex RuntimeCliVersionPattern = 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 string[] PurchaseCases =
{
"purchase_unique_match_commit",
"purchase_ambiguous_match_blocked",
"purchase_overallocation_blocked",
"purchase_permission_denied",
"purchase_database_permission_recheck_denied",
"purchase_currency_field_missing_blocked",
"purchase_currency_crosswalk_unapproved_blocked",
"purchase_row_scope_denied",
"purchase_runtime_recheck_blocked",
"purchase_transaction_rollback",
"purchase_idempotency_replay",
"purchase_idempotency_conflict",
"purchase_audit_correlated"
};
private static readonly string[] LeaveCases =
{
"leave_natural_language_resolution",
"leave_multi_day_calendar_resolution",
"leave_resolution_proof_bypass_blocked",
"leave_ambiguous_type_blocked",
"leave_ambiguous_flow_type_blocked",
"leave_time_segment_required_blocked",
"leave_local_time_zone_rejected",
"leave_other_employee_denied",
"leave_permission_denied",
"leave_database_permission_recheck_denied",
"leave_create_draft_commit",
"leave_submit_separate_confirmation",
"leave_overlap_blocked",
"leave_stale_flow_type_blocked",
"leave_runtime_recheck_blocked",
"leave_transaction_rollback",
"leave_idempotency_replay",
"leave_idempotency_conflict",
"leave_audit_correlated"
};
private static readonly IDictionary<string, string> ExpectedResultCodes =
new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "purchase_unique_match_commit", "purchase_document_created" },
{ "purchase_ambiguous_match_blocked", "purchase_match_invalid" },
{ "purchase_overallocation_blocked", "purchase_match_invalid" },
{ "purchase_permission_denied", "command_access_denied" },
{ "purchase_database_permission_recheck_denied", "purchase_write_permission_denied" },
{ "purchase_currency_field_missing_blocked", "purchase_currency_field_not_configured" },
{ "purchase_currency_crosswalk_unapproved_blocked", "purchase_currency_crosswalk_not_approved" },
{ "purchase_row_scope_denied", "purchase_row_scope_denied" },
{ "purchase_runtime_recheck_blocked", "purchase_source_changed" },
{ "purchase_transaction_rollback", "purchase_legacy_create_failed" },
{ "purchase_idempotency_replay", "purchase_document_created" },
{ "purchase_idempotency_conflict", "idempotency_key_conflict" },
{ "purchase_audit_correlated", "purchase_document_created" },
{ "leave_natural_language_resolution", "leave_intent_resolved" },
{ "leave_multi_day_calendar_resolution", "leave_intent_resolved" },
{ "leave_resolution_proof_bypass_blocked", "leave_resolution_invalid" },
{ "leave_ambiguous_type_blocked", "leave_resolution_invalid" },
{ "leave_ambiguous_flow_type_blocked", "leave_resolution_invalid" },
{ "leave_time_segment_required_blocked", "leave_resolution_invalid" },
{ "leave_local_time_zone_rejected", "input_schema_violation" },
{ "leave_other_employee_denied", "leave_resolution_invalid" },
{ "leave_permission_denied", "command_access_denied" },
{ "leave_database_permission_recheck_denied", "leave_write_permission_denied" },
{ "leave_create_draft_commit", "leave_draft_created" },
{ "leave_submit_separate_confirmation", "leave_submitted" },
{ "leave_overlap_blocked", "leave_request_invalid" },
{ "leave_stale_flow_type_blocked", "leave_request_changed" },
{ "leave_runtime_recheck_blocked", "leave_request_changed" },
{ "leave_transaction_rollback", "leave_legacy_create_failed" },
{ "leave_idempotency_replay", "leave_draft_created" },
{ "leave_idempotency_conflict", "idempotency_key_conflict" },
{ "leave_audit_correlated", "leave_draft_created" }
};
private static readonly IDictionary<string, string> ExpectedIssueCodes =
new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "leave_ambiguous_type_blocked", "leave_type_ambiguous" },
{ "leave_ambiguous_flow_type_blocked", "leave_flow_type_ambiguous" },
{ "leave_time_segment_required_blocked", "leave_time_segment_required" },
{ "leave_other_employee_denied", "leave_employee_reference_unsupported" }
};
internal static string ExpectedResultCodeForCase(string caseCode)
{
string value;
return !string.IsNullOrWhiteSpace(caseCode)
&& ExpectedResultCodes.TryGetValue(caseCode, out value)
? value
: null;
}
internal static string ExpectedIssueCodeForCase(string caseCode)
{
string value;
return !string.IsNullOrWhiteSpace(caseCode)
&& ExpectedIssueCodes.TryGetValue(caseCode, out value)
? value
: null;
}
internal static string ExpectedCommandNameForCase(string caseCode)
{
if (PurchaseCases.Contains(caseCode, StringComparer.Ordinal))
return "purchase.invoice.create";
switch (caseCode)
{
case "leave_natural_language_resolution":
case "leave_multi_day_calendar_resolution":
case "leave_ambiguous_type_blocked":
case "leave_ambiguous_flow_type_blocked":
case "leave_time_segment_required_blocked":
case "leave_other_employee_denied":
return "hr.leave.resolve";
case "leave_submit_separate_confirmation":
return "hr.leave.submit";
default:
return LeaveCases.Contains(caseCode, StringComparer.Ordinal)
? "hr.leave.create"
: null;
}
}
internal static void ValidateProjectedCase(JObject item, DateTime nowUtc)
{
string caseCode = item == null
? null
: Convert.ToString(item["caseCode"], CultureInfo.InvariantCulture);
string[] required = PurchaseCases.Contains(
caseCode,
StringComparer.Ordinal)
? PurchaseCases
: LeaveCases.Contains(caseCode, StringComparer.Ordinal)
? LeaveCases
: null;
if (required == null)
throw Error(
"write_integration_evidence_case_invalid",
"写集成证据包含未知用例。");
ValidateCase(
item,
new HashSet<string>(StringComparer.Ordinal),
required,
nowUtc.ToUniversalTime(),
new CaseScopeValidation(
null, null, null, null, null, null,
null, null, null));
}
internal static string[] RequiredCaseCodesForWorkflow(string workflow)
{
string[] source = workflow == "purchase"
? PurchaseCases
: workflow == "leave"
? LeaveCases
: null;
if (source == null)
throw Error(
"write_integration_evidence_schema_invalid",
"写集成证据工作流无效。");
return source.ToArray();
}
internal static void ValidateProjectedCaseSet(
string workflow,
JArray cases,
DateTime testedAtUtc)
{
ValidateProjectedCaseSet(
workflow,
cases,
testedAtUtc,
null,
null,
null,
null,
null,
null,
null,
null,
null);
}
private static void ValidateProjectedCaseSet(
string workflow,
JArray cases,
DateTime testedAtUtc,
string expectedModuleCode,
string expectedAccountBook,
string expectedSubSystemId,
string expectedUatAuthorizationSourceSha256,
string expectedUatAuthorizationContentSha256,
string expectedUatAuthorizationIdSha256,
string expectedRuntimeCliVersion,
string expectedRuntimeCliSha256,
string expectedRuntimeCliSignerThumbprint)
{
string[] requiredCases = RequiredCaseCodesForWorkflow(workflow);
if (cases == null || cases.Count != requiredCases.Length)
throw Error(
"write_integration_evidence_coverage_incomplete",
"写集成证据没有覆盖工作流全部必测用例。");
HashSet<string> observed = new HashSet<string>(StringComparer.Ordinal);
Dictionary<string, ValidatedCaseEvidence> validatedCases =
new Dictionary<string, ValidatedCaseEvidence>(StringComparer.Ordinal);
CaseScopeValidation scope = new CaseScopeValidation(
expectedModuleCode,
expectedAccountBook == null ? null : Sha256(expectedAccountBook),
expectedSubSystemId == null ? null : Sha256(expectedSubSystemId),
expectedUatAuthorizationSourceSha256,
expectedUatAuthorizationContentSha256,
expectedUatAuthorizationIdSha256,
expectedRuntimeCliVersion,
expectedRuntimeCliSha256,
expectedRuntimeCliSignerThumbprint);
foreach (JToken token in cases)
{
JObject item = token as JObject;
if (item == null)
throw Error(
"write_integration_evidence_case_invalid",
"写集成证据用例必须是对象。");
ValidatedCaseEvidence validated = ValidateCase(
item,
observed,
requiredCases,
testedAtUtc.ToUniversalTime(),
scope);
validatedCases.Add(validated.Code, validated);
}
if (observed.Count != requiredCases.Length
|| requiredCases.Any(item => !observed.Contains(item)))
throw Error(
"write_integration_evidence_coverage_incomplete",
"写集成证据没有覆盖工作流全部必测用例。");
scope.ValidateComplete();
ValidateCaseRelationships(workflow, validatedCases);
}
public static WorkflowWriteIntegrationEvidenceResult VerifyFile(
string path,
string expectedWorkflow,
string expectedModuleCode,
string expectedAccountBook,
string expectedSubSystemId,
string expectedRuntimeConfigurationSha256,
string expectedSourceCommit,
string expectedPackageSha256,
DateTime nowUtc)
{
JObject root = LoadStrict(path);
EnsureExact(root, "schemaVersion", "contentSha256", "content");
if (RequiredString(root, "schemaVersion", 1, 16) != "1.6")
throw Error("write_integration_evidence_schema_invalid", "写集成证据版本无效。");
string contentHash = RequiredHash(root, "contentSha256");
JObject content = RequiredObject(root, "content");
EnsureExact(content,
"evidenceType", "workflow", "moduleCode", "erpScope", "sourceCommit",
"packageSha256", "runtimeConfigurationSha256", "runtimeCli",
"environmentId",
"uatAuthorizationSourceSha256",
"uatAuthorizationContentSha256",
"uatAuthorizationIdSha256",
"testedAtUtc", "testedBy", "cases");
if (RequiredString(content, "evidenceType", 1, 64) != "workflow_write_integration")
throw Error("write_integration_evidence_schema_invalid", "写集成证据类型无效。");
string canonical = content.ToString(Formatting.None);
if (!string.Equals(contentHash, Sha256(canonical), StringComparison.Ordinal))
throw Error("write_integration_evidence_hash_mismatch", "写集成证据内容哈希不一致。");
string workflow = RequiredString(content, "workflow", 1, 32);
string moduleCode = RequiredString(content, "moduleCode", 1, 64);
if ((workflow != "purchase" && workflow != "leave") || !SafeModule.IsMatch(moduleCode))
throw Error("write_integration_evidence_schema_invalid", "写集成证据工作流或模块编号无效。");
JObject scope = RequiredObject(content, "erpScope");
EnsureExact(scope, "accountBook", "subSystemId");
string accountBook = RequiredString(scope, "accountBook", 1, 128);
string subSystemId = RequiredString(scope, "subSystemId", 1, 128);
string sourceCommit = RequiredString(content, "sourceCommit", 40, 40).ToLowerInvariant();
string packageHash = RequiredHash(content, "packageSha256");
string runtimeHash = RequiredHash(content, "runtimeConfigurationSha256");
JObject runtimeCli = RequiredObject(content, "runtimeCli");
EnsureExact(
runtimeCli,
"fileName", "version", "sha256", "signerThumbprint",
"requiresElevation", "bridgeOnly", "databaseDirectAccess",
"sessionSource");
string runtimeCliVersion = RequiredString(
runtimeCli,
"version",
5,
14);
string runtimeCliSha256 = RequiredHash(runtimeCli, "sha256");
string runtimeCliSigner = RequiredString(
runtimeCli,
"signerThumbprint",
40,
40);
if (RequiredString(runtimeCli, "fileName", 19, 19)
!= "lserp-agent-cli.exe"
|| !RuntimeCliVersionPattern.IsMatch(runtimeCliVersion)
|| !SignerThumbprintPattern.IsMatch(runtimeCliSigner)
|| RequiredBoolean(runtimeCli, "requiresElevation")
|| !RequiredBoolean(runtimeCli, "bridgeOnly")
|| RequiredBoolean(runtimeCli, "databaseDirectAccess")
|| RequiredString(runtimeCli, "sessionSource", 29, 29)
!= "current_logged_in_erp_process")
throw Error(
"write_integration_evidence_runtime_cli_invalid",
"写集成证据运行 CLI 身份或本地会话边界无效。");
string uatSourceHash = RequiredHash(
content,
"uatAuthorizationSourceSha256");
string uatContentHash = RequiredHash(
content,
"uatAuthorizationContentSha256");
string uatAuthorizationIdHash = RequiredHash(
content,
"uatAuthorizationIdSha256");
if (!CommitPattern.IsMatch(sourceCommit))
throw Error("write_integration_evidence_schema_invalid", "写集成证据源码提交无效。");
string environmentId = RequiredString(content, "environmentId", 8, 128);
string testedBy = RequiredString(content, "testedBy", 1, 128);
if (!SafeIdentifier.IsMatch(environmentId) || !SafeIdentifier.IsMatch(testedBy))
throw Error("write_integration_evidence_schema_invalid", "验收环境或验证人标识无效。");
DateTime testedAt = RequiredUtc(content, "testedAtUtc");
nowUtc = nowUtc.Kind == DateTimeKind.Utc ? nowUtc : nowUtc.ToUniversalTime();
if (testedAt > nowUtc.AddMinutes(5) || testedAt < nowUtc.AddDays(-30))
throw Error("write_integration_evidence_expired", "写集成证据必须在最近 30 天内生成。");
RequireExpected(workflow, expectedWorkflow, "workflow");
RequireExpected(moduleCode, expectedModuleCode, "moduleCode");
RequireExpected(accountBook, expectedAccountBook, "accountBook");
RequireExpected(subSystemId, expectedSubSystemId, "subSystemId");
if (!string.IsNullOrWhiteSpace(expectedRuntimeConfigurationSha256)
&& !string.Equals(runtimeHash, expectedRuntimeConfigurationSha256,
StringComparison.OrdinalIgnoreCase))
throw Error("write_integration_evidence_scope_mismatch", "写集成证据与运行时配置哈希不一致。");
if (!string.IsNullOrWhiteSpace(expectedSourceCommit)
&& !string.Equals(sourceCommit, expectedSourceCommit,
StringComparison.OrdinalIgnoreCase))
throw Error("write_integration_evidence_scope_mismatch", "写集成证据与发布源码提交不一致。");
if (!string.IsNullOrWhiteSpace(expectedPackageSha256)
&& !string.Equals(packageHash, expectedPackageSha256,
StringComparison.OrdinalIgnoreCase))
throw Error("write_integration_evidence_scope_mismatch", "写集成证据与商用包哈希不一致。");
JArray cases = content["cases"] as JArray;
if (cases == null || cases.Count <= 0 || cases.Count > 32)
throw Error("write_integration_evidence_schema_invalid", "写集成证据用例集合无效。");
ValidateProjectedCaseSet(
workflow,
cases,
testedAt,
moduleCode,
accountBook,
subSystemId,
uatSourceHash,
uatContentHash,
uatAuthorizationIdHash,
runtimeCliVersion,
runtimeCliSha256,
runtimeCliSigner);
Dictionary<string, string> uatTokenSha256ByCase = cases
.OfType<JObject>()
.ToDictionary(
item => RequiredString(item, "caseCode", 1, 128),
item => RequiredHash(item, "uatTokenSha256"),
StringComparer.Ordinal);
JObject firstCase = (JObject)cases[0];
return new WorkflowWriteIntegrationEvidenceResult
{
ContentSha256 = contentHash,
Workflow = workflow,
ModuleCode = moduleCode,
AccountBook = accountBook,
SubSystemId = subSystemId,
UserIdSha256 = RequiredHash(firstCase, "userIdSha256"),
UserNameSha256 = RequiredHash(firstCase, "userNameSha256"),
DatabaseScopeFingerprint = RequiredHash(
firstCase,
"databaseScopeFingerprint"),
IsAdministrator = RequiredBoolean(
firstCase,
"isAdministrator"),
SourceCommit = sourceCommit,
PackageSha256 = packageHash,
RuntimeConfigurationSha256 = runtimeHash,
RuntimeCliVersion = runtimeCliVersion,
RuntimeCliSha256 = runtimeCliSha256,
RuntimeCliSignerThumbprint = runtimeCliSigner,
UatAuthorizationSourceSha256 = uatSourceHash,
UatAuthorizationContentSha256 = uatContentHash,
UatAuthorizationIdSha256 = uatAuthorizationIdHash,
EnvironmentId = environmentId,
TestedAtUtc = testedAt,
TestedBy = testedBy,
CaseCount = cases.Count,
UatTokenSha256ByCase = uatTokenSha256ByCase
};
}
private static ValidatedCaseEvidence ValidateCase(
JObject item,
ISet<string> observed,
IEnumerable<string> requiredCases,
DateTime testedAtUtc,
CaseScopeValidation scope)
{
EnsureExact(item,
"caseCode", "uatAuthorizationSourceSha256",
"uatAuthorizationContentSha256", "uatAuthorizationIdSha256",
"uatTokenSha256", "runtimeCliVersion", "runtimeCliSha256",
"runtimeCliSignerThumbprint", "passed", "correlationId",
"contextCorrelationBound", "commandName",
"planCommandVersion", "planModuleCode", "planRisk",
"accountBookSha256", "subSystemIdSha256", "userIdSha256",
"userNameSha256", "databaseScopeFingerprint",
"isAdministrator",
"inputFingerprintSha256",
"planFingerprintSha256", "resultCode", "issueCode", "recordIdSha256",
"transactionEvidenceIdSha256", "businessAuditIdSha256",
"idempotencyKeySha256", "businessMutationCount", "replayed",
"nativeConfirmationObserved", "auditEventCount",
"sourceDocumentSetSha256",
"sourceDocumentPreprocessContracts",
"sourceDocumentInputFingerprintBound",
"sourceDocumentWritePayloadBound", "sourceDocumentAuditCount",
"observedAtUtc");
string code = RequiredString(item, "caseCode", 1, 128);
if (!SafeCode.IsMatch(code) || !requiredCases.Contains(code) || !observed.Add(code))
throw Error("write_integration_evidence_case_invalid", "写集成证据包含未知或重复用例。");
if (!RequiredBoolean(item, "passed"))
throw Error("write_integration_evidence_case_failed", "写集成证据包含未通过用例。");
if (!RequiredBoolean(item, "contextCorrelationBound"))
throw Error(
"write_integration_evidence_scope_mismatch",
"写集成证据用例没有把 ERP 上下文绑定到计划关联 ID。");
string correlation = RequiredString(item, "correlationId", 8, 128);
string commandName = RequiredString(item, "commandName", 1, 128);
string expectedCommand = ExpectedCommandNameForCase(code);
string resultCode = RequiredString(item, "resultCode", 1, 128);
if (!SafeIdentifier.IsMatch(correlation)
|| !SafeIdentifier.IsMatch(commandName)
|| !string.Equals(
commandName,
expectedCommand,
StringComparison.Ordinal)
|| !SafeCode.IsMatch(resultCode))
throw Error("write_integration_evidence_case_invalid", "写集成证据关联 ID 或结果码无效。");
string expectedResultCode;
if (!ExpectedResultCodes.TryGetValue(code, out expectedResultCode)
|| !string.Equals(resultCode, expectedResultCode, StringComparison.Ordinal))
throw Error("write_integration_evidence_case_invalid", "写集成证据结果码与必测场景不一致。");
string issueCode = NullableCode(item, "issueCode");
string expectedIssueCode;
if (ExpectedIssueCodes.TryGetValue(code, out expectedIssueCode))
{
if (!string.Equals(issueCode, expectedIssueCode, StringComparison.Ordinal))
throw Error("write_integration_evidence_case_invalid", "写集成证据没有记录精确的解析阻断原因。");
}
else if (issueCode != null)
throw Error("write_integration_evidence_case_invalid", "写集成证据在非解析场景携带了未知问题码。");
RequiredHash(item, "inputFingerprintSha256");
string plan = NullableHash(item, "planFingerprintSha256");
string planVersion = NullableIdentifier(
item,
"planCommandVersion",
3,
16);
string planModule = NullableIdentifier(
item,
"planModuleCode",
1,
64);
string planRisk = NullableIdentifier(item, "planRisk", 1, 16);
bool hasPlanContract = planVersion != null
|| planModule != null
|| planRisk != null;
if (hasPlanContract
&& (planVersion == null || planModule == null || planRisk == null))
throw Error(
"write_integration_evidence_case_invalid",
"写集成证据计划合同字段不完整。");
if ((plan == null) != !hasPlanContract)
throw Error(
"write_integration_evidence_case_invalid",
"写集成证据计划指纹与计划合同不一致。");
string expectedVersion;
string expectedRisk;
ExpectedPlanContract(
commandName,
out expectedVersion,
out expectedRisk);
if (hasPlanContract
&& (!SafeModule.IsMatch(planModule)
|| !string.Equals(
planVersion,
expectedVersion,
StringComparison.Ordinal)
|| !string.Equals(
planRisk,
expectedRisk,
StringComparison.Ordinal)))
throw Error(
"write_integration_evidence_case_invalid",
"写集成证据计划命令版本、模块或风险合同无效。");
bool planRequired = PlanRequiredForCase(code);
if (planRequired != hasPlanContract)
throw Error(
"write_integration_evidence_case_invalid",
"写集成证据的计划阶段与固定场景不一致。");
string accountBookHash = RequiredHash(item, "accountBookSha256");
string subSystemHash = RequiredHash(item, "subSystemIdSha256");
string userHash = RequiredHash(item, "userIdSha256");
string userNameHash = RequiredHash(item, "userNameSha256");
string databaseScopeFingerprint = RequiredHash(
item,
"databaseScopeFingerprint");
bool isAdministrator = RequiredBoolean(item, "isAdministrator");
string uatSourceHash = RequiredHash(
item,
"uatAuthorizationSourceSha256");
string uatContentHash = RequiredHash(
item,
"uatAuthorizationContentSha256");
string uatAuthorizationIdHash = RequiredHash(
item,
"uatAuthorizationIdSha256");
string uatTokenHash = RequiredHash(item, "uatTokenSha256");
string runtimeCliVersion = RequiredString(
item,
"runtimeCliVersion",
5,
14);
string runtimeCliSha256 = RequiredHash(item, "runtimeCliSha256");
string runtimeCliSigner = RequiredString(
item,
"runtimeCliSignerThumbprint",
40,
40);
if (!RuntimeCliVersionPattern.IsMatch(runtimeCliVersion)
|| !SignerThumbprintPattern.IsMatch(runtimeCliSigner))
throw Error(
"write_integration_evidence_runtime_cli_invalid",
"写集成证据用例运行 CLI 身份无效。");
scope.Observe(
planModule,
accountBookHash,
subSystemHash,
userHash,
userNameHash,
databaseScopeFingerprint,
isAdministrator,
uatSourceHash,
uatContentHash,
uatAuthorizationIdHash,
uatTokenHash,
runtimeCliVersion,
runtimeCliSha256,
runtimeCliSigner);
string record = NullableHash(item, "recordIdSha256");
string transaction = NullableHash(item, "transactionEvidenceIdSha256");
string audit = NullableHash(item, "businessAuditIdSha256");
string idempotency = NullableHash(item, "idempotencyKeySha256");
int mutationCount = RequiredInteger(item, "businessMutationCount", 0, 1000);
bool replayed = RequiredBoolean(item, "replayed");
bool confirmed = RequiredBoolean(item, "nativeConfirmationObserved");
int auditEvents = RequiredInteger(item, "auditEventCount", 1, 1000);
string sourceDocumentSet = NullableHash(item, "sourceDocumentSetSha256");
string[] sourcePreprocessContracts =
RequiredSourcePreprocessContracts(
item,
"sourceDocumentPreprocessContracts");
bool sourceInputBound = RequiredBoolean(
item, "sourceDocumentInputFingerprintBound");
bool sourcePayloadBound = RequiredBoolean(
item, "sourceDocumentWritePayloadBound");
int sourceAuditCount = RequiredInteger(
item, "sourceDocumentAuditCount", 0, 1000);
DateTime observedAtUtc = RequiredUtc(item, "observedAtUtc");
if (observedAtUtc > testedAtUtc.AddMinutes(5)
|| observedAtUtc < testedAtUtc.AddDays(-30))
throw Error(
"write_integration_evidence_case_invalid",
"写集成证据用例观察时间必须位于报告生成前 30 天内。");
bool commit = code == "purchase_unique_match_commit"
|| code == "leave_create_draft_commit"
|| code == "leave_submit_separate_confirmation";
bool replay = code == "purchase_idempotency_replay"
|| code == "leave_idempotency_replay";
bool idempotencyConflict = code == "purchase_idempotency_conflict"
|| code == "leave_idempotency_conflict";
bool auditCase = code == "purchase_audit_correlated"
|| code == "leave_audit_correlated";
bool sourceProofRequired = code == "purchase_unique_match_commit"
|| code == "purchase_idempotency_replay"
|| code == "purchase_audit_correlated";
bool postConfirmationBlock = code == "purchase_database_permission_recheck_denied"
|| code == "purchase_currency_field_missing_blocked"
|| code == "purchase_currency_crosswalk_unapproved_blocked"
|| code == "purchase_row_scope_denied"
|| code == "purchase_runtime_recheck_blocked"
|| code == "purchase_transaction_rollback"
|| code == "leave_database_permission_recheck_denied"
|| code == "leave_stale_flow_type_blocked"
|| code == "leave_runtime_recheck_blocked"
|| code == "leave_transaction_rollback";
if (commit && (plan == null || record == null || transaction == null || audit == null
|| idempotency == null || mutationCount < 1 || replayed || !confirmed
|| auditEvents < 2))
throw Error("write_integration_evidence_case_invalid", "成功写用例缺少事务、幂等、确认或审计证据。");
if (replay && (plan == null || record == null || transaction == null
|| audit == null || idempotency == null || mutationCount != 0
|| !replayed || !confirmed || auditEvents < 2))
throw Error("write_integration_evidence_case_invalid", "幂等重放用例证据无效。");
if (auditCase && (plan == null || record == null || transaction == null
|| audit == null || idempotency == null || mutationCount != 0
|| replayed || !confirmed || auditEvents < 2))
throw Error("write_integration_evidence_case_invalid", "审计关联用例证据无效。");
if (!commit && !replay && !auditCase && (mutationCount != 0 || replayed))
throw Error("write_integration_evidence_case_invalid", "阻断或只读用例不得产生业务写入或重放结果。");
if (!commit && !replay && !auditCase
&& (record != null || transaction != null || audit != null))
throw Error("write_integration_evidence_case_invalid", "阻断或只读用例不得声明业务记录、事务或业务审计 ID。");
if (postConfirmationBlock
&& (plan == null || idempotency == null || !confirmed))
throw Error("write_integration_evidence_case_invalid", "运行时阻断用例缺少计划、幂等键或原生确认证据。");
if (idempotencyConflict
&& (plan == null || idempotency == null || !confirmed
|| auditEvents < 2))
throw Error("write_integration_evidence_case_invalid", "幂等冲突用例缺少计划、幂等键、原生确认或完整命令审计证据。");
if (!postConfirmationBlock && !commit && !replay
&& !idempotencyConflict && !auditCase && confirmed)
throw Error("write_integration_evidence_case_invalid", "确认前阻断场景不应声明原生确认已发生。");
if (sourceProofRequired
&& (sourceDocumentSet == null || !sourceInputBound
|| !sourcePayloadBound || sourceAuditCount < 1
|| !sourcePreprocessContracts.Contains(
PurchaseSourceDocumentContract.PdfPreprocessContract,
StringComparer.Ordinal)))
throw Error("write_integration_evidence_case_invalid", "采购来源文件没有同时绑定电子 PDF 处理契约、输入指纹、写入载荷和审计证据。");
if (!sourceProofRequired
&& (sourceDocumentSet != null || sourceInputBound
|| sourcePayloadBound || sourceAuditCount != 0
|| sourcePreprocessContracts.Length != 0))
throw Error("write_integration_evidence_case_invalid", "非来源文件证明场景携带了不一致的来源证据。");
return new ValidatedCaseEvidence
{
Code = code,
CorrelationId = correlation,
InputFingerprintSha256 = RequiredHash(item, "inputFingerprintSha256"),
PlanFingerprintSha256 = plan,
RecordIdSha256 = record,
TransactionEvidenceIdSha256 = transaction,
BusinessAuditIdSha256 = audit,
IdempotencyKeySha256 = idempotency,
SourceDocumentSetSha256 = sourceDocumentSet,
SourceDocumentPreprocessContracts =
sourcePreprocessContracts,
SourceDocumentAuditCount = sourceAuditCount
};
}
private static bool PlanRequiredForCase(string caseCode)
{
return caseCode != "purchase_permission_denied"
&& caseCode != "leave_permission_denied"
&& caseCode != "leave_local_time_zone_rejected";
}
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_integration_evidence_case_invalid",
"写集成证据命令不属于固定工作流合同。");
}
}
private static void ValidateCaseRelationships(
string workflow,
IDictionary<string, ValidatedCaseEvidence> cases)
{
if (workflow == "purchase")
{
ValidatedCaseEvidence commit = cases["purchase_unique_match_commit"];
ValidatedCaseEvidence replay = cases["purchase_idempotency_replay"];
ValidatedCaseEvidence conflict = cases["purchase_idempotency_conflict"];
ValidatedCaseEvidence audit = cases["purchase_audit_correlated"];
RequireSameBusinessResult(commit, replay, true);
RequirePresentAndDifferent(
commit.PlanFingerprintSha256,
replay.PlanFingerprintSha256);
RequireSameBusinessResult(commit, audit, true);
RequireSame(
commit.PlanFingerprintSha256,
audit.PlanFingerprintSha256);
RequireSame(commit.CorrelationId, audit.CorrelationId);
RequireDifferent(commit.CorrelationId, replay.CorrelationId);
RequireSame(commit.IdempotencyKeySha256, conflict.IdempotencyKeySha256);
RequireDifferent(commit.CorrelationId, conflict.CorrelationId);
RequireDifferent(
commit.InputFingerprintSha256,
conflict.InputFingerprintSha256);
RequirePresentAndDifferent(
commit.PlanFingerprintSha256,
conflict.PlanFingerprintSha256);
return;
}
ValidatedCaseEvidence create = cases["leave_create_draft_commit"];
ValidatedCaseEvidence replayLeave = cases["leave_idempotency_replay"];
ValidatedCaseEvidence conflictLeave = cases["leave_idempotency_conflict"];
ValidatedCaseEvidence auditLeave = cases["leave_audit_correlated"];
ValidatedCaseEvidence submit = cases["leave_submit_separate_confirmation"];
RequireSameBusinessResult(create, replayLeave, false);
RequirePresentAndDifferent(
create.PlanFingerprintSha256,
replayLeave.PlanFingerprintSha256);
RequireSameBusinessResult(create, auditLeave, false);
RequireSame(
create.PlanFingerprintSha256,
auditLeave.PlanFingerprintSha256);
RequireSame(create.CorrelationId, auditLeave.CorrelationId);
RequireDifferent(create.CorrelationId, replayLeave.CorrelationId);
RequireSame(create.IdempotencyKeySha256, conflictLeave.IdempotencyKeySha256);
RequireDifferent(create.CorrelationId, conflictLeave.CorrelationId);
RequireDifferent(
create.InputFingerprintSha256,
conflictLeave.InputFingerprintSha256);
RequirePresentAndDifferent(
create.PlanFingerprintSha256,
conflictLeave.PlanFingerprintSha256);
RequireSame(create.RecordIdSha256, submit.RecordIdSha256);
RequireDifferent(create.CorrelationId, submit.CorrelationId);
RequireDifferent(
create.InputFingerprintSha256,
submit.InputFingerprintSha256);
RequirePresentAndDifferent(
create.PlanFingerprintSha256,
submit.PlanFingerprintSha256);
RequireDifferent(
create.TransactionEvidenceIdSha256,
submit.TransactionEvidenceIdSha256);
RequireDifferent(
create.BusinessAuditIdSha256,
submit.BusinessAuditIdSha256);
RequireDifferent(
create.IdempotencyKeySha256,
submit.IdempotencyKeySha256);
}
private static void RequireSameBusinessResult(
ValidatedCaseEvidence expected,
ValidatedCaseEvidence observed,
bool requireSourceDocuments)
{
RequireSame(
expected.InputFingerprintSha256,
observed.InputFingerprintSha256);
RequireSame(expected.RecordIdSha256, observed.RecordIdSha256);
RequireSame(
expected.TransactionEvidenceIdSha256,
observed.TransactionEvidenceIdSha256);
RequireSame(
expected.BusinessAuditIdSha256,
observed.BusinessAuditIdSha256);
RequireSame(
expected.IdempotencyKeySha256,
observed.IdempotencyKeySha256);
if (!requireSourceDocuments) return;
RequireSame(
expected.SourceDocumentSetSha256,
observed.SourceDocumentSetSha256);
if (!expected.SourceDocumentPreprocessContracts.SequenceEqual(
observed.SourceDocumentPreprocessContracts,
StringComparer.Ordinal))
throw RelationshipInvalid();
if (expected.SourceDocumentAuditCount != observed.SourceDocumentAuditCount)
throw RelationshipInvalid();
}
private static void RequireSame(string left, string right)
{
if (string.IsNullOrWhiteSpace(left)
|| !string.Equals(left, right, StringComparison.Ordinal))
throw RelationshipInvalid();
}
private static void RequireDifferent(string left, string right)
{
if (string.IsNullOrWhiteSpace(left)
|| string.IsNullOrWhiteSpace(right)
|| string.Equals(left, right, StringComparison.Ordinal))
throw RelationshipInvalid();
}
private static void RequirePresentAndDifferent(string left, string right)
{
RequireDifferent(left, right);
}
private static CommandKernelException RelationshipInvalid()
{
return Error(
"write_integration_evidence_relationship_invalid",
"写集成证据的提交、重放、冲突、审计或独立提交记录没有关联到同一业务事实。");
}
private sealed class ValidatedCaseEvidence
{
public string Code { get; set; }
public string CorrelationId { get; set; }
public string InputFingerprintSha256 { get; set; }
public string PlanFingerprintSha256 { get; set; }
public string RecordIdSha256 { get; set; }
public string TransactionEvidenceIdSha256 { get; set; }
public string BusinessAuditIdSha256 { get; set; }
public string IdempotencyKeySha256 { get; set; }
public string SourceDocumentSetSha256 { get; set; }
public string[] SourceDocumentPreprocessContracts { get; set; }
public int SourceDocumentAuditCount { get; set; }
}
private sealed class CaseScopeValidation
{
private readonly string _expectedModuleCode;
private readonly string _expectedAccountBookSha256;
private readonly string _expectedSubSystemIdSha256;
private readonly string _expectedUatAuthorizationSourceSha256;
private readonly string _expectedUatAuthorizationContentSha256;
private readonly string _expectedUatAuthorizationIdSha256;
private readonly string _expectedRuntimeCliVersion;
private readonly string _expectedRuntimeCliSha256;
private readonly string _expectedRuntimeCliSignerThumbprint;
private readonly HashSet<string> _modules = new HashSet<string>(
StringComparer.Ordinal);
private readonly HashSet<string> _accountBooks = new HashSet<string>(
StringComparer.Ordinal);
private readonly HashSet<string> _subSystems = new HashSet<string>(
StringComparer.Ordinal);
private readonly HashSet<string> _users = new HashSet<string>(
StringComparer.Ordinal);
private readonly HashSet<string> _userNames = new HashSet<string>(
StringComparer.Ordinal);
private readonly HashSet<string> _databaseScopes = new HashSet<string>(
StringComparer.Ordinal);
private readonly HashSet<bool> _administratorStates =
new HashSet<bool>();
private readonly HashSet<string> _uatSources = new HashSet<string>(
StringComparer.Ordinal);
private readonly HashSet<string> _uatContents = new HashSet<string>(
StringComparer.Ordinal);
private readonly HashSet<string> _uatAuthorizationIds =
new HashSet<string>(StringComparer.Ordinal);
private readonly HashSet<string> _uatTokens = new HashSet<string>(
StringComparer.Ordinal);
private readonly HashSet<string> _runtimeCliVersions =
new HashSet<string>(StringComparer.Ordinal);
private readonly HashSet<string> _runtimeCliHashes =
new HashSet<string>(StringComparer.Ordinal);
private readonly HashSet<string> _runtimeCliSigners =
new HashSet<string>(StringComparer.Ordinal);
public CaseScopeValidation(
string expectedModuleCode,
string expectedAccountBookSha256,
string expectedSubSystemIdSha256,
string expectedUatAuthorizationSourceSha256,
string expectedUatAuthorizationContentSha256,
string expectedUatAuthorizationIdSha256,
string expectedRuntimeCliVersion,
string expectedRuntimeCliSha256,
string expectedRuntimeCliSignerThumbprint)
{
_expectedModuleCode = expectedModuleCode;
_expectedAccountBookSha256 = expectedAccountBookSha256;
_expectedSubSystemIdSha256 = expectedSubSystemIdSha256;
_expectedUatAuthorizationSourceSha256 =
expectedUatAuthorizationSourceSha256;
_expectedUatAuthorizationContentSha256 =
expectedUatAuthorizationContentSha256;
_expectedUatAuthorizationIdSha256 =
expectedUatAuthorizationIdSha256;
_expectedRuntimeCliVersion = expectedRuntimeCliVersion;
_expectedRuntimeCliSha256 = expectedRuntimeCliSha256;
_expectedRuntimeCliSignerThumbprint =
expectedRuntimeCliSignerThumbprint;
}
public void Observe(
string moduleCode,
string accountBookSha256,
string subSystemIdSha256,
string userIdSha256,
string userNameSha256,
string databaseScopeFingerprint,
bool isAdministrator,
string uatAuthorizationSourceSha256,
string uatAuthorizationContentSha256,
string uatAuthorizationIdSha256,
string uatTokenSha256,
string runtimeCliVersion,
string runtimeCliSha256,
string runtimeCliSignerThumbprint)
{
if (moduleCode != null)
{
if (_expectedModuleCode != null
&& !string.Equals(
moduleCode,
_expectedModuleCode,
StringComparison.Ordinal))
throw Error(
"write_integration_evidence_scope_mismatch",
"写集成证据用例计划模块与报告模块不一致。");
_modules.Add(moduleCode);
}
if (_expectedAccountBookSha256 != null
&& !string.Equals(
accountBookSha256,
_expectedAccountBookSha256,
StringComparison.Ordinal))
throw Error(
"write_integration_evidence_scope_mismatch",
"写集成证据用例账套与报告账套不一致。");
if (_expectedSubSystemIdSha256 != null
&& !string.Equals(
subSystemIdSha256,
_expectedSubSystemIdSha256,
StringComparison.Ordinal))
throw Error(
"write_integration_evidence_scope_mismatch",
"写集成证据用例子系统与报告子系统不一致。");
if ((_expectedUatAuthorizationSourceSha256 != null
&& !string.Equals(
uatAuthorizationSourceSha256,
_expectedUatAuthorizationSourceSha256,
StringComparison.Ordinal))
|| (_expectedUatAuthorizationContentSha256 != null
&& !string.Equals(
uatAuthorizationContentSha256,
_expectedUatAuthorizationContentSha256,
StringComparison.Ordinal))
|| (_expectedUatAuthorizationIdSha256 != null
&& !string.Equals(
uatAuthorizationIdSha256,
_expectedUatAuthorizationIdSha256,
StringComparison.Ordinal)))
throw Error(
"write_integration_evidence_scope_mismatch",
"写集成证据用例与报告声明的短时 UAT 授权不一致。");
if ((_expectedRuntimeCliVersion != null
&& !string.Equals(
runtimeCliVersion,
_expectedRuntimeCliVersion,
StringComparison.Ordinal))
|| (_expectedRuntimeCliSha256 != null
&& !string.Equals(
runtimeCliSha256,
_expectedRuntimeCliSha256,
StringComparison.Ordinal))
|| (_expectedRuntimeCliSignerThumbprint != null
&& !string.Equals(
runtimeCliSignerThumbprint,
_expectedRuntimeCliSignerThumbprint,
StringComparison.Ordinal)))
throw Error(
"write_integration_evidence_runtime_cli_mismatch",
"写集成证据用例与报告声明的运行 CLI 不一致。");
_accountBooks.Add(accountBookSha256);
_subSystems.Add(subSystemIdSha256);
_users.Add(userIdSha256);
_userNames.Add(userNameSha256);
_databaseScopes.Add(databaseScopeFingerprint);
_administratorStates.Add(isAdministrator);
_uatSources.Add(uatAuthorizationSourceSha256);
_uatContents.Add(uatAuthorizationContentSha256);
_uatAuthorizationIds.Add(uatAuthorizationIdSha256);
_runtimeCliVersions.Add(runtimeCliVersion);
_runtimeCliHashes.Add(runtimeCliSha256);
_runtimeCliSigners.Add(runtimeCliSignerThumbprint);
if (!_uatTokens.Add(uatTokenSha256))
throw Error(
"write_integration_evidence_scope_mismatch",
"每个 UAT 用例必须绑定不同的短时用例令牌摘要。");
}
public void ValidateComplete()
{
if (_modules.Count != 1
|| _accountBooks.Count != 1
|| _subSystems.Count != 1
|| _users.Count != 1
|| _userNames.Count != 1
|| _databaseScopes.Count != 1
|| _administratorStates.Count != 1
|| _uatSources.Count != 1
|| _uatContents.Count != 1
|| _uatAuthorizationIds.Count != 1
|| _runtimeCliVersions.Count != 1
|| _runtimeCliHashes.Count != 1
|| _runtimeCliSigners.Count != 1)
throw Error(
"write_integration_evidence_scope_mismatch",
"写集成证据用例不是来自同一模块、账套、子系统、ERP 用户、数据库作用域、短时 UAT 授权和运行 CLI。");
}
}
private static JObject LoadStrict(string path)
{
if (string.IsNullOrWhiteSpace(path))
throw Error("write_integration_evidence_input_required", "请提供写集成证据文件。");
string full;
try { full = Path.GetFullPath(path); }
catch { throw Error("write_integration_evidence_input_invalid", "写集成证据路径无效。"); }
FileInfo file = new FileInfo(full);
if (!file.Exists) throw Error("write_integration_evidence_not_found", "写集成证据文件不存在。");
if (file.Length <= 0 || file.Length > MaximumBytes
|| (file.Attributes & FileAttributes.ReparsePoint) != 0)
throw Error("write_integration_evidence_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_integration_evidence_json_invalid", "写集成证据包含多个 JSON 根值。");
return value;
}
}
catch (CommandKernelException) { throw; }
catch { throw Error("write_integration_evidence_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_integration_evidence_schema_invalid", "写集成证据包含缺失或未知字段。");
}
private static JObject RequiredObject(JObject source, string name)
{
JObject value = source[name] as JObject;
if (value == null) throw Error("write_integration_evidence_schema_invalid", "写集成证据对象字段无效。");
return value;
}
private static string RequiredString(JObject source, string name, int minimum, int maximum)
{
JToken token = source[name];
if (token == null || token.Type != JTokenType.String)
throw Error("write_integration_evidence_schema_invalid", "写集成证据文本字段无效。");
string value = ((string)token).Trim();
if (value.Length < minimum || value.Length > maximum)
throw Error("write_integration_evidence_schema_invalid", "写集成证据文本长度无效。");
return value;
}
private static string RequiredHash(JObject source, string name)
{
string value = RequiredString(source, name, 64, 64).ToLowerInvariant();
if (!Sha256Pattern.IsMatch(value))
throw Error("write_integration_evidence_schema_invalid", "写集成证据 SHA-256 字段无效。");
return value;
}
private static string NullableHash(JObject source, string name)
{
JToken token = source[name];
if (token == null || token.Type == JTokenType.Null) return null;
return RequiredHash(source, name);
}
private static string NullableIdentifier(
JObject source,
string name,
int minimum,
int maximum)
{
JToken token = source[name];
if (token == null || token.Type == JTokenType.Null) return null;
string value = RequiredString(source, name, minimum, maximum);
if (!SafeIdentifier.IsMatch(value))
throw Error(
"write_integration_evidence_schema_invalid",
"写集成证据标识字段无效。");
return value;
}
private static string NullableCode(JObject source, string name)
{
JToken token = source[name];
if (token == null || token.Type == JTokenType.Null) return null;
string value = RequiredString(source, name, 1, 128);
if (!SafeCode.IsMatch(value))
throw Error("write_integration_evidence_schema_invalid", "写集成证据问题码无效。");
return value;
}
private static string[] RequiredSourcePreprocessContracts(
JObject source,
string name)
{
JArray values = source[name] as JArray;
if (values == null || values.Count > 4)
throw Error(
"write_integration_evidence_schema_invalid",
"写集成证据来源预处理契约集合无效。");
HashSet<string> allowed = new HashSet<string>(
new[]
{
PurchaseSourceDocumentContract.ImagePreprocessContract,
PurchaseSourceDocumentContract.PdfPreprocessContract,
PurchaseSourceDocumentContract.CsvPreprocessContract,
PurchaseSourceDocumentContract.XlsxPreprocessContract
},
StringComparer.Ordinal);
List<string> result = new List<string>();
string previous = null;
foreach (JToken token in values)
{
string value = token != null && token.Type == JTokenType.String
? token.Value<string>()
: null;
if (value == null
|| !allowed.Contains(value)
|| (previous != null
&& string.CompareOrdinal(previous, value) >= 0))
throw Error(
"write_integration_evidence_schema_invalid",
"写集成证据来源预处理契约必须是有序、不重复的固定值。");
result.Add(value);
previous = value;
}
return result.ToArray();
}
private static bool RequiredBoolean(JObject source, string name)
{
JToken token = source[name];
if (token == null || token.Type != JTokenType.Boolean)
throw Error("write_integration_evidence_schema_invalid", "写集成证据布尔字段无效。");
return (bool)token;
}
private static int RequiredInteger(JObject source, string name, int minimum, int maximum)
{
JToken token = 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_integration_evidence_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_integration_evidence_schema_invalid", "写集成证据 UTC 时间字段无效。");
return value;
}
private static void RequireExpected(string actual, string expected, string field)
{
if (!string.IsNullOrWhiteSpace(expected)
&& !string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase))
throw Error("write_integration_evidence_scope_mismatch", "写集成证据与预期 " + field + " 不一致。");
}
private static string Sha256(string value)
{
using (SHA256 sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(new UTF8Encoding(false).GetBytes(value));
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 RejectCommentsJsonReader : JsonTextReader
{
public RejectCommentsJsonReader(TextReader reader) : base(reader) { }
public override bool Read()
{
bool result = base.Read();
if (result && TokenType == JsonToken.Comment)
throw Error("write_integration_evidence_json_invalid", "写集成证据不允许 JSON 注释。");
return result;
}
}
}
}