soloforge 1.3.3 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +19 -3
  2. package/dist/adapters/claude_code/server.js +1 -1
  3. package/dist/adapters/claude_code/server.js.map +1 -1
  4. package/dist/adapters/claude_code/tools.d.ts.map +1 -1
  5. package/dist/adapters/claude_code/tools.js +164 -44
  6. package/dist/adapters/claude_code/tools.js.map +1 -1
  7. package/dist/adapters/shared/workflow_template.js +2 -2
  8. package/dist/adapters/shared/workflow_template.js.map +1 -1
  9. package/dist/bin/soloforge.d.ts.map +1 -1
  10. package/dist/bin/soloforge.js +148 -1
  11. package/dist/bin/soloforge.js.map +1 -1
  12. package/dist/engine/asset_manifest.d.ts.map +1 -1
  13. package/dist/engine/asset_manifest.js +11 -0
  14. package/dist/engine/asset_manifest.js.map +1 -1
  15. package/dist/engine/backend_implementation_contract.d.ts +1 -1
  16. package/dist/engine/backend_implementation_contract.d.ts.map +1 -1
  17. package/dist/engine/backend_implementation_contract.js +22 -0
  18. package/dist/engine/backend_implementation_contract.js.map +1 -1
  19. package/dist/engine/brainstorm_contract.d.ts +1 -1
  20. package/dist/engine/brainstorm_contract.js +1 -1
  21. package/dist/engine/code_maintainability_observability_contract.d.ts +74 -0
  22. package/dist/engine/code_maintainability_observability_contract.d.ts.map +1 -0
  23. package/dist/engine/code_maintainability_observability_contract.js +711 -0
  24. package/dist/engine/code_maintainability_observability_contract.js.map +1 -0
  25. package/dist/engine/config_write_boundary.d.ts +29 -0
  26. package/dist/engine/config_write_boundary.d.ts.map +1 -0
  27. package/dist/engine/config_write_boundary.js +69 -0
  28. package/dist/engine/config_write_boundary.js.map +1 -0
  29. package/dist/engine/consumable_asset_registry.d.ts.map +1 -1
  30. package/dist/engine/consumable_asset_registry.js +49 -1
  31. package/dist/engine/consumable_asset_registry.js.map +1 -1
  32. package/dist/engine/contract_registry.js +1 -1
  33. package/dist/engine/diagnostic_registry.d.ts +13 -0
  34. package/dist/engine/diagnostic_registry.d.ts.map +1 -1
  35. package/dist/engine/diagnostic_registry.js +68 -0
  36. package/dist/engine/diagnostic_registry.js.map +1 -1
  37. package/dist/engine/dual_layer_mechanism_registry.d.ts.map +1 -1
  38. package/dist/engine/dual_layer_mechanism_registry.js +195 -2
  39. package/dist/engine/dual_layer_mechanism_registry.js.map +1 -1
  40. package/dist/engine/explicit_asset_registry.d.ts.map +1 -1
  41. package/dist/engine/explicit_asset_registry.js +134 -0
  42. package/dist/engine/explicit_asset_registry.js.map +1 -1
  43. package/dist/engine/extension_scenario_registry.js +4 -4
  44. package/dist/engine/extension_scenario_registry.js.map +1 -1
  45. package/dist/engine/foundation_scenario_runners.d.ts.map +1 -1
  46. package/dist/engine/foundation_scenario_runners.js +4 -2
  47. package/dist/engine/foundation_scenario_runners.js.map +1 -1
  48. package/dist/engine/historical_issue_mechanization_matrix.d.ts +28 -0
  49. package/dist/engine/historical_issue_mechanization_matrix.d.ts.map +1 -0
  50. package/dist/engine/historical_issue_mechanization_matrix.js +134 -0
  51. package/dist/engine/historical_issue_mechanization_matrix.js.map +1 -0
  52. package/dist/engine/implementation_roadmap_registry.d.ts.map +1 -1
  53. package/dist/engine/implementation_roadmap_registry.js +114 -13
  54. package/dist/engine/implementation_roadmap_registry.js.map +1 -1
  55. package/dist/engine/intent_expander.d.ts.map +1 -1
  56. package/dist/engine/intent_expander.js +151 -1
  57. package/dist/engine/intent_expander.js.map +1 -1
  58. package/dist/engine/knowledge_governance_gate.d.ts +38 -0
  59. package/dist/engine/knowledge_governance_gate.d.ts.map +1 -0
  60. package/dist/engine/knowledge_governance_gate.js +123 -0
  61. package/dist/engine/knowledge_governance_gate.js.map +1 -0
  62. package/dist/engine/log_governance.d.ts +25 -0
  63. package/dist/engine/log_governance.d.ts.map +1 -0
  64. package/dist/engine/log_governance.js +76 -0
  65. package/dist/engine/log_governance.js.map +1 -0
  66. package/dist/engine/mechanism_contract_registry.d.ts +1 -0
  67. package/dist/engine/mechanism_contract_registry.d.ts.map +1 -1
  68. package/dist/engine/mechanism_contract_registry.js +104 -0
  69. package/dist/engine/mechanism_contract_registry.js.map +1 -1
  70. package/dist/engine/mechanism_health_check.d.ts +23 -0
  71. package/dist/engine/mechanism_health_check.d.ts.map +1 -0
  72. package/dist/engine/mechanism_health_check.js +140 -0
  73. package/dist/engine/mechanism_health_check.js.map +1 -0
  74. package/dist/engine/next_action_planner.d.ts.map +1 -1
  75. package/dist/engine/next_action_planner.js +72 -4
  76. package/dist/engine/next_action_planner.js.map +1 -1
  77. package/dist/engine/observability.js +1 -1
  78. package/dist/engine/observability.js.map +1 -1
  79. package/dist/engine/project_knowledge_contract.d.ts +58 -0
  80. package/dist/engine/project_knowledge_contract.d.ts.map +1 -0
  81. package/dist/engine/project_knowledge_contract.js +298 -0
  82. package/dist/engine/project_knowledge_contract.js.map +1 -0
  83. package/dist/engine/project_knowledge_system_regression_matrix.d.ts +27 -0
  84. package/dist/engine/project_knowledge_system_regression_matrix.d.ts.map +1 -0
  85. package/dist/engine/project_knowledge_system_regression_matrix.js +295 -0
  86. package/dist/engine/project_knowledge_system_regression_matrix.js.map +1 -0
  87. package/dist/engine/release_issue_scenario_registry.d.ts.map +1 -1
  88. package/dist/engine/release_issue_scenario_registry.js +506 -95
  89. package/dist/engine/release_issue_scenario_registry.js.map +1 -1
  90. package/dist/engine/release_readiness_gate.d.ts +4 -0
  91. package/dist/engine/release_readiness_gate.d.ts.map +1 -1
  92. package/dist/engine/release_readiness_gate.js +643 -12
  93. package/dist/engine/release_readiness_gate.js.map +1 -1
  94. package/dist/engine/team_awareness.js +6 -6
  95. package/dist/engine/team_awareness.js.map +1 -1
  96. package/dist/engine/technology_decision.js +5 -5
  97. package/dist/engine/technology_decision.js.map +1 -1
  98. package/dist/engine/template_asset_contract_registry.d.ts.map +1 -1
  99. package/dist/engine/template_asset_contract_registry.js +6 -5
  100. package/dist/engine/template_asset_contract_registry.js.map +1 -1
  101. package/dist/engine/verifier.js +1 -1
  102. package/dist/engine/verifier.js.map +1 -1
  103. package/dist/engine/workflow_navigation_contract.d.ts +10 -0
  104. package/dist/engine/workflow_navigation_contract.d.ts.map +1 -1
  105. package/dist/knowledge/loader.d.ts +3 -1
  106. package/dist/knowledge/loader.d.ts.map +1 -1
  107. package/dist/knowledge/loader.js +2 -2
  108. package/dist/knowledge/loader.js.map +1 -1
  109. package/dist/types.d.ts +23 -0
  110. package/dist/types.d.ts.map +1 -1
  111. package/package.json +1 -1
  112. package/templates/knowledge/acceptance_templates//344/273/243/347/240/201/346/263/250/351/207/212/344/270/216/346/227/245/345/277/227/351/252/214/346/224/266/346/250/241/346/235/277.md +78 -0
  113. package/templates/knowledge/acceptance_templates//351/200/232/347/224/250/350/264/250/351/207/217/351/252/214/346/224/266/346/270/205/345/215/225.md +1 -1
  114. package/templates/knowledge/acceptance_templates//351/207/215/346/236/204/346/226/271/346/241/210/346/250/241/347/211/210.md +1 -1
  115. package/templates/knowledge/domain//346/224/257/344/273/230/350/247/204/345/210/231.md +1 -1
  116. package/templates/knowledge/procedures//346/225/260/346/215/256/345/272/223/350/277/201/347/247/273/346/265/201/347/250/213.md +1 -1
  117. package/templates/knowledge/procedures//351/203/250/347/275/262/345/217/221/345/270/203/346/265/201/347/250/213.md +1 -1
  118. package/templates/knowledge/procedures//351/207/215/346/236/204/346/265/201/346/260/264/347/272/277.md +1 -1
  119. package/templates/knowledge/review//344/273/243/347/240/201/345/217/257/347/273/264/346/212/244/346/200/247/344/270/216/345/217/257/350/247/202/346/265/213/346/200/247/345/256/241/346/237/245.md +81 -0
  120. package/templates/knowledge/review_rules//344/272/244/344/273/230/345/256/214/345/244/207/346/200/247/345/256/241/346/237/245/350/247/204/345/210/231.md +1 -1
  121. package/templates/knowledge/review_rules//350/264/250/351/207/217/345/256/241/346/237/245/350/247/204/345/210/231.md +3 -3
  122. package/templates/knowledge/rules//344/273/243/347/240/201/346/263/250/351/207/212/344/270/216/346/227/245/345/277/227/345/245/221/347/272/246/350/247/204/345/210/231.md +150 -0
  123. package/templates/knowledge/rules//346/225/217/346/204/237/344/277/241/346/201/257/346/227/245/345/277/227/350/247/204/345/210/231.md +69 -0
  124. package/templates/knowledge/rules//346/227/245/345/277/227/346/262/273/347/220/206/350/247/204/345/210/231.md +49 -0
  125. package/templates/knowledge/rules//346/234/272/345/210/266/350/207/252/346/262/273/347/220/206/350/247/204/345/210/231.md +48 -0
  126. package/templates/knowledge/rules//346/240/270/345/277/203/344/275/223/351/252/214/345/216/237/345/210/231.md +1 -1
  127. package/templates/knowledge/rules//346/240/270/345/277/203/345/267/245/347/250/213/346/211/247/350/241/214/345/216/237/345/210/231.md +2 -2
  128. package/templates/knowledge/rules//346/274/224/350/277/233/345/233/236/345/275/222/351/227/250/346/216/247/350/247/204/345/210/231.md +1 -1
  129. package/templates/knowledge/rules//347/237/245/350/257/206/346/262/273/347/220/206/350/247/204/345/210/231.md +50 -0
  130. package/templates/knowledge/rules//351/205/215/347/275/256/350/220/275/347/233/230/350/276/271/347/225/214/350/247/204/345/210/231.md +47 -0
  131. package/templates/patterns/Git/346/223/215/344/275/234/350/247/204/350/214/203.md +1 -1
  132. package/templates/scaffolds/react/Form.tsx.hbs +11 -3
  133. package/templates/scaffolds/react/List.tsx.hbs +11 -3
  134. package/templates/scaffolds/react/Page.tsx.hbs +1 -1
  135. package/templates/scaffolds/react/types.ts.hbs +4 -1
  136. package/templates/scaffolds/spring-boot/Controller.java.hbs +18 -4
  137. package/templates/scaffolds/spring-boot/DTO.java.hbs +4 -1
  138. package/templates/scaffolds/spring-boot/Entity.java.hbs +8 -3
  139. package/templates/scaffolds/spring-boot/Mapper.java.hbs +4 -1
  140. package/templates/scaffolds/spring-boot/ServiceImpl.java.hbs +34 -10
  141. package/templates/scaffolds/spring-boot/ServiceTest.java.hbs +0 -1
@@ -0,0 +1,711 @@
1
+ /**
2
+ * 用户项目代码可维护性与可观测性契约。
3
+ *
4
+ * 治理对象是用户项目代码,不是 SoloForge 自身。
5
+ * 覆盖必要注释、结构化日志、错误日志、审计日志、
6
+ * 敏感信息脱敏和低风险跳过。
7
+ */
8
+ /* ── 关键词 ─────────────────────────────────────── */
9
+ const OBSERVABILITY_KEYWORDS = /业务|支付|金额|账单|合同|入住|权限|登录|安全|通知|异步|状态机|审批|补贴|核销|外部系统|webhook|回调|异常处理|重试|补偿|事务|幂等|并发|数据修复|批处理|迁移|Controller|Service|UseCase|Domain|Repository|payment|billing|invoice|contract|permission|auth|security|state|workflow|migration|refund|charge|order/i;
10
+ const BUSINESS_IMPLEMENTATION_KEYWORDS = /实现|新增|创建|更新|删除|保存|提交|接口|API|Controller|Service|Repository|Mapper|DTO|Entity|UseCase|页面提交|表单提交|业务逻辑|业务规则|CRUD|create|update|delete|save|submit|endpoint|handler|controller|service|repository|usecase/i;
11
+ const DESIGN_ARTIFACT_KEYWORDS = /设计|文档|规格|OpenAPI|API接口规格|数据库设计|架构设计|接口设计|artifact|spec/i;
12
+ const CODE_IMPLEMENTATION_KEYWORDS = /实现|新增|创建|更新|删除|保存|提交|编码|代码|Controller|Service|Repository|Mapper|DTO|Entity|UseCase|CRUD|create|update|delete|save|submit|handler|controller|service|repository|usecase/i;
13
+ const LOW_RISK_KEYWORDS = /错别字|拼写|文案|样式|css|颜色|README|配置值|测试数据|fixture|getter|setter|简单类型|类型定义|interface\s+\w+\s*\{|type\s+\w+\s*=/i;
14
+ const CRITICAL_LOG_KEYWORDS = /支付|账单|金额|退款|核销|补贴|缴费|payment|billing|refund|charge|invoice/i;
15
+ const SECURITY_LOG_KEYWORDS = /权限|登录|认证|越权|安全|role|auth|permission|security|access.?denied|forbidden/i;
16
+ const STATE_CHANGE_KEYWORDS = /状态|流转|status|state|transition|pending|approved|rejected|cancelled/i;
17
+ const EXTERNAL_CALL_KEYWORDS = /外部|第三方|API|调用|HTTP|RPC|webhook|callback|RestTemplate|WebClient|fetch|axios/i;
18
+ const MIGRATION_KEYWORDS = /迁移|修复|批处理|migration|batch|data.?fix|script/i;
19
+ const COMPLEX_RULE_KEYWORDS = /金额计算|计算逻辑|领域不变量|状态机|幂等|并发控制|分布式锁|算法|formula|invariant|idempoten|concurr/i;
20
+ const BACKEND_IMPLEMENTATION_FILE = /(?:Controller|Service|ServiceImpl|UseCase|Facade|ApplicationService|DomainService|Mapper|Repository|Resolver|Handler|Route|Router|ViewSet|Resource|Endpoint)\.(?:java|kt|ts|tsx|js|mjs|cjs|py|go|rb|cs)$/i;
21
+ const BACKEND_IMPLEMENTATION_PATH = /(?:^|\/)(?:controllers?|services?|usecases?|application|domain|routes?|routers?|handlers?|resolvers?|repositories?|mappers?|views?|api|endpoints?)\//i;
22
+ const BACKEND_DATA_CONTRACT_FILE = /(?:Entity|DTO|Dto|Request|Response|VO|Vo|CreateReq|UpdateReq|Command|Query|Schema|Payload|Input|Output|Serializer|Model)\.(?:java|kt|ts|tsx|js|mjs|cjs|py|go|rb|cs)$/i;
23
+ const BACKEND_DATA_CONTRACT_PATH = /(?:^|\/)(?:dto|dtos|schemas?|payloads?|requests?|responses?|serializers?|models?|entities?)\//i;
24
+ const FRONTEND_IMPLEMENTATION_FILE = /(?:Page|View|Screen|Form|Panel|Table|List|Modal|Dialog|Store|Slice|Composable|Hook|Service|Client)\.(?:vue|tsx|jsx|ts|js|svelte)$/i;
25
+ const FRONTEND_IMPLEMENTATION_PATH = /(?:^|\/)(?:pages?|views?|screens?|components?|stores?|hooks?|composables?|clients?|services?)\//i;
26
+ const BUSINESS_WRITE_ENDPOINT = /@(PostMapping|PutMapping|PatchMapping|DeleteMapping)|\b(?:router|app|server)\.(?:post|put|patch|delete)\s*\(|\b(?:post|put|patch|delete)\s*\(\s*["'`/]|@(?:app|router)\.(?:post|put|patch|delete)\s*\(|\b(?:def|async\s+def)\s+(?:create|update|delete|remove|patch|post|put)_|\bfunc\s+(?:Create|Update|Delete|Remove|Patch|Post|Put)\w*\s*\(|\b(?:create|add|save|insert|update|edit|delete|remove|disable|enable|reset|assign|bind|unbind|approve|reject|submit|cancel|terminate|status|transition|leave|offboard)\w*\s*\(/i;
27
+ const BUSINESS_WRITE_CALL = /\.(?:save|insert|update|delete|remove|disable|enable|reset|assign|bind|unbind|approve|reject|submit|cancel|terminate|transition|create|patch)\w*\s*\(|\b(?:db|repo|repository|mapper|dao|client)\.(?:Save|Insert|Update|Delete|Create|Patch|Exec|QueryRow)\w*\s*\(|\b(?:session|db)\.(?:add|delete|commit|execute)\s*\(/i;
28
+ const FRONTEND_MUTATION_CALL = /(?:axios|fetch|request|http|apiClient|client)\.(?:post|put|patch|delete)\s*\(|\bfetch\s*\([^)]*method\s*:\s*["'`](?:POST|PUT|PATCH|DELETE)|\b(?:submit|save|create|update|delete|remove|disable|enable|reset|assign|approve|reject|cancel|pay|refund|upload)\w*\s*\(/i;
29
+ const FRONTEND_STATE_MUTATION = /(?:setState|useState|useReducer|dispatch|commit|store\.|zustand|pinia|redux|set[A-Z]\w*|status\s*=|\.value\s*=)/i;
30
+ const TRANSACTIONAL_OR_CASCADE_KEYWORDS = /@Transactional|事务|双写|级联|cascade|状态转换|状态流转|离职|密码重置|角色分配|权限分配|sys[_-]?user|userMapper|accountStatus|bedStatus|staffStatus|status transition/i;
31
+ const FIELD_DOC_ANNOTATION = /(?:\/\/|\/\*\*?|^\s*\*|#|@Schema|@ApiModelProperty|@Parameter|@Column\(.*comment|@Comment|Field\(|description\s*=|json_schema_extra|pydantic\.Field)/i;
32
+ const CJK_TEXT = /[\u4e00-\u9fff]/;
33
+ const JAVA_SOURCE_FILE = /\.java$/i;
34
+ const JAVA_TYPE_DECLARATION = /\b(?:public\s+)?(?:abstract\s+|final\s+)?(?:class|interface|enum|record)\s+(\w+)/;
35
+ const JAVA_METHOD_DECLARATION = /^\s*(?:public|protected|private)\s+(?:static\s+|final\s+|synchronized\s+|abstract\s+)*(?:<[^>]+>\s*)?([\w<>\[\]?.,\s]+)\s+(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w.,\s]+)?\s*\{/;
36
+ const IMPORTANT_BACKEND_LINE = /(?:\.insert\s*\(|\.update\s*\(|\.delete\s*\(|\.remove\s*\(|\.save\s*\(|setStatus\s*\(|updateStatus\s*\(|assignRole\s*\(|resetPassword\s*\(|sysUserMapper|userMapper|paymentGateway|externalClient|permissionService|auditMapper)/i;
37
+ function isBackendImplementationFile(file) {
38
+ return BACKEND_IMPLEMENTATION_FILE.test(file) || BACKEND_IMPLEMENTATION_PATH.test(file);
39
+ }
40
+ function isBackendDataContractFile(file) {
41
+ return BACKEND_DATA_CONTRACT_FILE.test(file) || BACKEND_DATA_CONTRACT_PATH.test(file);
42
+ }
43
+ function isFrontendImplementationFile(file) {
44
+ return FRONTEND_IMPLEMENTATION_FILE.test(file) || FRONTEND_IMPLEMENTATION_PATH.test(file) || /\.(?:vue|tsx|jsx|svelte)$/i.test(file);
45
+ }
46
+ function containsChinese(text) {
47
+ return CJK_TEXT.test(text);
48
+ }
49
+ function isProductionJavaBackendFile(file) {
50
+ return JAVA_SOURCE_FILE.test(file) && (isBackendImplementationFile(file) || isBackendDataContractFile(file));
51
+ }
52
+ function extractJavadocBefore(lines, lineIndex, windowSize = 10) {
53
+ const start = Math.max(0, lineIndex - windowSize);
54
+ const window = lines.slice(start, lineIndex).join("\n");
55
+ const matches = [...window.matchAll(/\/\*\*[\s\S]*?\*\//g)];
56
+ return matches.at(-1)?.[0];
57
+ }
58
+ function javaParams(paramText) {
59
+ return paramText
60
+ .split(",")
61
+ .map((p) => p.trim())
62
+ .filter(Boolean)
63
+ .map((p) => p.replace(/@\w+(?:\([^)]*\))?\s*/g, "").trim().split(/\s+/).at(-1) ?? "")
64
+ .filter((p) => p.length > 0);
65
+ }
66
+ function hasNearbyChineseLineComment(lines, lineIndex) {
67
+ const nearby = lines.slice(Math.max(0, lineIndex - 3), lineIndex + 1).join("\n");
68
+ return /\/\//.test(nearby) && containsChinese(nearby);
69
+ }
70
+ const SENSITIVE_PATTERNS = [
71
+ { pattern: /(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key)\s*[:=]\s*["'][^"']{4,}/i, name_zh: "明文密钥/令牌" },
72
+ { pattern: /(?:cookie|session[_-]?id|set[_-]?cookie)\s*[:=]\s*["'][^"']{8,}/i, name_zh: "Cookie/会话" },
73
+ { pattern: /(?:手机号|phone|mobile|cell)\s*[:=]\s*["']?\d{11}/i, name_zh: "手机号全量" },
74
+ { pattern: /(?:身份证|id[_-]?card|id[_-]?number)\s*[:=]\s*["']?\d{15,18}/i, name_zh: "身份证全量" },
75
+ { pattern: /(?:银行卡|bank[_-]?card|card[_-]?number|pan)\s*[:=]\s*["']?\d{13,19}/i, name_zh: "银行卡全量" },
76
+ { pattern: /(?:健康|病历|诊断|处方|health|medical|diagnosis|prescription)/i, name_zh: "健康隐私" },
77
+ ];
78
+ // 日志输出中的敏感信息 — 区分声明与输出
79
+ const LOG_OUTPUT_SENSITIVE_PATTERNS = [
80
+ // logger.info({ token }) / console 输出含 password 字段的对象
81
+ { pattern: /(?:log|logger|LOG|console)\.(?:info|warn|error|debug|log|INFO|WARN|ERROR|DEBUG)\s*\(\s*\{[^}]*(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|cookie|session[_-]?id)\b/i, name_zh: "日志对象含敏感字段" },
82
+ // console dot log 输出 user.password / log.info(data.secret)
83
+ { pattern: /(?:log|logger|LOG|console)\.(?:info|warn|error|debug|log|INFO|WARN|ERROR|DEBUG)\s*\([^)]*\b\w+\.(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|cookie|session[_-]?id)\b/i, name_zh: "日志输出敏感属性值" },
84
+ // logger.info("token={}", token) / log.info("password=" + pwd) / log.info("token: %s", t)
85
+ { pattern: /(?:log|logger|LOG|console)\.(?:info|warn|error|debug|log|INFO|WARN|ERROR|DEBUG)\s*\(\s*["'][^"']*\b(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|cookie|session[_-]?id)\s*[=::]\s*(?:["']?\s*\+|\{?\}|%[sd])/i, name_zh: "日志拼接/格式化敏感值" },
86
+ ];
87
+ /* ── 触发判断 ─────────────────────────────────── */
88
+ export function requiresCodeObservabilityContract(intent, route, changedFiles = []) {
89
+ const text = `${intent} ${changedFiles.join(" ")}`;
90
+ if (route === "artifact_generation" && DESIGN_ARTIFACT_KEYWORDS.test(intent) && !CODE_IMPLEMENTATION_KEYWORDS.test(intent))
91
+ return false;
92
+ const hasObservabilitySignal = OBSERVABILITY_KEYWORDS.test(text) || BUSINESS_IMPLEMENTATION_KEYWORDS.test(text);
93
+ if (LOW_RISK_KEYWORDS.test(text) && !hasObservabilitySignal)
94
+ return false;
95
+ const relevantRoute = !route
96
+ || ["code_change", "artifact_generation", "operation", "multi_stage_plan"].includes(route);
97
+ if (!relevantRoute)
98
+ return false;
99
+ if (!hasObservabilitySignal)
100
+ return false;
101
+ // 简单修复/调试/查询不触发完整契约
102
+ if (/^(?:修复|调试|排查|查看|分析|解释|为什么|查一下|帮我看看|debug|fix\s+(?:a\s+)?(?:typo|lint|import|build))/i.test(intent.trim()))
103
+ return false;
104
+ return true;
105
+ }
106
+ /* ── 项目 logger 检测 ──────────────────────────── */
107
+ export function detectProjectLogger(fileContents) {
108
+ const allContent = Object.values(fileContents).join("\n");
109
+ if (/import\s+.*(?:LoggerFactory|@Slf4j|log\s*=.*Logger)/.test(allContent) || /log\.(info|warn|error|debug)\s*\(/.test(allContent)) {
110
+ return { type: "slf4j", import_pattern: "LoggerFactory.getLogger", call_pattern: "log.info/warn/error", confidence: "high" };
111
+ }
112
+ if (/import\s+.*(?:pino|from\s+['"]pino['"])/.test(allContent) || /(?:const|let)\s+\w*(?:logger|log)\s*=\s*pino/.test(allContent)) {
113
+ return { type: "pino", import_pattern: "import pino", call_pattern: "logger.info/warn/error", confidence: "high" };
114
+ }
115
+ if (/import\s+.*(?:winston|from\s+['"]winston['"])/.test(allContent) || /createLogger\s*\(/.test(allContent)) {
116
+ return { type: "winston", import_pattern: "import winston", call_pattern: "logger.info/warn/error", confidence: "high" };
117
+ }
118
+ if (/import\s+.*(?:@nestjs|NestJS|Logger\s*}\s*from\s+['"]@nestjs)/.test(allContent)) {
119
+ return { type: "nestjs_logger", import_pattern: "import { Logger } from '@nestjs/common'", call_pattern: "this.logger.log/warn/error", confidence: "high" };
120
+ }
121
+ const fileKeys = Object.keys(fileContents).join(" ");
122
+ const hasFrontendExt = /\.(?:vue|tsx|jsx|svelte)\b/.test(fileKeys);
123
+ const hasFrontendImport = /\.vue\s*['"]|\.tsx\s*['"]|\.jsx\s*['"]|react['"]|vue['"]/.test(allContent);
124
+ if (hasFrontendExt || hasFrontendImport) {
125
+ if (/(?:const|let)\s+\w*(?:logger|Logger)\s*=/.test(allContent)) {
126
+ return { type: "custom_logger", import_pattern: "项目封装 logger", call_pattern: "logger.info/warn/error", confidence: "medium" };
127
+ }
128
+ return { type: "frontend_project", import_pattern: "前端项目", call_pattern: "生产环境禁止 console", confidence: "medium" };
129
+ }
130
+ if (/(?:const|let)\s+\w*(?:logger|Logger)\s*=/.test(allContent)) {
131
+ return { type: "custom_logger", import_pattern: "项目封装 logger", call_pattern: "logger.info/warn/error", confidence: "medium" };
132
+ }
133
+ return { type: "none_detected", import_pattern: "无", call_pattern: "console.log", confidence: "low" };
134
+ }
135
+ /* ── 工作包创建 ───────────────────────────────── */
136
+ export function createCodeObservabilityWorkPackage(taskId, intent, loggerInfo) {
137
+ const text = intent;
138
+ const requiredLogs = [];
139
+ const sensitiveFields = [];
140
+ const requiredComments = [];
141
+ if (CRITICAL_LOG_KEYWORDS.test(text)) {
142
+ requiredLogs.push("支付/金额/退款/核销状态变更必须有业务日志,含对象 ID、操作人、前后状态");
143
+ sensitiveFields.push("payment_amount", "card_number", "bank_account");
144
+ }
145
+ if (SECURITY_LOG_KEYWORDS.test(text)) {
146
+ requiredLogs.push("权限拒绝/越权访问必须有安全日志,含操作人、目标资源、拒绝原因");
147
+ }
148
+ if (STATE_CHANGE_KEYWORDS.test(text)) {
149
+ requiredLogs.push("关键状态流转必须有日志,含流转前后状态和触发原因");
150
+ }
151
+ if (EXTERNAL_CALL_KEYWORDS.test(text)) {
152
+ requiredLogs.push("外部调用失败/超时/重试必须有日志,含目标、响应码/错误、重试次数");
153
+ }
154
+ if (MIGRATION_KEYWORDS.test(text)) {
155
+ requiredLogs.push("数据修复/迁移脚本必须有审计日志,含修复前值、修复后值、影响行数");
156
+ }
157
+ if (COMPLEX_RULE_KEYWORDS.test(text)) {
158
+ requiredComments.push("复杂业务规则必须有注释说明原因(为什么),不得只复述代码行为(做什么)");
159
+ }
160
+ if (/事务|幂等|并发|分布式锁/i.test(text)) {
161
+ requiredComments.push("事务边界、幂等策略、并发控制必须有注释说明设计意图");
162
+ }
163
+ return {
164
+ task_id: taskId,
165
+ status: "required",
166
+ applicability_reason_zh: `该任务涉及业务编码,需确认代码可维护性与可观测性约束: ${intent}`,
167
+ required_logs: requiredLogs,
168
+ required_comments: requiredComments,
169
+ sensitive_fields: sensitiveFields,
170
+ detected_logger: loggerInfo,
171
+ };
172
+ }
173
+ function workPackageSatisfied(wp) {
174
+ return !!wp && wp.status === "satisfied";
175
+ }
176
+ /* ── Gate ─────────────────────────────────────── */
177
+ export function evaluateCodeObservabilityGate(input) {
178
+ if (!requiresCodeObservabilityContract(input.intent, input.route, input.changed_files)) {
179
+ return {
180
+ applicable: false,
181
+ allowed: true,
182
+ status: "not_applicable",
183
+ reason_zh: "低风险或非业务编码任务,不触发代码可维护性/可观测性工作包",
184
+ findings: [],
185
+ };
186
+ }
187
+ const loggerInfo = input.project_files
188
+ ? detectProjectLogger(input.project_files)
189
+ : undefined;
190
+ const wp = input.work_package ?? createCodeObservabilityWorkPackage(input.task_id, input.intent, loggerInfo);
191
+ if (!workPackageSatisfied(wp)) {
192
+ return {
193
+ applicable: true,
194
+ allowed: false,
195
+ status: "blocked",
196
+ reason_zh: "业务编码任务缺少代码可维护性/可观测性工作包,不得开始实现",
197
+ work_package: { ...wp, detected_logger: loggerInfo },
198
+ findings: [],
199
+ };
200
+ }
201
+ return {
202
+ applicable: true,
203
+ allowed: true,
204
+ status: "satisfied",
205
+ reason_zh: "代码可维护性/可观测性工作包已具备,可进入实现",
206
+ work_package: { ...wp, detected_logger: loggerInfo },
207
+ findings: [],
208
+ };
209
+ }
210
+ /* ── Review: 敏感信息检测 ─────────────────────── */
211
+ export function detectSensitiveLogs(fileContents) {
212
+ const findings = [];
213
+ for (const [file, content] of Object.entries(fileContents)) {
214
+ if (/\.(?:test|spec)\./.test(file))
215
+ continue;
216
+ if (isProductionJavaBackendFile(file)) {
217
+ const lines = content.split(/\r?\n/);
218
+ const typeMatch = content.match(JAVA_TYPE_DECLARATION);
219
+ const typeName = typeMatch?.[1];
220
+ for (let i = 0; i < lines.length; i++) {
221
+ const typeLine = lines[i] ?? "";
222
+ const typeDecl = typeLine.match(JAVA_TYPE_DECLARATION);
223
+ if (!typeDecl)
224
+ continue;
225
+ const doc = extractJavadocBefore(lines, i);
226
+ if (!doc || !containsChinese(doc)) {
227
+ findings.push({
228
+ category: "missing_class_doc",
229
+ severity: "hard_fail",
230
+ file,
231
+ evidence: `后端类型 ${typeDecl[1]} 缺少说明类作用的中文 Javadoc`,
232
+ suggestion_zh: "在类、接口、枚举或记录声明前补充中文 Javadoc,说明该类型承担的业务职责、边界和使用场景。",
233
+ });
234
+ }
235
+ }
236
+ for (let i = 0; i < lines.length; i++) {
237
+ const line = lines[i] ?? "";
238
+ const method = line.match(JAVA_METHOD_DECLARATION);
239
+ if (!method)
240
+ continue;
241
+ const returnType = method[1]?.trim() ?? "";
242
+ const methodName = method[2] ?? "";
243
+ const params = javaParams(method[3] ?? "");
244
+ if (typeName && methodName === typeName)
245
+ continue;
246
+ const doc = extractJavadocBefore(lines, i);
247
+ const hasReturnDoc = returnType === "void" || /@return|返回|出参|响应|结果/.test(doc ?? "");
248
+ const hasParamDoc = params.length === 0 || /@param|参数|入参|请求/.test(doc ?? "");
249
+ if (!doc || !containsChinese(doc) || !hasParamDoc || !hasReturnDoc) {
250
+ findings.push({
251
+ category: "missing_method_doc",
252
+ severity: "hard_fail",
253
+ file,
254
+ evidence: `后端方法 ${methodName} 缺少说明方法、入参或出参的中文 Javadoc`,
255
+ suggestion_zh: "每个后端方法必须有中文 Javadoc:说明方法作用;有入参时说明参数语义;有返回值时说明返回对象或结果含义。",
256
+ });
257
+ }
258
+ }
259
+ const importantLines = lines
260
+ .map((line, index) => ({ line, index }))
261
+ .filter(({ line }) => IMPORTANT_BACKEND_LINE.test(line));
262
+ const missingImportantComment = importantLines.some(({ index }) => !hasNearbyChineseLineComment(lines, index));
263
+ if (importantLines.length > 0 && missingImportantComment) {
264
+ findings.push({
265
+ category: "missing_important_line_comment",
266
+ severity: "hard_fail",
267
+ file,
268
+ evidence: "后端关键写入、状态变更、外部调用或账号联动行缺少中文行注释",
269
+ suggestion_zh: "在关键业务行附近补充中文行注释,说明为什么需要该写入、状态变更、外部调用或联动操作,以及失败/回滚边界。",
270
+ });
271
+ }
272
+ }
273
+ for (const { pattern, name_zh } of SENSITIVE_PATTERNS) {
274
+ if (pattern.test(content)) {
275
+ findings.push({
276
+ category: "sensitive_log_leak",
277
+ severity: "hard_fail",
278
+ file,
279
+ evidence: `日志或输出可能包含敏感信息: ${name_zh}`,
280
+ suggestion_zh: `对 ${name_zh} 字段脱敏后再输出(如仅显示末四位或哈希值)。`,
281
+ });
282
+ }
283
+ }
284
+ for (const { pattern, name_zh } of LOG_OUTPUT_SENSITIVE_PATTERNS) {
285
+ if (pattern.test(content)) {
286
+ findings.push({
287
+ category: "sensitive_log_leak",
288
+ severity: "hard_fail",
289
+ file,
290
+ evidence: `日志输出包含敏感信息: ${name_zh}`,
291
+ suggestion_zh: `禁止在日志中输出敏感字段值,仅记录脱敏值或哈希。`,
292
+ });
293
+ }
294
+ }
295
+ }
296
+ return findings;
297
+ }
298
+ /* ── Review: 缺失日志检测 ─────────────────────── */
299
+ export function reviewMissingLogs(fileContents) {
300
+ const findings = [];
301
+ for (const [file, content] of Object.entries(fileContents)) {
302
+ if (/\.(?:test|spec)\./.test(file))
303
+ continue;
304
+ const lowered = file.toLowerCase();
305
+ const hasLog = /(?:log\.(info|warn|error|debug)|logger\.(info|warn|error|debug|log)|LOG\.(INFO|WARN|ERROR)|console\.(log|error|warn))\s*\(/i.test(content);
306
+ const hasProjectLog = /(?:log\.(info|warn|error|debug)|logger\.(info|warn|error|debug|log)|LOG\.(INFO|WARN|ERROR))\s*\(/i.test(content);
307
+ const logStatements = content.match(/(?:log\.|logger\.|LOG\.|console\.)\s*(?:info|warn|error|debug|log|INFO|WARN|ERROR|DEBUG)\s*\([^)]*\)/gi) ?? [];
308
+ for (const stmt of logStatements) {
309
+ const firstLiteral = stmt.match(/["'`]([^"'`]+)["'`]/)?.[1] ?? "";
310
+ if (firstLiteral && /[A-Za-z\u4e00-\u9fff]/.test(firstLiteral) && !containsChinese(firstLiteral)) {
311
+ findings.push({
312
+ category: "non_chinese_log",
313
+ severity: "hard_fail",
314
+ file,
315
+ evidence: `用户项目日志必须使用中文: ${firstLiteral.slice(0, 80)}`,
316
+ suggestion_zh: "将日志事件名和说明改为中文,并保留业务对象 ID、操作人、前后状态等可定位字段。",
317
+ });
318
+ }
319
+ }
320
+ const backendWrite = isBackendImplementationFile(file)
321
+ && (BUSINESS_WRITE_ENDPOINT.test(content) || BUSINESS_WRITE_CALL.test(content) || TRANSACTIONAL_OR_CASCADE_KEYWORDS.test(content));
322
+ if (backendWrite && !hasProjectLog) {
323
+ findings.push({
324
+ category: "missing_log_business_write",
325
+ severity: "hard_fail",
326
+ file,
327
+ evidence: "后端业务写操作缺少结构化业务日志",
328
+ suggestion_zh: "Controller/Service 的新增、更新、删除、状态流转、事务双写、级联校验等操作必须记录业务日志,至少包含业务对象 ID、操作人、事件名和结果。",
329
+ });
330
+ }
331
+ if (backendWrite && hasProjectLog) {
332
+ const logStmts = content.match(/(?:log\.|logger\.|LOG\.)\s*(?:info|warn|error|debug|log|INFO|WARN|ERROR|DEBUG)\s*\([^)]*\)/gi) ?? [];
333
+ const hasLocatableField = logStmts.some((stmt) => /\w+(?:Id|_id|ID|No|Number|Code)\b/i.test(stmt)
334
+ || /\bid\s*[=::]/i.test(stmt)
335
+ || /(?:before|after|prev|old|new|from|to)\s*[=::]/i.test(stmt)
336
+ || /(?:操作人|操作者|operator|userId|user_id|adminId|staffId)\b/i.test(stmt)
337
+ || /(?:resourceId|target|resource)\s*[=::]/i.test(stmt));
338
+ if (!hasLocatableField) {
339
+ findings.push({
340
+ category: "unlocatable_log",
341
+ severity: "hard_fail",
342
+ file,
343
+ evidence: "后端业务写操作日志缺少可定位字段(业务对象ID、操作人、前后状态或目标资源)",
344
+ suggestion_zh: "日志必须包含业务对象 ID、操作人、前后状态或目标资源,不能只写“成功/完成/已更新”。",
345
+ });
346
+ }
347
+ }
348
+ const frontendWrite = isFrontendImplementationFile(file)
349
+ && (FRONTEND_MUTATION_CALL.test(content) || (FRONTEND_STATE_MUTATION.test(content) && /(?:submit|save|delete|status|payment|permission|role|auth|upload|导入|导出|审批|支付|权限|状态)/i.test(`${file} ${content}`)));
350
+ if (frontendWrite && !hasProjectLog) {
351
+ findings.push({
352
+ category: "missing_log_business_write",
353
+ severity: "hard_fail",
354
+ file,
355
+ evidence: "前端业务写操作或关键交互缺少可观测记录",
356
+ suggestion_zh: "表单提交、状态变更、权限动作、支付/敏感操作、上传导入等前端关键交互必须记录可定位日志、埋点或错误上报,至少包含事件名、业务对象 ID 和结果。",
357
+ });
358
+ }
359
+ if (frontendWrite && hasProjectLog) {
360
+ const logStmts = content.match(/(?:log\.|logger\.|LOG\.)\s*(?:info|warn|error|debug|log|INFO|WARN|ERROR|DEBUG)\s*\([^)]*\)/gi) ?? [];
361
+ const hasLocatableField = logStmts.some((stmt) => /\w+(?:Id|_id|ID|No|Number|Code)\b/i.test(stmt)
362
+ || /\bid\s*[=::]/i.test(stmt)
363
+ || /(?:before|after|prev|old|new|from|to)\s*[=::]/i.test(stmt)
364
+ || /(?:operator|userId|user_id|staffId|resourceId|target|resource)\b/i.test(stmt));
365
+ if (!hasLocatableField) {
366
+ findings.push({
367
+ category: "unlocatable_log",
368
+ severity: "hard_fail",
369
+ file,
370
+ evidence: "前端关键交互日志缺少可定位字段(事件对象ID、操作人、前后状态或目标资源)",
371
+ suggestion_zh: "前端日志/埋点必须能定位到业务对象和操作结果,不能只写“提交成功/保存成功”。",
372
+ });
373
+ }
374
+ }
375
+ if (CRITICAL_LOG_KEYWORDS.test(`${file} ${content}`)) {
376
+ const hasWriteOp = /\.(?:save|insert|update|delete|create|refund|charge|pay)\s*\(/i.test(content);
377
+ if (hasWriteOp && !/(?:log\.|logger\.|LOG\.)/i.test(content)) {
378
+ findings.push({
379
+ category: "missing_log_critical",
380
+ severity: "hard_fail",
381
+ file,
382
+ evidence: "支付/金额相关写操作缺少业务日志",
383
+ suggestion_zh: "补充包含对象 ID、操作人、金额前后值和操作结果的结构化日志。",
384
+ });
385
+ }
386
+ // 有日志但不可定位 — 支付/金额写操作的日志必须含对象ID或前后状态
387
+ // 事件词(退款/支付/changed/updated)不是可定位字段
388
+ if (hasWriteOp && /(?:log\.|logger\.|LOG\.)/i.test(content)) {
389
+ const logStmts = content.match(/(?:log\.|logger\.|LOG\.|console\.)\s*(?:info|warn|error|debug|log|INFO|WARN|ERROR|DEBUG)\s*\([^)]*\)/gi) ?? [];
390
+ const hasLocatableField = logStmts.some((stmt) => /\w+(?:Id|_id|ID|No|Number|Code)\b/i.test(stmt)
391
+ || /\bid\s*[=::]/i.test(stmt)
392
+ || /(?:before|after|prev|old|new|from|to)\s*[=::]/i.test(stmt)
393
+ || /(?:操作人|操作者|operator|userId|user_id|adminId|staffId)\b/i.test(stmt)
394
+ || /(?:resourceId|target|resource)\s*[=::]/i.test(stmt));
395
+ if (!hasLocatableField) {
396
+ findings.push({
397
+ category: "unlocatable_log",
398
+ severity: "hard_fail",
399
+ file,
400
+ evidence: "支付/金额写操作日志缺少可定位字段(对象ID、前后状态、操作人/目标资源)",
401
+ suggestion_zh: "日志必须包含对象 ID、前后状态或操作人+目标资源,事件词不是可定位字段。",
402
+ });
403
+ }
404
+ }
405
+ }
406
+ if (SECURITY_LOG_KEYWORDS.test(`${file} ${content}`)) {
407
+ const hasPermissionCheck = /(?:permission|auth|role|check|deny|reject|forbidden|authorize)/i.test(content);
408
+ if (hasPermissionCheck && !/(?:log\.|logger\.|LOG\.)/i.test(content)) {
409
+ findings.push({
410
+ category: "missing_log_critical",
411
+ severity: "hard_fail",
412
+ file,
413
+ evidence: "权限校验逻辑缺少安全日志",
414
+ suggestion_zh: "补充包含操作人、目标资源和拒绝原因的安全日志。",
415
+ });
416
+ }
417
+ }
418
+ const catchBlocks = content.match(/catch\s*\([^)]*\)\s*\{[^}]*\}/g) ?? [];
419
+ for (const block of catchBlocks) {
420
+ const hasLogInBlock = /(?:log\.|logger\.|LOG\.|console\.(error|warn))/i.test(block);
421
+ const onlyReturnOrRethrow = /^(?:\s*(?:return\s+(?:false|null|undefined|""|-1|0|[])|;?\s*\})+\s*$)/i.test(block.replace(/\s+/g, " ").trim());
422
+ if (!hasLogInBlock && (onlyReturnOrRethrow || /return\s+(?:false|null|""|-1|0)/i.test(block))) {
423
+ findings.push({
424
+ category: "catch_swallow",
425
+ severity: "hard_fail",
426
+ file,
427
+ evidence: `catch 块吞异常且无日志: ${block.slice(0, 80).replace(/\n/g, " ")}`,
428
+ suggestion_zh: "至少记录异常类型、消息和影响上下文,不得静默忽略。",
429
+ });
430
+ }
431
+ }
432
+ if (STATE_CHANGE_KEYWORDS.test(`${file} ${content}`)) {
433
+ const hasStateUpdate = /(?:status|state)\s*(?:=|\.set|\.update|transition|change)\s*/i.test(content);
434
+ if (hasStateUpdate && !/(?:log\.|logger\.|LOG\.)/i.test(content)) {
435
+ findings.push({
436
+ category: "missing_log_state_change",
437
+ severity: "warning",
438
+ file,
439
+ evidence: "状态变更逻辑缺少流转日志",
440
+ suggestion_zh: "补充状态流转日志,记录变更前后状态和触发原因。",
441
+ });
442
+ }
443
+ }
444
+ if (EXTERNAL_CALL_KEYWORDS.test(`${file} ${content}`)) {
445
+ const hasExternalCall = /(?:fetch|axios|RestTemplate|WebClient|Feign|http\.request|rpc|grpc)/i.test(content);
446
+ if (hasExternalCall && !/(?:log\.|logger\.|LOG\.)/i.test(content)) {
447
+ findings.push({
448
+ category: "missing_log_external",
449
+ severity: "warning",
450
+ file,
451
+ evidence: "外部调用缺少失败/超时日志",
452
+ suggestion_zh: "补充外部调用失败日志,含目标、响应码和重试信息。",
453
+ });
454
+ }
455
+ }
456
+ if (MIGRATION_KEYWORDS.test(`${file} ${content}`)) {
457
+ const hasDataModification = /(?:UPDATE|INSERT|DELETE|update\(|save\(|delete\(|remove\()/i.test(content);
458
+ if (hasDataModification && !/(?:log\.|logger\.|LOG\.|audit|审计)/i.test(content)) {
459
+ findings.push({
460
+ category: "missing_audit_log",
461
+ severity: "hard_fail",
462
+ file,
463
+ evidence: "数据修复/迁移脚本缺少审计日志",
464
+ suggestion_zh: "补充审计日志,记录修复前值、后值和影响行数。",
465
+ });
466
+ }
467
+ }
468
+ if (hasLog && /console\.(log|debug|info)\s*\(/i.test(content) && !/\.(?:test|spec)\./.test(file)) {
469
+ const isFrontendProd = /\.(?:vue|tsx|jsx|svelte)\b/.test(lowered);
470
+ findings.push({
471
+ category: "raw_console_log",
472
+ severity: isFrontendProd ? "hard_fail" : "warning",
473
+ file,
474
+ evidence: "生产代码使用裸 console.log/debug/info",
475
+ suggestion_zh: isFrontendProd
476
+ ? "前端生产代码禁止裸 console 输出,使用项目封装 logger 或移除。"
477
+ : "使用项目 logger 替代 console.log,确保日志可追踪和可过滤。",
478
+ });
479
+ }
480
+ if (hasLog) {
481
+ const logLines = content.match(/(?:log\.info|logger\.info|LOG\.INFO|console\.log)\s*\(\s*["'](?:error|failed|success|done|ok|ok|完成|失败|错误)["']/gi) ?? [];
482
+ for (const _line of logLines) {
483
+ findings.push({
484
+ category: "unlocatable_log",
485
+ severity: "warning",
486
+ file,
487
+ evidence: "日志仅含通用状态词,无业务对象标识",
488
+ suggestion_zh: "日志必须包含事件名、业务对象 ID 和操作上下文,确保可定位。",
489
+ });
490
+ }
491
+ }
492
+ const loopLogPattern = /(?:for|while|forEach|map|flatMap|reduce)\s*\([^)]*\)\s*\{[^}]*(?:log\.|logger\.|console\.)/i;
493
+ if (loopLogPattern.test(content) && !/(?:batch|批量|each|every|per.?item)/i.test(content)) {
494
+ findings.push({
495
+ category: "noisy_log",
496
+ severity: "advisory",
497
+ file,
498
+ evidence: "循环内可能产生刷屏日志",
499
+ suggestion_zh: "在循环内使用 DEBUG 级别,或汇总后以单条日志输出。",
500
+ });
501
+ }
502
+ }
503
+ return findings;
504
+ }
505
+ /* ── Review: 注释质量检测 ─────────────────────── */
506
+ export function reviewCommentQuality(fileContents) {
507
+ const findings = [];
508
+ for (const [file, content] of Object.entries(fileContents)) {
509
+ if (/\.(?:test|spec)\./.test(file))
510
+ continue;
511
+ if (COMPLEX_RULE_KEYWORDS.test(`${file} ${content}`)) {
512
+ const hasFunction = /(?:function\s+\w+|(?:const|let)\s+\w+\s*=\s*(?:async\s+)?\(|(?:public|private|protected)\s+(?:\w+\s+)+\w+\s*\(|def\s+\w+\s*\(|class\s+\w+)/i.test(content);
513
+ const hasComment = /(?:\/\/|\/\*|\*|<!--)/.test(content);
514
+ if (hasFunction && !hasComment) {
515
+ findings.push({
516
+ category: "missing_comment_complex",
517
+ severity: "warning",
518
+ file,
519
+ evidence: "复杂业务规则缺少注释",
520
+ suggestion_zh: "添加注释说明设计意图(为什么),不得只复述代码行为。",
521
+ });
522
+ }
523
+ }
524
+ const backendComplexRule = isBackendImplementationFile(file)
525
+ && TRANSACTIONAL_OR_CASCADE_KEYWORDS.test(content)
526
+ && /(?:class\s+\w+|(?:public|private|protected)\s+(?:\w+\s+)+\w+\s*\()/i.test(content);
527
+ if (backendComplexRule && !/(?:\/\/|\/\*|\*|@Schema|@ApiModelProperty)/.test(content)) {
528
+ findings.push({
529
+ category: "missing_comment_complex",
530
+ severity: "warning",
531
+ file,
532
+ evidence: "后端复杂业务规则缺少解释性注释",
533
+ suggestion_zh: "级联删除校验、状态转换、事务双写、账号联动、密码重置等规则必须说明业务原因、边界和不变量。",
534
+ });
535
+ }
536
+ const frontendComplexRule = isFrontendImplementationFile(file)
537
+ && /(?:权限|状态|审批|支付|金额|表单联动|级联|缓存一致|乐观更新|回滚|debounce|throttle|optimistic|rollback|permission|status|workflow|payment|validation)/i.test(`${file} ${content}`)
538
+ && /(?:function\s+\w+|const\s+\w+\s*=\s*(?:async\s*)?\(|watch\(|useEffect\(|computed\()/i.test(content);
539
+ if (frontendComplexRule && !/(?:\/\/|\/\*|\*|<!--)/.test(content)) {
540
+ findings.push({
541
+ category: "missing_comment_complex",
542
+ severity: "warning",
543
+ file,
544
+ evidence: "前端复杂交互规则缺少解释性注释",
545
+ suggestion_zh: "权限控制、状态流转、表单联动、乐观更新/回滚、支付等复杂交互必须说明业务原因和边界。",
546
+ });
547
+ }
548
+ if (isBackendDataContractFile(file) && !/\benum\s+\w+/i.test(content)) {
549
+ const lines = content.split(/\r?\n/);
550
+ let fieldCount = 0;
551
+ let documentedFieldCount = 0;
552
+ for (let i = 0; i < lines.length; i++) {
553
+ const line = lines[i] ?? "";
554
+ if (!/\bprivate\s+(?!static\b)(?:final\s+)?[\w<>?,\s]+\s+\w+\s*;/i.test(line))
555
+ continue;
556
+ fieldCount++;
557
+ const window = lines.slice(Math.max(0, i - 4), i).join("\n");
558
+ if (FIELD_DOC_ANNOTATION.test(window))
559
+ documentedFieldCount++;
560
+ }
561
+ if (fieldCount >= 3 && documentedFieldCount < fieldCount) {
562
+ findings.push({
563
+ category: "missing_domain_field_comment",
564
+ severity: "hard_fail",
565
+ file,
566
+ evidence: `领域/DTO 字段说明不完整: ${documentedFieldCount}/${fieldCount} 个字段具备说明`,
567
+ suggestion_zh: "Entity、Request、Response、VO 等用户项目契约类的每个业务字段都要有中文含义说明或 OpenAPI/Schema 注解,避免后续 AI 开发误解字段语义。",
568
+ });
569
+ }
570
+ }
571
+ const commentMatches = content.matchAll(/\/\/\s*(.+)|\/\*\*?\s*([\s\S]*?)\s*\*\//g);
572
+ for (const match of commentMatches) {
573
+ const commentText = (match[1] ?? match[2] ?? "").trim();
574
+ if (!commentText)
575
+ continue;
576
+ if (/[A-Za-z\u4e00-\u9fff]/.test(commentText) && !containsChinese(commentText)) {
577
+ findings.push({
578
+ category: "non_chinese_comment",
579
+ severity: "hard_fail",
580
+ file,
581
+ evidence: `用户项目注释必须使用中文: ${commentText.slice(0, 80)}`,
582
+ suggestion_zh: "将用户项目注释改为中文;保留必要的英文代码标识符,但业务说明、方法说明和风险说明必须中文表达。",
583
+ });
584
+ }
585
+ const lineStart = match.index ?? 0;
586
+ const afterComment = content.slice(lineStart + match[0].length, lineStart + match[0].length + 200);
587
+ const nextCodeLine = afterComment.split("\n").find((l) => l.trim().length > 0 && !l.trim().startsWith("//") && !l.trim().startsWith("*"));
588
+ if (nextCodeLine) {
589
+ const codeStripped = nextCodeLine.replace(/[{}()\[\];,.]/g, "").trim();
590
+ const commentStripped = commentText.replace(/[{}()\[\];,.]/g, "").trim();
591
+ const codeLower = codeStripped.toLowerCase();
592
+ const commentLower = commentStripped.toLowerCase();
593
+ if (codeLower.length > 10 && commentLower.length > 10) {
594
+ const codeWords = new Set(codeLower.split(/\s+/));
595
+ const commentWords = new Set(commentLower.split(/\s+/));
596
+ let overlap = 0;
597
+ const total = Math.max(codeWords.size, commentWords.size);
598
+ for (const w of codeWords) {
599
+ if (commentWords.has(w) && w.length > 3)
600
+ overlap++;
601
+ }
602
+ if (total > 0 && overlap / total > 0.7) {
603
+ findings.push({
604
+ category: "verbose_comment",
605
+ severity: "advisory",
606
+ file,
607
+ evidence: `注释仅复述代码: "${commentText.slice(0, 60)}"`,
608
+ suggestion_zh: "删除废话注释;如需保留,改为说明设计意图或业务背景。",
609
+ });
610
+ }
611
+ }
612
+ }
613
+ }
614
+ }
615
+ return findings;
616
+ }
617
+ /* ── Review: 项目 logger 使用检查 ─────────────── */
618
+ export function reviewLoggerUsage(fileContents, loggerInfo) {
619
+ const findings = [];
620
+ if (loggerInfo.type === "frontend_project" || loggerInfo.type === "none_detected")
621
+ return findings;
622
+ for (const [file, content] of Object.entries(fileContents)) {
623
+ if (/\.(?:test|spec|d\.ts|\.types\.ts)\./.test(file))
624
+ continue;
625
+ const hasAnyLog = /(?:log\.|logger\.|LOG\.|console\.)/i.test(content);
626
+ if (!hasAnyLog)
627
+ continue;
628
+ const usesProjectLogger = new RegExp(loggerInfo.call_pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\./g, "\\."), "i").test(content);
629
+ const usesConsoleDirectly = /console\.(log|debug|info|warn|error)\s*\(/i.test(content);
630
+ if (usesConsoleDirectly && !usesProjectLogger && loggerInfo.confidence === "high") {
631
+ findings.push({
632
+ category: "wrong_logger",
633
+ severity: "warning",
634
+ file,
635
+ evidence: `使用裸 console 而非项目 logger (${loggerInfo.type})`,
636
+ suggestion_zh: `使用项目 ${loggerInfo.type} logger 确保日志可追踪和可过滤。`,
637
+ });
638
+ }
639
+ }
640
+ return findings;
641
+ }
642
+ /* ── 综合审查入口 ─────────────────────────────── */
643
+ export function reviewCodeObservability(fileContents, loggerInfo) {
644
+ const findings = [];
645
+ findings.push(...detectSensitiveLogs(fileContents));
646
+ findings.push(...reviewMissingLogs(fileContents));
647
+ findings.push(...reviewCommentQuality(fileContents));
648
+ const resolvedLogger = loggerInfo ?? detectProjectLogger(fileContents);
649
+ findings.push(...reviewLoggerUsage(fileContents, resolvedLogger));
650
+ return findings;
651
+ }
652
+ /* ── 低风险跳过判断 ───────────────────────────── */
653
+ export function isLowRiskChange(intent, changedFiles = []) {
654
+ const text = `${intent} ${changedFiles.join(" ")}`;
655
+ if (/^(?:fix\s+)?(?:typo|错别字|拼写|文案|README|注释样式|格式化|lint|import\s*排序)/i.test(text.trim())) {
656
+ return { low_risk: true, reason_zh: "纯文案或格式修改,跳过可维护性/可观测性检查" };
657
+ }
658
+ if (/^(?:get|set|is|has|can)\w*\s*[\({]/i.test(text.trim()) && changedFiles.length <= 1) {
659
+ return { low_risk: true, reason_zh: "简单 getter/setter,跳过可维护性/可观测性检查" };
660
+ }
661
+ const allStyle = changedFiles.length > 0 && changedFiles.every((f) => /\.(?:css|scss|less|styl|style)$/i.test(f));
662
+ if (allStyle) {
663
+ return { low_risk: true, reason_zh: "纯样式修改,跳过可维护性/可观测性检查" };
664
+ }
665
+ const allTest = changedFiles.length > 0 && changedFiles.every((f) => /\.(?:test|spec)\./i.test(f));
666
+ if (allTest && !OBSERVABILITY_KEYWORDS.test(text)) {
667
+ return { low_risk: true, reason_zh: "纯测试文件修改,跳过可维护性/可观测性检查" };
668
+ }
669
+ return { low_risk: false, reason_zh: "" };
670
+ }
671
+ /* ── 阻断判断 ─────────────────────────────────── */
672
+ export function hasBlockingObservabilityFindings(findings) {
673
+ return findings.some((f) => f.severity === "hard_fail");
674
+ }
675
+ export function hasP1OrAboveFindings(findings) {
676
+ return findings.some((f) => f.severity === "hard_fail" || f.severity === "warning");
677
+ }
678
+ /* ── sf_verify changed_files 校验 ─────────────── */
679
+ export function verifyChangedFilesObservability(input) {
680
+ const filtered = {};
681
+ for (const [file, content] of Object.entries(input.file_contents)) {
682
+ if (/\.(?:test|spec|d\.ts|\.types\.ts|\.md|\.json|\.yaml|\.yml|\.lock|\.map)$/i.test(file))
683
+ continue;
684
+ if (input.changed_files.length > 0 && !input.changed_files.some((cf) => file.endsWith(cf) || cf.endsWith(file)))
685
+ continue;
686
+ filtered[file] = content;
687
+ }
688
+ if (Object.keys(filtered).length === 0) {
689
+ return [];
690
+ }
691
+ const lowRisk = isLowRiskChange(input.intent, input.changed_files);
692
+ if (lowRisk.low_risk) {
693
+ return [{ category: "low_risk_skip", severity: "info", file: "", evidence: lowRisk.reason_zh, suggestion_zh: "" }];
694
+ }
695
+ return reviewCodeObservability(filtered);
696
+ }
697
+ /* ── sf_deliver 交付阻断 ──────────────────────── */
698
+ export function evaluateDeliveryBlock(findings) {
699
+ const blocking = findings.filter((f) => f.severity === "hard_fail"
700
+ || (f.severity === "warning" && f.category === "missing_comment_complex"));
701
+ if (blocking.length > 0) {
702
+ const reasons = blocking.map((f) => `[${f.category}] ${f.file}: ${f.evidence}`).join("\n");
703
+ return {
704
+ blocked: true,
705
+ reason_zh: `代码可维护性/可观测性存在 ${blocking.length} 项阻断:\n${reasons}`,
706
+ blocking_findings: blocking,
707
+ };
708
+ }
709
+ return { blocked: false, reason_zh: "", blocking_findings: [] };
710
+ }
711
+ //# sourceMappingURL=code_maintainability_observability_contract.js.map