import { existsSync, readFileSync } from "node:fs"; import { isAbsolute, join, relative } from "node:path"; import type { ActiveRun } from "./types.ts"; import { readArtifact } from "./artifacts.ts"; import { hasEvidenceEntry, readEvidenceIndex } from "./evidence.ts"; import { runRoot } from "./paths.ts"; import { isSourceLikePath } from "../platform/path.ts"; import { dataSourcePlanBlockedReason, dataSourceWriteBlockedReason, implementationPlanBlockedReason, implementationWriteBlockedReason, integrationPlanBlockedReason, integrationWriteBlockedReason, } from "./messages.ts"; import { DATA_SOURCE_CONTEXT_FIELDS, IMPLEMENTATION_CONTRACT_FIELDS, INTEGRATION_CONTEXT_FIELDS, type ContractField, } from "./prompt-policy.ts"; import { formatAnsweredQuestionFacts } from "./question-memory.ts"; import { firstFactsContractIssue, type RequiredFactsContractEvaluation } from "./facts-contract.ts"; export const COSMIC_METADATA_EVIDENCE = "evidence/cosmic-metadata.json"; export const DATA_SOURCE_EVIDENCE = "evidence/data-source.md"; const DATA_SOURCE_KEYWORDS = /数据源|元数据|表单|单据|字段|实体|分录|单据体|基础资料|物料|客户|供应商|Form\s*ID|FormId|formid|DynamicObject|DataSet|SQL|KSQL|查询|保存|校验|插件|事件|操作|列表|表名|数据库|dbName|dbKey|下推|上推|转换|生成单据/i; const IMPLEMENTATION_KEYWORDS = /插件|服务|处理器|二开|开发|实现|新增|修改|保存|校验|联动|下推|上推|转换|生成|同步|接口|审批|提交|审核|操作|按钮|定时|任务|脚本|SQL|KSQL/i; const INTEGRATION_KEYWORDS = /第三方|外部系统|外部接口|接口对接|对接|接口文档|HTTP|HTTPS|REST|SOAP|Webhook|webhook|回调|推送|拉取|同步|上报|下发|开放平台|API\s*接口/i; const IMPLEMENTATION_FIELDS = Object.fromEntries(IMPLEMENTATION_CONTRACT_FIELDS.map((field) => [field.id, field])) as Record; const DATA_SOURCE_FIELDS = Object.fromEntries(DATA_SOURCE_CONTEXT_FIELDS.map((field) => [field.id, field])) as Record; const INTEGRATION_FIELDS = Object.fromEntries(INTEGRATION_CONTEXT_FIELDS.map((field) => [field.id, field])) as Record; export function requiresDataSourceEvidence(cwd: string, run: ActiveRun): boolean { if (run.mode !== "normal") return false; if (!run.profile?.requiresMetadataVerification) return false; if (run.profile.product === "unknown") return false; return DATA_SOURCE_KEYWORDS.test(dataSourcePlanningText(cwd, run, { includePlan: false })) || planDeclaresConcreteDataSourceWork(cwd, run); } export function dataSourceEvidenceForRun(run: ActiveRun): string | undefined { if (!run.profile?.requiresMetadataVerification) return undefined; if (run.profile.platform === "cosmic") return COSMIC_METADATA_EVIDENCE; if (run.profile.product === "enterprise") return DATA_SOURCE_EVIDENCE; return DATA_SOURCE_EVIDENCE; } export function hasValidDataSourceEvidence(cwd: string, run: ActiveRun): boolean { const evidence = dataSourceEvidenceForRun(run); if (!evidence) return true; const path = join(runRoot(cwd, run), evidence); if (!existsSync(path)) return false; if (!hasEvidenceEntry(cwd, run, evidence)) return false; if (!hasSuccessfulEvidenceEntry(cwd, run, evidence)) return false; const content = readFileSync(path, "utf8"); if (evidence === COSMIC_METADATA_EVIDENCE) return cosmicMetadataLooksConcrete(content); return dataSourceEvidenceContentLooksConcrete(content); } /** * @deprecated Data source write blocking is now handled by V2 SourceWritePolicy * via GateRunner. Other data source utility functions remain active. * @see src/harness/gates/policies/source-write-policy.ts */ export function dataSourceProductionWriteBlockReason(_cwd: string, _run: ActiveRun | undefined, _path: string | undefined): string | undefined { return undefined; } export function dataSourceContextBlockReason(cwd: string, run: ActiveRun): string | undefined { const firstIssue = firstFactsContractIssue(evaluateRequiredFactsContract(cwd, run)); if (firstIssue?.contract === "implementation") return implementationPlanBlockedReason(firstIssue.missing); if (firstIssue?.contract === "integration") return integrationPlanBlockedReason(firstIssue.missing); if (firstIssue?.contract === "data-source") return dataSourcePlanBlockedReason(firstIssue.missing); if (!requiresDataSourceEvidence(cwd, run)) return undefined; return undefined; } export function evaluateRequiredFactsContract(cwd: string, run: ActiveRun): RequiredFactsContractEvaluation { if (run.mode !== "normal") { return { required: false, issues: [], }; } const issues = [ { contract: "implementation" as const, missing: missingImplementationContractItems(cwd, run) }, { contract: "integration" as const, missing: missingIntegrationContextItems(cwd, run) }, { contract: "data-source" as const, missing: requiresDataSourceEvidence(cwd, run) ? missingDataSourceContextItems(cwd, run) : [] }, ].filter((issue) => issue.missing.length > 0); return { required: requiresImplementationContract(cwd, run) || requiresIntegrationContext(cwd, run) || requiresDataSourceEvidence(cwd, run), issues, }; } function dataSourcePlanningText(cwd: string, run: ActiveRun, options: { includePlan?: boolean } = { includePlan: true }): string { return [ run.goal ?? "", readArtifact(cwd, run, "spec") ?? "", formatAnsweredQuestionFacts(run), options.includePlan === false ? "" : readArtifact(cwd, run, "plan") ?? "", ].join("\n"); } function planDeclaresConcreteDataSourceWork(cwd: string, run: ActiveRun): boolean { const plan = readArtifact(cwd, run, "plan") ?? ""; const withoutBoilerplate = plan .split(/\r?\n/) .filter((line) => !/必须先确认真实数据源|数据源证据|进入 execute 前必须写明|不能只根据 API 文档|必需的金蝶查证项/.test(line)) .join("\n"); return DATA_SOURCE_KEYWORDS.test(withoutBoilerplate); } function missingImplementationContractItems(cwd: string, run: ActiveRun): string[] { if (!requiresImplementationContract(cwd, run)) return []; const text = dataSourcePlanningText(cwd, run); const missing: string[] = []; if (!hasTriggerOrEntry(text)) missing.push(IMPLEMENTATION_FIELDS.trigger.label); if (!hasSourceObject(text)) missing.push(IMPLEMENTATION_FIELDS.source.label); if (!hasTargetObject(text)) missing.push(IMPLEMENTATION_FIELDS.target.label); if (!hasDataChangeContract(text)) missing.push(IMPLEMENTATION_FIELDS["data-change"].label); if (!hasRuleOrCondition(text)) missing.push(IMPLEMENTATION_FIELDS.rules.label); if (!hasFailureContract(text)) missing.push(IMPLEMENTATION_FIELDS.failure.label); if (!hasAcceptanceSample(text)) missing.push(IMPLEMENTATION_FIELDS.acceptance.label); return missing; } function requiresImplementationContract(cwd: string, run: ActiveRun): boolean { if (!run.profile?.requiresMetadataVerification || run.profile.product === "unknown") return false; return IMPLEMENTATION_KEYWORDS.test(dataSourcePlanningText(cwd, run, { includePlan: false })) || planDeclaresConcreteImplementationWork(cwd, run); } function planDeclaresConcreteImplementationWork(cwd: string, run: ActiveRun): boolean { const plan = readArtifact(cwd, run, "plan") ?? ""; const withoutBoilerplate = plan .split(/\r?\n/) .filter((line) => !/实现就绪|进入 execute 前必须写明|第三方对接必须写明|必需的金蝶查证项|不要写模板代码/.test(line)) .join("\n"); return IMPLEMENTATION_KEYWORDS.test(withoutBoilerplate); } function missingDataSourceContextItems(cwd: string, run: ActiveRun): string[] { const text = trustedDataSourceContextText(cwd, run); const missing: string[] = []; if (!hasTargetObjectIdentifier(text)) missing.push(DATA_SOURCE_FIELDS["target-form"].label); if (!hasPluginHook(text)) missing.push(DATA_SOURCE_FIELDS["plugin-hook"].label); if (!hasFieldOrEntityIdentifier(text)) missing.push(DATA_SOURCE_FIELDS["field-entity"].label); if (!hasDataAccessStrategy(text)) missing.push(DATA_SOURCE_FIELDS["data-access"].label); if (usesSql(text) && !hasSqlIdentifier(text)) missing.push(DATA_SOURCE_FIELDS["sql-identifiers"].label); return missing; } function trustedDataSourceContextText(cwd: string, run: ActiveRun): string { return [formatAnsweredQuestionFacts(run), dataSourceEvidenceContent(cwd, run)].join("\n"); } function dataSourceEvidenceContent(cwd: string, run: ActiveRun): string { const evidence = dataSourceEvidenceForRun(run); if (!evidence) return ""; const path = join(runRoot(cwd, run), evidence); return existsSync(path) ? readFileSync(path, "utf8") : ""; } function hasTriggerOrEntry(text: string): boolean { if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.trigger)) return true; return /(?:触发入口|执行时机|触发时机|入口|事件|生命周期|插件类型|按钮|操作|定时|任务|保存前|保存后|提交后|审核后|Before\w+|After\w+)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:保存前|保存后|提交后|审核后|定时|按钮点击|操作执行)/i.test(text); } function hasSourceObject(text: string): boolean { if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.source)) return true; return /(?:源对象|源单|来源单据|来源表单|输入数据|数据来源|取数来源|源 FormId|源FormId|源表|源实体)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:从|读取|基于).{0,20}(?:单据|表单|实体|字段|DynamicObject|DataSet|SQL|KSQL)/i.test(text); } function hasTargetObject(text: string): boolean { if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.target)) return true; return /(?:目标对象|目标单据|目标表单|目标 FormId|目标FormId|输出结果|生成结果|下游单据|目标表|目标实体)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:下推到|生成|写入|更新|推送到).{0,20}(?:单据|表单|实体|字段|接口|系统|表)/i.test(text); } function hasDataChangeContract(text: string): boolean { if (hasLabeledValue(text, IMPLEMENTATION_FIELDS["data-change"])) return true; return /(?:数据变化|字段映射|字段对应|字段改写|修改字段|写入字段|赋值规则|转换规则|映射关系|更新字段)\s*[::=]\s*[^。\n\r;;]{2,}|(?:字段|field).{0,20}(?:->|映射|对应|写入|修改|赋值|更新)/i.test(text); } function hasRuleOrCondition(text: string): boolean { if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.rules)) return true; return /(?:业务规则|适用条件|过滤条件|执行条件|前置条件|判断条件|范围|何时执行|哪些数据)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:当|如果|仅|只处理|满足).{0,40}(?:时|则|才)/i.test(text); } function hasFailureContract(text: string): boolean { if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.failure)) return true; return /(?:失败处理|异常处理|错误处理|回滚|补偿|人工处理|失败后|重复执行|幂等|日志|告警)\s*[::=]\s*[^。\n\r,,;;]{2,}/i.test(text); } function missingIntegrationContextItems(cwd: string, run: ActiveRun): string[] { const text = dataSourcePlanningText(cwd, run); if (!requiresIntegrationContext(cwd, run)) return []; const missing: string[] = []; if (!hasInterfaceDocument(text)) missing.push(INTEGRATION_FIELDS["interface-doc"].label); if (!hasIntegrationDirection(text)) missing.push(INTEGRATION_FIELDS.direction.label); if (!hasEndpointAndAuth(text)) missing.push(INTEGRATION_FIELDS["endpoint-auth"].label); if (!hasFieldMapping(text)) missing.push(INTEGRATION_FIELDS["field-mapping"].label); if (!hasConcurrencyStrategy(text)) missing.push(INTEGRATION_FIELDS.concurrency.label); if (!hasRetryTimeoutStrategy(text)) missing.push(INTEGRATION_FIELDS.retry.label); if (!hasErrorHandlingStrategy(text)) missing.push(INTEGRATION_FIELDS.error.label); if (!hasLoggingStrategy(text)) missing.push(INTEGRATION_FIELDS.logging.label); if (!hasAcceptanceSample(text)) missing.push(INTEGRATION_FIELDS.samples.label); return missing; } function requiresIntegrationContext(cwd: string, run: ActiveRun): boolean { return INTEGRATION_KEYWORDS.test(dataSourcePlanningText(cwd, run, { includePlan: false })) || planDeclaresConcreteIntegrationWork(cwd, run); } function planDeclaresConcreteIntegrationWork(cwd: string, run: ActiveRun): boolean { const plan = readArtifact(cwd, run, "plan") ?? ""; const withoutBoilerplate = plan .split(/\r?\n/) .filter((line) => !/第三方对接必须写明|接口文档来源\/版本|没有这些信息只能写模板代码|必需的金蝶查证项/.test(line)) .join("\n"); return INTEGRATION_KEYWORDS.test(withoutBoilerplate); } function hasInterfaceDocument(text: string): boolean { if (hasLabeledValue(text, INTEGRATION_FIELDS["interface-doc"])) return true; return /(?:接口文档|API\s*文档|文档来源|协议文档|OpenAPI|Swagger)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:https?:\/\/|\.pdf|\.docx?|\.xlsx?|swagger\.json|openapi\.json)/i.test(text); } function hasIntegrationDirection(text: string): boolean { if (hasLabeledValue(text, INTEGRATION_FIELDS.direction)) return true; return /(?:对接方向|触发时机|同步方向|数据流向|调用方|被调用方)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}|(?:金蝶调用第三方|第三方调用金蝶|入站|出站|推送|拉取|回调|定时|保存后|审核后|提交后)/i.test(text); } function hasEndpointAndAuth(text: string): boolean { if (hasLabeledValue(text, INTEGRATION_FIELDS["endpoint-auth"])) return true; return /(?:接口地址|Endpoint|URL|Base\s*URL|认证|鉴权|Token|AK|SK|AppKey|AppSecret|密钥|签名|OAuth)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}/i.test(text); } function hasFieldMapping(text: string): boolean { if (hasLabeledValue(text, INTEGRATION_FIELDS["field-mapping"])) return true; return /(?:字段映射|映射关系|字段对应|字段对照|mapping)\s*[::=]\s*[^。\n\r;;]{2,}|(?:第三方字段|外部字段).*(?:金蝶字段|单据字段|字段标识)/is.test(text); } function hasConcurrencyStrategy(text: string): boolean { if (hasLabeledValue(text, INTEGRATION_FIELDS.concurrency)) return true; return /(?:并发|幂等|去重|唯一键|业务主键|重复提交|锁|乐观锁|重入)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}/i.test(text); } function hasRetryTimeoutStrategy(text: string): boolean { if (hasLabeledValue(text, INTEGRATION_FIELDS.retry)) return true; return /(?:超时|timeout|重试|retry|限流|频率|rate\s*limit|熔断)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}/i.test(text); } function hasErrorHandlingStrategy(text: string): boolean { if (hasLabeledValue(text, INTEGRATION_FIELDS.error)) return true; return /(?:错误处理|异常处理|失败处理|失败补偿|补偿|回滚|死信|告警|人工处理)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}/i.test(text); } function hasLoggingStrategy(text: string): boolean { if (hasLabeledValue(text, INTEGRATION_FIELDS.logging)) return true; return /(?:日志|审计|留痕|脱敏|敏感信息|traceId|requestId|请求日志|响应日志)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}/i.test(text); } function hasAcceptanceSample(text: string): boolean { if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.acceptance) || hasLabeledValue(text, INTEGRATION_FIELDS.samples)) return true; return /(?:请求样例|响应样例|报文样例|验收样例|测试数据|示例报文|payload)\s*[::=]\s*[^。\n\r;;]{2,}/i.test(text); } function hasTargetObjectIdentifier(text: string): boolean { if (hasLabeledValue(text, DATA_SOURCE_FIELDS["target-form"])) return true; return /["']?(?:Form\s*ID|FormId|formId|formid|表单标识|单据标识|目标单据|目标表单)["']?\s*[::=]\s*["']?[A-Za-z0-9_.-]{2,}|(?:单据|表单)\s*[::=]\s*[^。\n\r,,;;]{2,}/i.test(text); } function hasPluginHook(text: string): boolean { if (hasLabeledValue(text, DATA_SOURCE_FIELDS["plugin-hook"])) return true; return /(?:插件类型|插件|事件|生命周期|触发时机|挂载点)\s*[::=]\s*[^。\n\r,,;;]{2,}|Before\w+|After\w+|Save|保存前|保存后|提交|审核|字段值改变|F7|列表/i.test(text); } function hasFieldOrEntityIdentifier(text: string): boolean { if (hasLabeledValue(text, DATA_SOURCE_FIELDS["field-entity"])) return true; return /(?:字段标识|字段|实体标识|实体|分录标识|单据体标识|Entry|Entity)\s*[::=]\s*[^。\n\r,,;;]{2,}|[A-Za-z][A-Za-z0-9_]*(?:Id|ID|No|Name|Qty|Amount)\b/.test(text); } function hasDataAccessStrategy(text: string): boolean { if (hasLabeledValue(text, DATA_SOURCE_FIELDS["data-access"])) return true; return /(?:读取方式|写入方式|数据访问|取数方式|数据源|读写策略)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:表单数据|模型数据|单据数据|DynamicObject|DataSet|QueryService|BusinessDataService|SQL|KSQL|直接读表|数据库查询)/i.test(text); } function usesSql(text: string): boolean { const normalized = text.replace(/不(?:需要|使用|直接)?读表|不使用\s*(?:SQL|KSQL)|无需\s*(?:SQL|KSQL)|非\s*(?:SQL|KSQL)/gi, ""); return /\bSQL\b|\bKSQL\b|直接读表|数据库查询|表名/i.test(normalized); } function hasSqlIdentifier(text: string): boolean { if (hasLabeledValue(text, DATA_SOURCE_FIELDS["sql-identifiers"])) return true; return /(?:表名|数据库表|SQL\s*表|KSQL\s*表)\s*[::=]\s*[A-Za-z0-9_.$-]{2,}/i.test(text) && /(?:数据库字段|SQL\s*字段|字段名|列名)\s*[::=]\s*[A-Za-z0-9_.$,\s-]{2,}/i.test(text); } function dataSourceEvidenceContentLooksConcrete(content: string): boolean { const text = content.trim(); if (text.length < 30) return false; if (/待确认|未知|TODO|TBD|未提供|无数据源/i.test(text)) return false; return /Form\s*ID|FormId|formid|单据|表单|字段|实体|表名|数据源|BOS|DynamicObject|DataSet|数据库|测试数据/i.test(text); } function cosmicMetadataLooksConcrete(content: string): boolean { try { const parsed = JSON.parse(content) as unknown; if (!parsed || (Array.isArray(parsed) && parsed.length === 0)) return false; const text = JSON.stringify(parsed); if (/待确认|未知|TODO|TBD|未提供/i.test(text)) return false; return /Form\s*ID|FormId|formid|field|fields|entity|entities|字段|实体|单据体|name|key|id/i.test(text); } catch { return false; } } function hasSuccessfulEvidenceEntry(cwd: string, run: ActiveRun, artifact: string): boolean { const normalized = artifact.replace(/\\/g, "/").replace(/^\/+/, "").replace(/^\.\//, ""); const entry = readEvidenceIndex(cwd, run).entries.find((item) => typeof item.path === "string" && item.path.replace(/\\/g, "/") === normalized); return Boolean(entry && entry.exitCode === 0); } function normalizeRelativePath(path: string): string { return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, ""); } function hasLabeledValue(text: string, field: ContractField): boolean { return [field.label, ...field.aliases].some((alias) => { const escaped = escapeRegExp(alias); const match = new RegExp(`(?:^|[\\r\\n\\-\\*\\s])${escaped}\\s*[::=]\\s*([^\\r\\n。;;,,]{2,})`, "i").exec(text); return Boolean(match && labeledValueLooksConcrete(match[1] ?? "")); }); } function labeledValueLooksConcrete(value: string): boolean { const trimmed = value.trim(); if (trimmed.length < 2) return false; return !/^(待确认|未知|不清楚|未提供|none|unknown|todo|tbd|n\/a|按实际|按实际环境|后续确认|以后再说)$/i.test(trimmed); } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }