@stalfh233/omc-cli 0.0.0-stage → 0.4.10

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 (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -3
  3. package/coverage/m1-coverage-manifest-v1.json +1278 -0
  4. package/dist/approval-token.js +102 -0
  5. package/dist/args.js +25 -0
  6. package/dist/artifacts.js +119 -0
  7. package/dist/bench/call-face-eval.js +256 -0
  8. package/dist/bench/context-attribution.js +151 -0
  9. package/dist/bench/discovery-cost-eval.js +230 -0
  10. package/dist/bench/driver.js +81 -0
  11. package/dist/bench/evals.js +236 -0
  12. package/dist/bench/fake-http-server.js +65 -0
  13. package/dist/bench/instrument.js +87 -0
  14. package/dist/bench/intent-face-eval.js +343 -0
  15. package/dist/bench/run.js +166 -0
  16. package/dist/bench/scenario.js +343 -0
  17. package/dist/bench/types.js +76 -0
  18. package/dist/bi-wire.js +41 -0
  19. package/dist/bizservice-config.js +581 -0
  20. package/dist/call.js +153 -0
  21. package/dist/capability-absences.js +23 -0
  22. package/dist/capability-overview.js +492 -0
  23. package/dist/capability-shape.js +154 -0
  24. package/dist/cli-contract.js +70 -0
  25. package/dist/cli-output.js +73 -0
  26. package/dist/cli.js +1418 -0
  27. package/dist/code-rules.js +69 -0
  28. package/dist/command-transport.js +155 -0
  29. package/dist/config-store.js +195 -0
  30. package/dist/context.js +21 -0
  31. package/dist/contract-consistency.js +66 -0
  32. package/dist/contract-resources.js +62 -0
  33. package/dist/coverage-consistency.js +62 -0
  34. package/dist/coverage-registry.js +76 -0
  35. package/dist/coverage.js +130 -0
  36. package/dist/data-list-filter.js +114 -0
  37. package/dist/discovery.js +390 -0
  38. package/dist/endpoints.js +154 -0
  39. package/dist/environment-policy.js +26 -0
  40. package/dist/execution-metadata.js +1092 -0
  41. package/dist/fake/app.js +45 -0
  42. package/dist/fake/b2-registration.js +594 -0
  43. package/dist/fake/businessrule.js +242 -0
  44. package/dist/fake/datarule.js +72 -0
  45. package/dist/fake/dictionary.js +108 -0
  46. package/dist/fake/environment.js +30 -0
  47. package/dist/fake/field.js +167 -0
  48. package/dist/fake/form.js +101 -0
  49. package/dist/fake/index.js +121 -0
  50. package/dist/fake/list-view.js +289 -0
  51. package/dist/fake/model.js +136 -0
  52. package/dist/fake/online-js.js +18 -0
  53. package/dist/fake/report.js +253 -0
  54. package/dist/fake/routes.js +47 -0
  55. package/dist/fake/rule-lifecycle.js +37 -0
  56. package/dist/fake/runtime-data.js +367 -0
  57. package/dist/fake/state.js +67 -0
  58. package/dist/fake/workflow.js +364 -0
  59. package/dist/field-change.js +200 -0
  60. package/dist/field-families.js +896 -0
  61. package/dist/form-layout.js +111 -0
  62. package/dist/form-support.js +821 -0
  63. package/dist/goal-routes.js +468 -0
  64. package/dist/governed-execution.js +87 -0
  65. package/dist/human-summary.js +212 -0
  66. package/dist/identity.js +62 -0
  67. package/dist/intent/baseline.js +57 -0
  68. package/dist/intent/capabilities/bizservice.js +274 -0
  69. package/dist/intent/capabilities/businessrule.js +493 -0
  70. package/dist/intent/capabilities/datarule.js +187 -0
  71. package/dist/intent/capabilities/field.js +545 -0
  72. package/dist/intent/capabilities/form.js +136 -0
  73. package/dist/intent/capabilities/index.js +64 -0
  74. package/dist/intent/capabilities/listview.js +157 -0
  75. package/dist/intent/capabilities/model.js +100 -0
  76. package/dist/intent/capabilities/onlinejs.js +108 -0
  77. package/dist/intent/capabilities/report.js +355 -0
  78. package/dist/intent/capabilities/workflow.js +458 -0
  79. package/dist/intent/capability.js +6 -0
  80. package/dist/intent/cli.js +91 -0
  81. package/dist/intent/compare.js +56 -0
  82. package/dist/intent/compiler.js +79 -0
  83. package/dist/intent/dsl.js +129 -0
  84. package/dist/intent/plan-file.js +63 -0
  85. package/dist/intent/readback.js +65 -0
  86. package/dist/intent/schema.js +158 -0
  87. package/dist/intent/validation.js +30 -0
  88. package/dist/intent/yaml.js +315 -0
  89. package/dist/json-column.js +68 -0
  90. package/dist/lanes/app-contract.js +95 -0
  91. package/dist/lanes/app-coverage.js +16 -0
  92. package/dist/lanes/app.js +174 -0
  93. package/dist/lanes/apply-changes.js +231 -0
  94. package/dist/lanes/b2-registration-contract.js +292 -0
  95. package/dist/lanes/b2-registration-coverage.js +48 -0
  96. package/dist/lanes/b2-registration.js +1187 -0
  97. package/dist/lanes/businessrule-contract.js +232 -0
  98. package/dist/lanes/businessrule-coverage.js +16 -0
  99. package/dist/lanes/businessrule.js +221 -0
  100. package/dist/lanes/contract-support.js +65 -0
  101. package/dist/lanes/coverage-declaration.js +9 -0
  102. package/dist/lanes/datarule-contract.js +155 -0
  103. package/dist/lanes/datarule-coverage.js +19 -0
  104. package/dist/lanes/datarule-protocol.js +308 -0
  105. package/dist/lanes/datarule.js +818 -0
  106. package/dist/lanes/dictionary-contract.js +78 -0
  107. package/dist/lanes/dictionary-coverage.js +24 -0
  108. package/dist/lanes/dictionary.js +235 -0
  109. package/dist/lanes/environment-contract.js +70 -0
  110. package/dist/lanes/environment-coverage.js +14 -0
  111. package/dist/lanes/environment.js +163 -0
  112. package/dist/lanes/field-contract.js +223 -0
  113. package/dist/lanes/field-coverage.js +27 -0
  114. package/dist/lanes/field.js +374 -0
  115. package/dist/lanes/form-contract.js +97 -0
  116. package/dist/lanes/form-coverage.js +16 -0
  117. package/dist/lanes/form.js +185 -0
  118. package/dist/lanes/lane-ids.js +34 -0
  119. package/dist/lanes/list-view-contract.js +175 -0
  120. package/dist/lanes/list-view-coverage.js +20 -0
  121. package/dist/lanes/list-view-shapes.js +1207 -0
  122. package/dist/lanes/list-view.js +578 -0
  123. package/dist/lanes/meta-contract.js +85 -0
  124. package/dist/lanes/meta.js +255 -0
  125. package/dist/lanes/model-contract.js +168 -0
  126. package/dist/lanes/model-coverage.js +20 -0
  127. package/dist/lanes/model.js +986 -0
  128. package/dist/lanes/online-js-contract.js +99 -0
  129. package/dist/lanes/online-js-coverage.js +28 -0
  130. package/dist/lanes/online-js.js +127 -0
  131. package/dist/lanes/report-contract.js +144 -0
  132. package/dist/lanes/report-coverage.js +21 -0
  133. package/dist/lanes/report.js +476 -0
  134. package/dist/lanes/rule-graph.js +1846 -0
  135. package/dist/lanes/rule-lifecycle-contract.js +92 -0
  136. package/dist/lanes/rule-lifecycle-coverage.js +20 -0
  137. package/dist/lanes/rule-lifecycle.js +176 -0
  138. package/dist/lanes/runtime-data-contract.js +264 -0
  139. package/dist/lanes/runtime-data-coverage.js +25 -0
  140. package/dist/lanes/runtime-data.js +1054 -0
  141. package/dist/lanes/workflow-contract.js +223 -0
  142. package/dist/lanes/workflow-coverage.js +40 -0
  143. package/dist/lanes/workflow.js +1813 -0
  144. package/dist/online-js-layout.js +58 -0
  145. package/dist/online-js-source.js +276 -0
  146. package/dist/package-tool.js +51 -0
  147. package/dist/package.js +73 -0
  148. package/dist/plan.js +73 -0
  149. package/dist/read.js +144 -0
  150. package/dist/redact.js +28 -0
  151. package/dist/rule-support.js +587 -0
  152. package/dist/runtime-support.js +134 -0
  153. package/dist/server.js +92 -0
  154. package/dist/session-manager.js +30 -0
  155. package/dist/session.js +149 -0
  156. package/dist/skills.js +112 -0
  157. package/dist/support.js +98 -0
  158. package/dist/tool-types.js +127 -0
  159. package/dist/tools.js +59 -0
  160. package/dist/usage-log.js +197 -0
  161. package/dist/wire.js +287 -0
  162. package/dist/workflow-support.js +99 -0
  163. package/dist/write-lease.js +26 -0
  164. package/dist/write-lock.js +109 -0
  165. package/dist/write.js +176 -0
  166. package/dist/zip.js +156 -0
  167. package/docs/tool-surface-map.md +42 -0
  168. package/package.json +71 -6
  169. package/skills/omc-acceptance-criteria.md +47 -0
  170. package/skills/omc-business-configuration.md +257 -0
  171. package/skills/omc-capabilities.md +197 -0
  172. package/skills/omc-capability-scouting.md +55 -0
  173. package/skills/omc-config-draft-review.md +171 -0
  174. package/skills/omc-five-piece-flow.md +35 -0
  175. package/skills/omc-glossary.md +86 -0
  176. package/skills/omc-refusals.md +110 -0
  177. package/skills/omc-requirement-analysis.md +161 -0
  178. package/skills/omc-requirement-vocabulary.md +51 -0
  179. package/skills/omc-start-here.md +82 -0
  180. package/skills/omc-tool-selection.md +119 -0
  181. package/skills/omc-write-hazards.md +87 -0
@@ -0,0 +1,232 @@
1
+ import { behaviorBinding, CONTRACT_FAMILY, CONTRACT_REFERENCE_VERSION, assertDescriptionCarries, assertLaneContract, describeTool, emitterBinding } from "./contract-support.js";
2
+ import { LANES } from "./lane-ids.js";
3
+ import { RULE_ACTION_TYPES, RULE_CODE_TYPES, RULE_CONDITION_TYPES, RULE_DATA_SOURCE_TYPES, RULE_NODE_TEMPLATES, RULE_NODE_TYPES, RULE_SCHEDULER_CYCLE_TYPES, RULE_SCHEDULER_END_TYPES, RULE_SCHEDULER_EXECUTE_TYPES, RULE_SCHEDULER_SETTING_TEMPLATE, RULE_SCHEDULER_TRIGGER_TIME_TYPES, RULE_VALUE_TYPES } from "./rule-graph.js";
4
+ /** Rule `enabled` is the runtime dispatch truth; `status` may lag (platform quirk). */
5
+ export const RULE_STATUS = Object.freeze({ enabled: "ENABLE", disabled: "DISABLED" });
6
+ /** Enum re-exports: rule-graph.ts is the single source (validator + fake + contract all import it). */
7
+ export { RULE_VALUE_TYPES, RULE_ACTION_TYPES, RULE_CONDITION_TYPES, RULE_NODE_TYPES, RULE_NODE_TEMPLATES };
8
+ /** The DISABLED dead-state body every fresh rule.save upsert writes. */
9
+ export const RULE_CREATE_BODY = Object.freeze({
10
+ enabled: false,
11
+ status: RULE_STATUS.disabled,
12
+ defaultRule: false,
13
+ developmentMode: 0,
14
+ });
15
+ export const RULE_PLATFORM_QUIRK = "enabled is the dispatch truth; status may still read DISABLED after enable — never infer enable state from status";
16
+ const RULE_NODE_GRAPH_EXAMPLE = '[{"nodeCode":"Start","nodeName":"开始","nodeType":"START",x,y,height,width},{"nodeCode":"UPDATE_1","nodeType":"UPDATE","targetObjectCode":"<target schemaCode>","inputParam":"_input","updateNodeType":"UPDATE_MODEL","execFormMapping":false,"filterCondition":{"mainConditions":[[{"targetSchemaDataItem":"<target field>","ruleConditionType":"EQ","ruleValueType":"DYNAMIC","currentSchemaDataItem":"<trigger field>"}]],"sheetConditions":[]},"dataActions":[{"targetDataItem":"<target field>","ruleValueType":"DYNAMIC","value":"","ruleActionType":"EQUALS","currentDataItemValue":"<trigger field>"}],x,y,height,width},{"nodeCode":"End","nodeName":"结束","nodeType":"END",x,y,height,width}]';
17
+ export const RULE_NODE_GRAPH_WIRE = Object.freeze({
18
+ inputParam: "_input",
19
+ valueTypes: RULE_VALUE_TYPES.join("/"),
20
+ actionTypes: RULE_ACTION_TYPES.join("/"),
21
+ conditionTypes: RULE_CONDITION_TYPES.join("/"),
22
+ nodeTypes: RULE_NODE_TYPES.join("/"),
23
+ dynamicConditionSource: "currentSchemaDataItem",
24
+ dynamicActionSource: "currentDataItemValue",
25
+ dynamicConditionValue: "filter value is omitted for DYNAMIC",
26
+ dynamicActionValue: "action value is empty for DYNAMIC",
27
+ dynamicNameKeys: "targetDataItemName/currentDataItemValueName",
28
+ fixedValueTypes: "select 族(RADIO/DROPDOWN_BOX/DROPDOWN_MULTI_BOX/CHECKBOX)= 裸选项字符串(如 \"待处理\")——RadioConverter.java:41-88:JSON 数组串 [\"x\"] 在 (JSONObject)selection 强转处 ClassCastException 被吞、原样透传导致 *_key 失配为 null(live 2026-09-15 复核修正 ticket 09 的数组串记载,旧模型数组形历史数据是老版本写入、同病);对象数组 [{key,label}] 形态是条件键值映射的另一用途,非 FIXED 赋值;number/boolean = native;text = bare string",
29
+ presetSystemNode: "预设触发规则 Create/Update/Delete 的图必须保留平台系统节点 SYSTEM_CREATE/SYSTEM_UPDATE/SYSTEM_DELETE(nodeCode Create/Update/Delete,isSystemNode:true)——它是触发行持久化节点;tool 自动补在 START 之后,调用方只写自己的动作节点(2026-09-12 live:缺它则行永远停在 DRAFT)。**不要往预置规则里放类型不匹配的 SYSTEM_* 节点**(如 Create 规则塞 SYSTEM_UPDATE)——错配会让触发行写错、data.submit 报 50000(真机 2026-09-25,工具已 fail closed `rule-graph-preset-system-node-mismatch`);省略 SYSTEM_* 即让工具注入正确的那个。",
30
+ schedulerSetting: "SCHEDULER 节点无时机字段,调度参数在规则行 schedulerSetting(JSON 字符串,bizRuleType:2);缺 schedulerSetting 则规则保存成功但永不触发",
31
+ triggerDispatch: "触发分发铁律(ticket 111 源码铁证):数据新增/修改/删除只分发到预置 code 行——DomainServiceImpl.java:226-314 经 getBusinessRuleOfExecutable(schemaCode, DefaultBusinessRuleType.CREATE/UPDATE/DELETE.getCode()) 按 code 字面量精确查询,绝不扫描 bizRuleType=0 的自定义规则;自定义 ruleCode 只能由按钮 targetCode(HistoricalRuleHandler.java:116-145)、流程事件 bizActions BIZ_RULE(InstanceContext.java:421-438)或显式 executeMethod 触发;定时入口强制 bizRuleType=TIMING_TASK(2)(BizRuleScheduling.java:202-205)。要求数据事件自动触发 → 动作必须挂在预置 Create/Update/Delete 行上",
32
+ systemNodeFamilies: "系统/流程节点 GET_LIST(dataSourceType:GET_LIST 原生列表 / BIZ_SERVICE 绑 BizMethod)、LOAD/CANCEL/AVAILABLE(isSystemNode:true)为平台预置系统节点,模板见 nodeTemplates",
33
+ bizMethodMapping: `BIZ_ACTION / GET_LIST(BIZ_SERVICE) 的 methodMapping:{schemaCode,serviceCode,serviceMethodCode,methodCode?,inputMappings[],outputMappings[]};serviceCode+serviceMethodCode 是集成方法存在性对,methodCode 是模型级 BizMethod 注册槽。映射元素 = 8.6.26 MappingModel,codeType 按 int:${RULE_CODE_TYPES.map((t, i) => `${i}=${t}`).join("/")}(1=PROPERTY 须带 bizCode;0=CONST)。映射元素只校验语义键,其余键非语义透传(工具不按版本增删;index/required/displayName/bizCodeName 等 v2 键原样随行、模板不产出);required 以方法参数面为权威自动镜像(面缺席则映射保持缺席,显式矛盾本地拒绝)`,
34
+ bizMethodValidation: "rule.save 写前 wire 校验:图内每个 methodMapping 的服务+方法必须经 bizservice get_by_code + list_by_service_code 解析存在,且 inputMappings/outputMappings 的 serviceMethodParameterCode 必须命中方法声明的参数面,否则 refused(平台 update/publish 只拦服务缺失 BUSINESS_SERVICE_NOT_EXIST;publish 干门 550018 同语义拒参数面悬空——live 2026-09-12);RESTFUL/DATA_SQL 绑定目标必须带适配器必需 configJson 键——只有名字/参数面的注册壳 refused `rule-graph-bizmethod-registration-shell`(INTERNAL/扩展包方法不受此门)",
35
+ bizActionPersistence: "BIZ_ACTION 的 outputMappings 结果由 SYSTEM_CREATE/SYSTEM_UPDATE 持久化节点消费,故动作节点必须排在系统节点之前(Start→BizAction→SYSTEM_CREATE→End),否则合并结果不落库(live 2026-09-12 ticket 36 铁证:系统节点在动作前时 echoResult 恒 null,重排后回写生效——IntegrationServiceExecutor.mergeResult 只刷新 _input,SystemCreateActivityBehavior 持久化其 _input 输入);injectDefaultSystemNode 默认把系统节点补在 START 之后,有回写动作时须显式提供系统节点并置于动作之后",
36
+ triggerConditions: "节点触发条件(平台 i18n 模块 196233 节点属性表;evidence/form-conditions-and-rules-truth.md §4):UPDATE.filterCondition=**查找满足条件的主表数据**(要更新哪条;空 mainConditions=无条件下更新整表,bench23 病灶,干 lint 出 `rule-graph-filter-empty-main` warning)、UPDATE.filterSubCondition=子表条件;CREATE.findMainData=查找满足条件的主表数据(只影响是否新增)、CREATE.filterCondition=存在符合数据则不新增(aggregation 布尔=分组/单条新增);DELETE.filterCondition=删除满足条件的数据;ASSIGN.filterCondition=满足条件时赋值、triggerType∈CURRENT/RULE_VARIABLE;BIZ_ACTION.execConditionGroups=满足条件时执行业务方法;BRANCH.sourceInputMethod(PRE_OUT/OTHER_MODEL)/exitConditionGroups;通用 dataCondition=输入数据满足条件时执行、childTriggerConditionType∈ADD/MODIFY/DELETE;路由 routeCondition/matchType(ANY/ALL/CUSTOM)。除 BRANCH.exitConditionGroups 外,条件组与 filterCondition 同构 [[{targetSchemaDataItem,ruleConditionType,ruleValueType,...}]](DYNAMIC 来源键 currentSchemaDataItem);**BRANCH.exitConditionGroups 例外**:反序列化为 DataFilterCondition,出口叶用 [[{currentSchemaDataItem,ruleDataConditionType,ruleValueType,value}]](照抄 UI 键会 save+publish 过但运行时 50000)。",
37
+ example: RULE_NODE_GRAPH_EXAMPLE,
38
+ draftRowNote: "目标行查询不匹配 DRAFT 行。",
39
+ });
40
+ /**
41
+ * The rule-node-graph wire contract, generated from the frozen facts above.
42
+ * The `_input` rule is the 2026-09-11 ticket-02 finding: `inputParam` must be
43
+ * the literal `"_input"` (RuleExecutorImpl.saveRuleInput registers the trigger
44
+ * data under that name); a schemaCode literal only passes the dry-gate and
45
+ * silently no-ops at runtime.
46
+ */
47
+ export function ruleNodeGraphDescription() {
48
+ const wire = RULE_NODE_GRAPH_WIRE;
49
+ return `rule nodes (array or pre-serialized JSON string). 自动布局:线性图(无 BRANCH)若缺坐标或节点框重叠,工具自动按平台标准单列铺开(x=327,158×40,y=36/116/196…,间距 80);已可正常渲染的布局与分支图原样保留——调用方无需手算几何。Live-proven cross-model UPDATE shape (2026-09-11, tickets 02/09): ${wire.example}. 铁律:inputParam 必须是 "${wire.inputParam}"(RuleExecutorImpl.saveRuleInput 以该名注册触发数据,schemaCode 字面量只过 dry-gate、运行时静默空转);ruleValueType ∈ ${wire.valueTypes};ruleActionType ∈ ${wire.actionTypes};ruleConditionType ∈ ${wire.conditionTypes}(BRANCH 出口叶例外,键见 BRANCH 模板);DYNAMIC 过滤来源必须放 ${wire.dynamicConditionSource},DYNAMIC 赋值来源必须放 ${wire.dynamicActionSource},${wire.dynamicConditionValue};${wire.dynamicActionValue},展示名惯例为 ${wire.dynamicNameKeys};FIXED 值分型:${wire.fixedValueTypes}。${wire.presetSystemNode}。触发分发:${wire.triggerDispatch}。${wire.draftRowNote} nodeType ∈ ${wire.nodeTypes}。${wire.systemNodeFamilies}。${wire.bizMethodMapping}。${wire.bizActionPersistence}。触发条件:${wire.triggerConditions}。${wire.schedulerSetting}。`;
50
+ }
51
+ export const RULE_ROUTES_DESCRIPTION = "edges, e.g. [{preNode:'Start',postNode:'UPDATE_1',points:[..],routeCondition:true,matchType:'ANY'}] (or a JSON string). 工具自动规范化:缺失的 points 由节点边中点(源底边中点→目标顶边中点)推导,缺失的 routeCondition/matchType 补 true/ANY;调用方无需手算坐标。";
52
+ export const RULE_BIZ_RULE_TYPE_DESCRIPTION = "rule trigger family; 0 = data operation, 1 = get-list, 2 = timing task (8.6.26 BizRuleType)";
53
+ /**
54
+ * Rule-level scheduler shape, emitted as a JSON string into the rule row's
55
+ * `schedulerSetting` column. Only meaningful with a SCHEDULER node /
56
+ * `bizRuleType:2`; the node itself carries no timing fields.
57
+ */
58
+ export function schedulerSettingDescription() {
59
+ const template = JSON.stringify(RULE_SCHEDULER_SETTING_TEMPLATE);
60
+ return `SCHEDULER 规则的调度参数(对象或 JSON 字符串;写在规则行 schedulerSetting,不在节点上)。形状示例:${template}。枚举:triggerTimeType ∈ ${RULE_SCHEDULER_TRIGGER_TIME_TYPES.join("/")};executeType ∈ ${RULE_SCHEDULER_EXECUTE_TYPES.join("/")};cycleType ∈ ${RULE_SCHEDULER_CYCLE_TYPES.join("/")};endType ∈ ${RULE_SCHEDULER_END_TYPES.join("/")};dataSourceType ∈ ${RULE_DATA_SOURCE_TYPES.join("/")};triggerTimeType=DATE_FIELD 时必须带 triggerDataItem。证据:8.6.26 SchedulerSetting.java:27-52。`;
61
+ }
62
+ const RULE_SAVE_DESCRIPTION = describeTool("保存业务规则(upsert):`ruleCode` 已存在则 `businessrule/update`,否则 `businessrule/create`。", [
63
+ "这样可把规则配置进**平台预置触发规则**(`Create`=数据新增、`Update`=数据更新、`Delete`=数据删除、`Available`=流程生效、`Cancel`=流程作废、`GetList`=获取集合、`Load`=加载表单详情),即在触发规则上追加动作节点。",
64
+ "`nodeGraph` 传 `{ nodes, routes, bizRuleType?, schedulerSetting? }`(可为数组或 JSON 字符串);工具拼成平台要求的 `node`/`route`/`schedulerSetting` JSON 字符串,保持 `enabled:false`/`status:DISABLED`。",
65
+ "**START/END 必须显式给出(2026-09-22 实测,勿与 cp-model 的 scripts/rule.js 混淆——那套会自动补,本工具不补)**:`nodes` 必须含恰好 1 个 START 与 ≥1 个 END,缺了干 lint 直接拒(rule-graph-start-count / rule-graph-end-missing);工具只合成 CUSTOM_NODE/SYSTEM_* 预置节点,不补 START/END。数据事件规则必须挂平台预置 `Create` 触发行才分发;跨表落库用 SYSTEM_CREATE 系统节点,动作节点排其前。",
66
+ "**系统/定时/流程节点(ticket 27)**:GET_LIST/LOAD/AVAILABLE/CANCEL/SCHEDULER/EXCEPTION/HINT 已入已知集并有形状模板;SCHEDULER 必须同时在 `nodeGraph.schedulerSetting` 传规则级调度形状(节点上无时机字段,缺它则规则保存成功但永不触发)。",
67
+ "**条件执行**:① 单分支→动作节点自身 `filterCondition`(UPDATE/DELETE/ASSIGN 的 `mainConditions`:身份匹配 DYNAMIC + 固定条件 FIXED,同叶组 AND)。② 多路分支→`BRANCH` 节点:出口叶(DataFilterCondition)键为 `{currentSchemaDataItem(左值),ruleDataConditionType(操作符),ruleValueType,value}`,出路由 `routeCondition:true` 走条件、`false` 兜底 else。**别照抄其它条件组的 targetSchemaDataItem+ruleConditionType——未知键 save+publish 过但运行时 50000(lint fail closed `rule-graph-branch-condition-keys`)**。按数据走流程流转用 `ruleConditionGroup`。",
68
+ "**生命周期铁律(2026-09-11 live 取证)**:update 只写 DRAFT 工作行,get/list 读 PUBLISH 面因此看不到变化——必须再 `rule.enable`(publish 出新版本 + 翻 DRAFT 启用开关)才会生效;数据事件分发:DRAFT 保存不触发,submit(DRAFT→COMPLETED)触发 Create。保存 ≠ 设计器可见:readback 的 `published:false` + nextAction 标明尚未发布;传 `publish:true` 可在同一次调用内发布新版本(仅发布,启用仍走 `rule.enable`)。",
69
+ "**BizMethod 绑定(ticket 35/96)**:BIZ_ACTION / GET_LIST(BIZ_SERVICE) 的 methodMapping 须逐键对 8.6.26 DTO(codeType int 0=CONST/1=PROPERTY…;非语义键原样透传);写前 wire 校验服务+方法真实存在(get_by_code + list_by_service_code),悬空即 refused `rule-graph-bizmethod-unresolved`——平台只拦服务不拦方法,绝不用注册壳冒充已发布 BizMethod;RESTFUL/DATA_SQL 方法必须带适配器必需 configJson 键,只有名字/参数面的注册壳 refused `rule-graph-bizmethod-registration-shell`;映射 `required` 以方法参数面为权威镜像(省略→按面填入,面缺席→保持缺席,显式矛盾→本地拒绝零写入)。",
70
+ "**幻影 schema 门(ticket 96 live 事故)**:rule.save 写前先以 bizmodels/get_all 探测模型存在(缺失 → refused `rule-schema-unresolved`,零规则面 wire 调用)——平台的 list/save 面会对任意 schemaCode 自动补建 7 条预置规则且 API 删除不可达(list 即重灌),幻影 schema 必须在写前拒绝。",
71
+ "**输出回写排序(ticket 36 live 铁证)**:BIZ_ACTION 的 outputMappings 由 SYSTEM_CREATE/SYSTEM_UPDATE 持久化节点消费,动作节点必须排在系统节点之前(`Start→BizAction→SYSTEM_CREATE→End`);工具默认把系统节点补在 START 之后,有回写动作时须显式提供系统节点并置于动作之后,否则合并结果不落库(echoResult 恒 null)。",
72
+ ]);
73
+ export const BUSINESSRULE_LANE_CONTRACT = Object.freeze({
74
+ lane: LANES.businessrule,
75
+ family: CONTRACT_FAMILY,
76
+ referenceVersion: CONTRACT_REFERENCE_VERSION,
77
+ endpoints: Object.freeze({
78
+ list: "businessRuleListBySchemaCode",
79
+ runtimeList: "runtimeBusinessRuleListBySchemaCode",
80
+ get: "businessRuleGet",
81
+ create: "businessRuleCreate",
82
+ update: "businessRuleUpdate",
83
+ }),
84
+ wire: Object.freeze({
85
+ inputParam: RULE_NODE_GRAPH_WIRE.inputParam,
86
+ valueTypes: RULE_NODE_GRAPH_WIRE.valueTypes,
87
+ actionTypes: RULE_NODE_GRAPH_WIRE.actionTypes,
88
+ conditionTypes: RULE_NODE_GRAPH_WIRE.conditionTypes,
89
+ nodeTypes: RULE_NODE_GRAPH_WIRE.nodeTypes,
90
+ nodeTemplates: Object.fromEntries(Object.entries(RULE_NODE_TEMPLATES).map(([type, entry]) => [type, entry.template])),
91
+ schedulerSettingTemplate: RULE_SCHEDULER_SETTING_TEMPLATE,
92
+ dryLint: "rule.save 出门前跑图校验(DYNAMIC 键位/路由连通/inputParam 链/nodeType↔dataTriggerType/GET_LIST dataSourceType/EXCEPTION errMsg/HINT content/SCHEDULER schedulerSetting/methodMapping 逐键),失败返回结构化 blocker",
93
+ bizMethodMapping: RULE_NODE_GRAPH_WIRE.bizMethodMapping,
94
+ bizMethodValidation: RULE_NODE_GRAPH_WIRE.bizMethodValidation,
95
+ bizActionPersistence: RULE_NODE_GRAPH_WIRE.bizActionPersistence,
96
+ triggerConditions: RULE_NODE_GRAPH_WIRE.triggerConditions,
97
+ compatibilityMatrix: "CREATE -> dictionary-backed selection is blocked by evidence; use UPDATE after CREATE. Rule EXPRESSION requires non-empty value.formula and never reuses FORMULA(100) parsing.",
98
+ dynamicConditionSource: RULE_NODE_GRAPH_WIRE.dynamicConditionSource,
99
+ dynamicActionSource: RULE_NODE_GRAPH_WIRE.dynamicActionSource,
100
+ dynamicConditionValue: RULE_NODE_GRAPH_WIRE.dynamicConditionValue,
101
+ dynamicActionValue: RULE_NODE_GRAPH_WIRE.dynamicActionValue,
102
+ dynamicNameKeys: RULE_NODE_GRAPH_WIRE.dynamicNameKeys,
103
+ fixedValueTypes: RULE_NODE_GRAPH_WIRE.fixedValueTypes,
104
+ triggerDispatch: RULE_NODE_GRAPH_WIRE.triggerDispatch,
105
+ upsertPreserves: "update keeps the existing enabled/status; create writes the DISABLED dead state",
106
+ lifecycle: "update writes the DRAFT working row; rule.enable publishes the new version and flips the DRAFT switch",
107
+ platformQuirk: RULE_PLATFORM_QUIRK,
108
+ }),
109
+ wireBindings: Object.freeze({
110
+ inputParam: Object.freeze([emitterBinding("lanes/rule-graph.RULE_TRIGGER_INPUT_PARAM", assertBusinessRuleContract)]),
111
+ valueTypes: Object.freeze([emitterBinding("lanes/rule-graph.RULE_VALUE_TYPES", assertBusinessRuleContract)]),
112
+ actionTypes: Object.freeze([emitterBinding("lanes/rule-graph.RULE_ACTION_TYPES", assertBusinessRuleContract)]),
113
+ conditionTypes: Object.freeze([emitterBinding("lanes/rule-graph.RULE_CONDITION_TYPES", assertBusinessRuleContract)]),
114
+ nodeTypes: Object.freeze([emitterBinding("lanes/rule-graph.RULE_NODE_TYPES", assertBusinessRuleContract)]),
115
+ nodeTemplates: Object.freeze([emitterBinding("lanes/rule-graph.RULE_NODE_TEMPLATES", assertBusinessRuleContract)]),
116
+ schedulerSettingTemplate: Object.freeze([emitterBinding("lanes/rule-graph.RULE_SCHEDULER_SETTING_TEMPLATE", assertBusinessRuleContract)]),
117
+ dryLint: Object.freeze([behaviorBinding("test/rule-graph-validation.test.ts#rule.save refuses a mis-shaped graph BEFORE any wire call", assertBusinessRuleContract)]),
118
+ bizMethodMapping: Object.freeze([behaviorBinding("test/b2-bizmethod-binding.test.ts#ticket 35 dry lint: methodMapping element shape is located per key", assertBusinessRuleContract)]),
119
+ bizMethodValidation: Object.freeze([behaviorBinding("test/b2-bizmethod-binding.test.ts#ticket 35 wire gate: dangling serviceMethodCode is refused before any rule write", assertBusinessRuleContract)]),
120
+ bizActionPersistence: Object.freeze([behaviorBinding("test/rule-list.test.ts#DYNAMIC UPDATE uses the production source-field wire and backfills the related target row", assertBusinessRuleContract)]),
121
+ triggerConditions: Object.freeze([behaviorBinding("test/rule-graph-validation.test.ts#C.2: UPDATE with an empty filterCondition is flagged (warning) as a whole-table operation", assertBusinessRuleContract)]),
122
+ compatibilityMatrix: Object.freeze([behaviorBinding("test/ticket-05-rule-compatibility-preflight.test.ts#known-invalid CREATE plus dictionary selection is refused before rule mutation", assertBusinessRuleContract)]),
123
+ dynamicConditionSource: Object.freeze([emitterBinding("lanes/rule-graph.DYNAMIC_CONDITION_SOURCE_KEY", assertBusinessRuleContract)]),
124
+ dynamicActionSource: Object.freeze([emitterBinding("lanes/rule-graph.DYNAMIC_ACTION_SOURCE_KEY", assertBusinessRuleContract)]),
125
+ dynamicConditionValue: Object.freeze([behaviorBinding("test/rule-graph-validation.test.ts#negative: ticket 09 lesion", assertBusinessRuleContract)]),
126
+ dynamicActionValue: Object.freeze([behaviorBinding("test/rule-graph-validation.test.ts#negative: ticket 09 lesion", assertBusinessRuleContract)]),
127
+ dynamicNameKeys: Object.freeze([behaviorBinding("test/rule-list.test.ts#DYNAMIC UPDATE uses the production source-field wire", assertBusinessRuleContract)]),
128
+ fixedValueTypes: Object.freeze([behaviorBinding("test/rule-list.test.ts#FIXED value typing remains explicit in the businessrule contract and fake", assertBusinessRuleContract)]),
129
+ triggerDispatch: Object.freeze([behaviorBinding("test/ticket-111-contract-fixes.test.ts#contract: trigger-dispatch and FIXED-typing facts are pinned", assertBusinessRuleContract)]),
130
+ upsertPreserves: Object.freeze([emitterBinding("lanes/businessrule-contract.RULE_CREATE_BODY", assertBusinessRuleContract)]),
131
+ lifecycle: Object.freeze([behaviorBinding("test/rule-save-publish.test.ts#rule.save without publish reports the draft-only state", assertBusinessRuleContract)]),
132
+ platformQuirk: Object.freeze([emitterBinding("lanes/businessrule-contract.RULE_PLATFORM_QUIRK", assertBusinessRuleContract)]),
133
+ }),
134
+ evidence: Object.freeze([
135
+ "ticket 02 (2026-09-11): cross-model UPDATE with inputParam:\"schemaCode\" dry-gates clean but silently no-ops at runtime; inputParam:\"_input\" is the live-proven binding",
136
+ "ticket 09 production capture (2026-09-11): DYNAMIC filter source is currentSchemaDataItem and DYNAMIC action source is currentDataItemValue; DYNAMIC value is empty; *Name keys are display-string metadata",
137
+ "ticket 09 production capture (2026-09-11): FIXED select values are JSON array strings, numeric/boolean values are native, and text values are bare strings",
138
+ "ticket 111 evidence correction (live 2026-09-15 + RadioConverter.java:41-88): FIXED select values are BARE option strings — the JSON array string form ClassCastExceptions inside parseKeyValueMap and passes through unresolved (matched *_key null); the ticket-09 capture recorded a legacy stored shape, not a working write",
139
+ "RuleExecutorImpl.saveRuleInput registers the trigger input under the name \"_input\"",
140
+ "ticket 02 (2026-09-11): update writes DRAFT; get/list read PUBLISH; rule.enable is required for the rule to take effect",
141
+ "ticket 27 (8.6.26 source): RuleNodeType enum has GET_LIST/LOAD/CANCEL/AVAILABLE/SCHEDULER/EXCEPTION/HINT (RuleNodeType.java:26-42); GetListNode.dataSourceType, ExceptionNode.errMsg, HintNode.content are the only semantic node fields; SchedulerSetting is a rule-row column (BusinessRuleModel.java:52-53)",
142
+ "ticket 35 + 96 (8.6.26 source): BizActionNode.methodMapping is BizMethodMappingModel{schemaCode,serviceCode,serviceName,serviceMethodCode,serviceMethodName,methodCode,businessRuleId,nodeCode,inputMappings,outputMappings}; MappingModel has NO index/required/displayName in 8.6.26 (fwb v2 keys ride along as NON-semantic passthrough — the tool neither adds nor drops them by version); CodeType serializes as int index (0=CONST,1=PROPERTY); BusinessRuleFacadeImpl.addBizMethodMapping gates SERVICE existence only (BUSINESS_SERVICE_NOT_EXIST) — a dangling serviceMethodCode is silently disabled at app import (BizModelExportImportService.parseBusinessRule:2063-2066); ticket 96: mapping `required` mirrors the method parameter face (absent face key keeps the mapping absent), and a RESTFUL/DATA_SQL binding whose method lacks adapter-required configJson keys is refused as a registration shell",
143
+ ]),
144
+ tools: Object.freeze({
145
+ "rule.list": Object.freeze({
146
+ scope: "latest rule business-field listing",
147
+ description: "列出某模型的最新业务规则版本,回显 enabled/status/developmentMode/ruleVersion 等业务字段;服务端不过滤,客户端自行筛选。",
148
+ }),
149
+ "rule.get": Object.freeze({
150
+ scope: "rule node route readback",
151
+ description: "读回一条业务规则的完整定义(businessrule/get?id=),解析并回显 node/route 图 JSON(触发节点、UPDATE/CREATE 动作节点的 targetObjectCode/inputParam/dataActions/filterCondition 等)——规则族读写链的权威读回面,也是对照设计器生成图形的取证入口。",
152
+ }),
153
+ "rule.save": Object.freeze({
154
+ scope: "dead-state CRUD conditional node graphs",
155
+ description: RULE_SAVE_DESCRIPTION,
156
+ }),
157
+ "rule.node.update": Object.freeze({
158
+ scope: "dead-state CRUD conditional node graphs",
159
+ description: "精确改一个节点:按 schemaCode+ruleCode 读当前图,把 patch 浅合并到 nodeCode 对应的节点,再走与 rule.save 相同的保存 seam(所有图门禁与读回一致)。用来改一个分支阈值/条件/动作而不必重发整图。nodeCode/nodeType 不可改(平台按 nodeCode 连路由、节点类型不可变);改完默认只存草稿,生效仍需 rule.enable(publish:true 可顺带发布)。",
160
+ }),
161
+ }),
162
+ });
163
+ export function assertBusinessRuleContract() {
164
+ assertLaneContract(BUSINESSRULE_LANE_CONTRACT);
165
+ if (RULE_NODE_GRAPH_WIRE.inputParam !== "_input")
166
+ throw new Error("businessrule contract pins inputParam to the literal _input");
167
+ assertDescriptionCarries(BUSINESSRULE_LANE_CONTRACT, "rule.save", ["nodeGraph", "rule.enable", "DRAFT", "PUBLISH", "businessrule/update", "schedulerSetting", "methodMapping", "rule-graph-bizmethod-unresolved", "rule-graph-bizmethod-registration-shell", "rule-schema-unresolved", "required", "参数面", "outputMappings", "SYSTEM_CREATE", "publish:true", "published:false", "条件执行", "filterCondition"]);
168
+ if (!String(BUSINESSRULE_LANE_CONTRACT.wire.compatibilityMatrix).includes("CREATE -> dictionary-backed selection"))
169
+ throw new Error("businessrule contract must expose the evidence-backed compatibility matrix");
170
+ assertDescriptionCarries(BUSINESSRULE_LANE_CONTRACT, "rule.get", ["inputParam", "node/route"]);
171
+ assertDescriptionCarries(BUSINESSRULE_LANE_CONTRACT, "rule.node.update", ["schemaCode", "ruleCode", "nodeCode", "patch", "rule.save", "rule.enable"]);
172
+ assertDescriptionCarries({ tools: { "rule.nodeGraph": { scope: "node graph", description: ruleNodeGraphDescription() } } }, "rule.nodeGraph", [
173
+ "inputParam",
174
+ "_input",
175
+ RULE_NODE_GRAPH_WIRE.valueTypes,
176
+ RULE_NODE_GRAPH_WIRE.actionTypes,
177
+ RULE_NODE_GRAPH_WIRE.conditionTypes,
178
+ RULE_NODE_GRAPH_WIRE.dynamicConditionSource,
179
+ RULE_NODE_GRAPH_WIRE.dynamicActionSource,
180
+ RULE_NODE_GRAPH_WIRE.dynamicConditionValue,
181
+ RULE_NODE_GRAPH_WIRE.dynamicActionValue,
182
+ RULE_NODE_GRAPH_WIRE.fixedValueTypes,
183
+ RULE_NODE_GRAPH_WIRE.triggerDispatch,
184
+ ]);
185
+ if (!RULE_NODE_GRAPH_WIRE.triggerDispatch.includes("DomainServiceImpl.java:226-314"))
186
+ throw new Error("rule contract must pin the ticket-111 trigger-dispatch evidence");
187
+ if (!RULE_NODE_GRAPH_WIRE.fixedValueTypes.includes("RadioConverter.java:41-88"))
188
+ throw new Error("rule contract must pin the ticket-111 FIXED typing evidence");
189
+ if (!RULE_NODE_GRAPH_WIRE.valueTypes.includes("DYNAMIC") || !RULE_NODE_GRAPH_WIRE.valueTypes.includes("FIXED"))
190
+ throw new Error("businessrule contract must pin the rule value types");
191
+ if (!RULE_NODE_GRAPH_WIRE.actionTypes.includes("EQUALS"))
192
+ throw new Error("businessrule contract must pin the rule action types");
193
+ if (!RULE_NODE_GRAPH_WIRE.conditionTypes.includes("EQ"))
194
+ throw new Error("businessrule contract must pin the rule condition types");
195
+ if (!RULE_NODE_GRAPH_WIRE.nodeTypes.includes("START") || !RULE_NODE_GRAPH_WIRE.nodeTypes.includes("SCHEDULER"))
196
+ throw new Error("businessrule contract must pin the rule node types");
197
+ if (!RULE_NODE_TEMPLATES.SCHEDULER)
198
+ throw new Error("businessrule contract must pin the SCHEDULER node template");
199
+ if (!("triggerTimeType" in RULE_SCHEDULER_SETTING_TEMPLATE))
200
+ throw new Error("businessrule contract must pin the scheduler setting template");
201
+ if (!String(BUSINESSRULE_LANE_CONTRACT.wire.dryLint).includes("inputParam"))
202
+ throw new Error("businessrule contract must describe the dry-lint coverage");
203
+ if (!RULE_NODE_GRAPH_WIRE.bizMethodMapping.includes("serviceMethodCode"))
204
+ throw new Error("businessrule contract must pin the biz-method mapping shape");
205
+ if (!RULE_NODE_GRAPH_WIRE.bizMethodValidation.includes("serviceMethodParameterCode"))
206
+ throw new Error("businessrule contract must pin the biz-method validation refusal");
207
+ if (!RULE_NODE_GRAPH_WIRE.bizActionPersistence.includes("SYSTEM_CREATE"))
208
+ throw new Error("businessrule contract must pin the biz-action persistence ordering");
209
+ if (!RULE_NODE_GRAPH_WIRE.triggerConditions.includes("filterCondition") || !RULE_NODE_GRAPH_WIRE.triggerConditions.includes("findMainData") || !RULE_NODE_GRAPH_WIRE.triggerConditions.includes("execConditionGroups"))
210
+ throw new Error("businessrule contract must pin the node trigger-condition semantics (filterCondition/findMainData/execConditionGroups)");
211
+ if (RULE_NODE_GRAPH_WIRE.dynamicConditionSource !== "currentSchemaDataItem")
212
+ throw new Error("businessrule contract pins the DYNAMIC condition source key");
213
+ if (RULE_NODE_GRAPH_WIRE.dynamicActionSource !== "currentDataItemValue")
214
+ throw new Error("businessrule contract pins the DYNAMIC action source key");
215
+ if (!RULE_NODE_GRAPH_WIRE.dynamicConditionValue.includes("DYNAMIC"))
216
+ throw new Error("businessrule contract must pin the DYNAMIC condition value rule");
217
+ if (!RULE_NODE_GRAPH_WIRE.dynamicActionValue.includes("DYNAMIC"))
218
+ throw new Error("businessrule contract must pin the DYNAMIC action value rule");
219
+ if (!RULE_NODE_GRAPH_WIRE.dynamicNameKeys.includes("currentDataItemValueName"))
220
+ throw new Error("businessrule contract must pin the DYNAMIC display-name keys");
221
+ if (!("disabled" in RULE_STATUS) || RULE_STATUS.disabled !== "DISABLED")
222
+ throw new Error("businessrule contract pins the DISABLED dead state");
223
+ if (!RULE_CREATE_BODY.status || RULE_CREATE_BODY.enabled !== false)
224
+ throw new Error("businessrule contract pins the create body dead state");
225
+ if (!RULE_PLATFORM_QUIRK.includes("status"))
226
+ throw new Error("businessrule contract must pin the enabled/status platform quirk");
227
+ if (!String(BUSINESSRULE_LANE_CONTRACT.wire.upsertPreserves).includes("DISABLED"))
228
+ throw new Error("businessrule contract must pin the upsert-preserves rule");
229
+ if (!String(BUSINESSRULE_LANE_CONTRACT.wire.lifecycle).includes("rule.enable"))
230
+ throw new Error("businessrule contract must pin the rule lifecycle");
231
+ }
232
+ //# sourceMappingURL=businessrule-contract.js.map
@@ -0,0 +1,16 @@
1
+ import { claim, declareLaneCoverage } from "./coverage-declaration.js";
2
+ import { BUSINESSRULE_LANE_CONTRACT } from "./businessrule-contract.js";
3
+ const T = BUSINESSRULE_LANE_CONTRACT.tools;
4
+ export const BUSINESSRULE_COVERAGE = declareLaneCoverage(BUSINESSRULE_LANE_CONTRACT.lane, [
5
+ {
6
+ capability: "B1 dead-state rules and B1.2 CRUD/conditional node graphs",
7
+ evidence: ["src/businessrule-contract.ts", ".scratch/omc-business-rules/acceptance/b1-dead-live-20260908044544040.md"],
8
+ tools: [claim("rule.save", T["rule.save"].scope), claim("rule.node.update", T["rule.node.update"].scope)],
9
+ },
10
+ {
11
+ capability: "rule.list latest business-rule listing and rule.get node route definition readback",
12
+ evidence: ["packages/omc-cli/src/tools.ts", "packages/omc-cli/test/rule-list.test.ts"],
13
+ tools: [claim("rule.list", T["rule.list"].scope), claim("rule.get", T["rule.get"].scope)],
14
+ },
15
+ ]);
16
+ //# sourceMappingURL=businessrule-coverage.js.map
@@ -0,0 +1,221 @@
1
+ import { z } from "zod";
2
+ import { ENDPOINTS } from "../endpoints.js";
3
+ import { capabilityArg, environmentArg } from "../args.js";
4
+ import { governedRead } from "../read.js";
5
+ import { writeTool } from "../write.js";
6
+ import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "../json-column.js";
7
+ import { executeRuleSaveWrite, ruleNodeGraphArg } from "../rule-support.js";
8
+ import { defineTool, ToolInputError, ToolRefusalError } from "../tool-types.js";
9
+ import { resolveRuleRowId } from "../identity.js";
10
+ import { wireCall, wireRows } from "../wire.js";
11
+ import { BUSINESSRULE_LANE_CONTRACT } from "./businessrule-contract.js";
12
+ const RULE_LANE = BUSINESSRULE_LANE_CONTRACT.lane;
13
+ const RULE_TOOLS = BUSINESSRULE_LANE_CONTRACT.tools;
14
+ /**
15
+ * Deterministic layout keys the designer/canvas derives and `prepareRuleGraph`
16
+ * re-injects on save: a node's `x/y/width/height` and a route's `points`. They
17
+ * carry no rule semantics, so the default `rule.get` projection drops them and
18
+ * `full:true` restores the raw stored graph (2026-09-25 cost pass; same pattern
19
+ * as the workflow route view). Every other key — nodeCode/nodeType/nodeName,
20
+ * sourceInputMethod/inputParam/judgeMethod, exitConditionGroups, filterCondition,
21
+ * dataActions, methodMapping, … — is preserved verbatim, so the projection still
22
+ * round-trips back into `rule.save`.
23
+ */
24
+ const RULE_NODE_LAYOUT_KEYS = ["x", "y", "width", "height"];
25
+ const RULE_ROUTE_LAYOUT_KEYS = ["points"];
26
+ function projectRuleGraph(nodes, routes, full) {
27
+ if (full)
28
+ return { nodes, routes };
29
+ const strip = (value, keys) => {
30
+ if (!Array.isArray(value))
31
+ return value;
32
+ return value.map((entry) => {
33
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry))
34
+ return entry;
35
+ const copy = { ...entry };
36
+ for (const key of keys)
37
+ delete copy[key];
38
+ return copy;
39
+ });
40
+ };
41
+ return { nodes: strip(nodes, RULE_NODE_LAYOUT_KEYS), routes: strip(routes, RULE_ROUTE_LAYOUT_KEYS) };
42
+ }
43
+ export function registerBusinessRuleLane(tools) {
44
+ tools.push(defineTool({
45
+ name: "rule.list",
46
+ layer: "low",
47
+ kind: "read",
48
+ lane: RULE_LANE,
49
+ scope: RULE_TOOLS["rule.list"].scope,
50
+ description: RULE_TOOLS["rule.list"].description,
51
+ inputSchema: {
52
+ environment: environmentArg,
53
+ schemaCode: z.string().min(1),
54
+ full: z.boolean().optional().describe("内联完整规则列表;默认只返回摘要与 detailFile"),
55
+ capability: capabilityArg,
56
+ },
57
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
58
+ const rows = wireRows((await wireCall(session, { path: ENDPOINTS.runtimeBusinessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } })).data);
59
+ return {
60
+ schemaCode: args.schemaCode,
61
+ rules: rows.map((row) => ({
62
+ id: row.id,
63
+ code: row.code,
64
+ name: row.name,
65
+ schemaCode: row.schemaCode,
66
+ bizRuleType: row.bizRuleType,
67
+ enabled: row.enabled,
68
+ status: row.status,
69
+ developmentMode: row.developmentMode,
70
+ ruleVersion: row.ruleVersion,
71
+ latestPublished: row.latestPublished,
72
+ templateType: row.templateType,
73
+ defaultRule: row.defaultRule,
74
+ createdTime: row.createdTime,
75
+ creator: row.creater ?? row.creator,
76
+ })),
77
+ };
78
+ }, { full: args.full === true, target: `rule.list:${args.schemaCode}` }),
79
+ }));
80
+ tools.push(defineTool({
81
+ name: "rule.get",
82
+ layer: "low",
83
+ kind: "read",
84
+ lane: RULE_LANE,
85
+ scope: RULE_TOOLS["rule.get"].scope,
86
+ description: RULE_TOOLS["rule.get"].description,
87
+ inputSchema: {
88
+ environment: environmentArg,
89
+ id: z.string().min(1).optional().describe("规则记录 id(历史版本行可能指向旧图);也可改用 schemaCode + ruleCode 业务键定位"),
90
+ schemaCode: z.string().min(1).optional().describe("业务键定位:模型 code(与 ruleCode 同传,免去先 rule.list 取 id)"),
91
+ ruleCode: z.string().min(1).optional().describe("业务键定位:规则 code(如 Create/Update/自定义),与 schemaCode 同传"),
92
+ full: z.boolean().optional().describe("返回原始存储图(含确定性布局 x/y/width/height 与路由 points);默认返回语义图,确定性布局已投影去掉(可直接重存,保存时会重新生成布局)"),
93
+ capability: capabilityArg,
94
+ },
95
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
96
+ // Identity seam: prefer the business key (schemaCode + ruleCode) when
97
+ // given; fall back to the row id. Resolving the business key here removes
98
+ // the forced rule.list → rule.get dance and the "same code, new id"
99
+ // confusion (we always read the CURRENT effective row for that code).
100
+ let ruleId = args.id;
101
+ if (!ruleId) {
102
+ if (!args.schemaCode || !args.ruleCode)
103
+ throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool: "rule.get", message: "需要 id,或 schemaCode + ruleCode 两者一起" });
104
+ ruleId = (await resolveRuleRowId(session, args.schemaCode, args.ruleCode)) ?? undefined;
105
+ if (!ruleId)
106
+ return { schemaCode: args.schemaCode, ruleCode: args.ruleCode, absent: true };
107
+ }
108
+ const record = (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id: ruleId } })).data;
109
+ if (!record)
110
+ return { id: ruleId, absent: true };
111
+ // node/route/schedulerSetting are JSON-string columns; policy:
112
+ // non-strings null-coalesce, unparseable strings pass through.
113
+ const parse = (value) => decodeJsonColumn(value, JSON_COLUMN_NULL_COALESCE);
114
+ const routes = parse(record.route);
115
+ // Ticket 109 — render compatibility: the designer canvas resolves
116
+ // e.points[0] DIRECTLY per route (admin/js/12404.1675d362.js
117
+ // addAnnotationToLine), so a route without a non-empty string-array
118
+ // points breaks RENDERING while the data layer stays green. The
119
+ // readback reports it instead of letting "data ok" masquerade as
120
+ // "designer ok".
121
+ const routeList = Array.isArray(routes) ? routes : [];
122
+ // renderCompat validates the RAW routes (it needs `points`); the returned
123
+ // graph is projected separately at the return below.
124
+ const validPoints = (points) => Array.isArray(points) && points.length > 0 && points.every((point) => typeof point === "string" && point.trim().length > 0);
125
+ const violations = routeList
126
+ .map((route, index) => ({ index, route, ok: validPoints(route?.points) }))
127
+ .filter((entry) => !entry.ok)
128
+ .map((entry) => ({
129
+ index: entry.index,
130
+ preNode: String(entry.route?.preNode ?? ""),
131
+ postNode: String(entry.route?.postNode ?? ""),
132
+ problem: "points missing or not non-empty string array",
133
+ }));
134
+ const renderCompat = { ok: violations.length === 0, ...(violations.length > 0 ? { violations } : {}) };
135
+ // Ticket 109 — stale-row detection: same-code saves mint NEW record
136
+ // ids while old ids stay readable (historical graphs). Reading by a
137
+ // superseded id shows an OLD graph; flag it and pin the effective id.
138
+ let staleRow = false;
139
+ let effectiveId;
140
+ let staleRowMessage;
141
+ if (typeof record.schemaCode === "string" && record.schemaCode && typeof record.code === "string" && record.code) {
142
+ try {
143
+ const currentRows = wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: record.schemaCode } })).data);
144
+ const current = currentRows.find((row) => row.code === record.code);
145
+ if (current?.id && current.id !== ruleId) {
146
+ staleRow = true;
147
+ effectiveId = String(current.id);
148
+ staleRowMessage = "该 id 是历史版本行(同 code 后续保存产生了新记录);rule.get 用 effectiveId 读当前生效行,否则看到的是旧图";
149
+ }
150
+ }
151
+ catch {
152
+ // the list face being unreachable must not break the row readback
153
+ }
154
+ }
155
+ const projected = projectRuleGraph(parse(record.node), routes, args.full === true);
156
+ return {
157
+ id: ruleId,
158
+ absent: false,
159
+ code: record.code,
160
+ name: record.name,
161
+ schemaCode: record.schemaCode,
162
+ bizRuleType: record.bizRuleType,
163
+ enabled: record.enabled,
164
+ ruleStatus: record.status,
165
+ defaultRule: record.defaultRule,
166
+ developmentMode: record.developmentMode,
167
+ ruleVersion: record.ruleVersion,
168
+ templateType: record.templateType,
169
+ nodes: projected.nodes,
170
+ routes: projected.routes,
171
+ schedulerSetting: parse(record.schedulerSetting),
172
+ renderCompat,
173
+ ...(staleRow ? { staleRow, effectiveId, staleRowMessage } : {}),
174
+ };
175
+ }, { full: args.full === true, target: `rule.get:${args.id ?? `${args.schemaCode}/${args.ruleCode}`}` }),
176
+ }));
177
+ writeTool(tools, "rule.save", RULE_LANE, RULE_TOOLS["rule.save"].scope, RULE_TOOLS["rule.save"].description, {
178
+ schemaCode: z.string().min(1),
179
+ ruleCode: z.string().min(1),
180
+ name: z.string().min(1),
181
+ nodeGraph: ruleNodeGraphArg,
182
+ publish: z.boolean().optional().describe("also publish the new version in this call (businessrule/publish) so the designer sees it; enabling stays with rule.enable — default false keeps the legacy draft-only save"),
183
+ customTrigger: z.boolean().optional().describe("默认开:自定义 ruleCode + bizRuleType 0(非平台触发码 Create/Update/Delete/GetList/Load/Available/Cancel)即按钮/流程规则,工具在 START 后合成平台标准 CUSTOM_NODE「自定义按钮/流程触发」系统触发节点(nodeCode=ruleCode,live 布局 x558/y233);调用方已写 CUSTOM_NODE 则原样保留;显式 false 退回"),
184
+ }, async (session, args) => executeRuleSaveWrite(session, args));
185
+ // Round 10 (issue 04) — precise modify. `rule.save` replaces the WHOLE nodeGraph,
186
+ // so changing one branch threshold meant read the whole graph, edit, resend. This
187
+ // tool reads the current graph, patches ONE node, and reuses the same save seam
188
+ // (so every graph gate + readback is identical). Platform constraints honoured:
189
+ // `nodeCode`/`nodeType` are immutable (routes reference nodeCode; the platform
190
+ // cannot retype a node), so the patch may not change them.
191
+ writeTool(tools, "rule.node.update", RULE_LANE, RULE_TOOLS["rule.node.update"].scope, RULE_TOOLS["rule.node.update"].description, {
192
+ schemaCode: z.string().min(1),
193
+ ruleCode: z.string().min(1),
194
+ nodeCode: z.string().min(1).describe("要改的节点 nodeCode(在现有图里定位,不是新建)"),
195
+ patch: z.record(z.string(), z.unknown()).describe("浅合并到该节点的字段(如 exitConditionGroups/filterCondition/dataActions/nodeName);不允许改 nodeCode/nodeType"),
196
+ publish: z.boolean().optional().describe("改完同样顺带 publish(默认 false,仅存草稿;生效仍需 rule.enable)"),
197
+ customTrigger: z.boolean().optional(),
198
+ }, async (session, args) => {
199
+ const id = await resolveRuleRowId(session, args.schemaCode, args.ruleCode);
200
+ if (!id)
201
+ throw new ToolRefusalError({ status: "refused", blocker: "rule-absent", message: `规则 ${args.schemaCode}.${args.ruleCode} 不存在;拒绝 node patch` });
202
+ const record = (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id } })).data;
203
+ if (!record)
204
+ throw new ToolRefusalError({ status: "refused", blocker: "rule-absent", message: `规则 ${args.schemaCode}.${args.ruleCode}(id ${id})读回为空` });
205
+ for (const key of ["nodeCode", "nodeType"])
206
+ if (key in args.patch)
207
+ throw new ToolRefusalError({ status: "refused", blocker: "rule-node-immutable-key", message: `node patch 不允许改 ${key}(平台按 nodeCode 连接路由、节点类型不可变);要改名/改型请整图 rule.save` });
208
+ // node/route are SEPARATE JSON-string columns (see rule.get): `node` is the
209
+ // nodes array, `route` the routes array — not one {nodes,routes} object.
210
+ const nodes = decodeJsonColumn(record.node, JSON_COLUMN_NULL_COALESCE);
211
+ const routes = decodeJsonColumn(record.route, JSON_COLUMN_NULL_COALESCE);
212
+ const nodeList = Array.isArray(nodes) ? nodes : [];
213
+ const target = nodeList.find((node) => node.nodeCode === args.nodeCode);
214
+ if (!target)
215
+ throw new ToolRefusalError({ status: "refused", blocker: "rule-node-absent", message: `现有图中没有 nodeCode=${args.nodeCode};用 rule.get schemaCode+ruleCode 查看 nodeCode` });
216
+ const mergedNode = { ...target, ...args.patch };
217
+ const mergedGraph = { nodes: nodeList.map((node) => (node.nodeCode === args.nodeCode ? mergedNode : node)), ...(Array.isArray(routes) ? { routes } : {}) };
218
+ return executeRuleSaveWrite(session, { schemaCode: args.schemaCode, ruleCode: args.ruleCode, name: String(record.name ?? args.ruleCode), nodeGraph: mergedGraph, ...(args.publish === true ? { publish: true } : {}), ...(args.customTrigger === true ? { customTrigger: true } : {}) });
219
+ });
220
+ }
221
+ //# sourceMappingURL=businessrule.js.map
@@ -0,0 +1,65 @@
1
+ import { ENDPOINTS } from "../endpoints.js";
2
+ export const CONTRACT_FAMILY = "8.6";
3
+ export const CONTRACT_REFERENCE_VERSION = "8.6.26";
4
+ /**
5
+ * An emitter/fake/decoder actually imports and consumes the referenced value,
6
+ * and `verify` re-checks the platform truth against it.
7
+ */
8
+ export function emitterBinding(ref, verify) {
9
+ return Object.freeze({ kind: "emitter-constant", ref, verify });
10
+ }
11
+ /**
12
+ * The referenced behavior test proves the fact. `verify` runs the lane's
13
+ * shared contract assertion (the same seam the test calls) so a constant or
14
+ * emitter change fails both the test and the build gate.
15
+ */
16
+ export function behaviorBinding(ref, verify) {
17
+ return Object.freeze({ kind: "behavior-test", ref, verify });
18
+ }
19
+ /** A UI-only fact proven by a conformance/frontend-oracle assertion id. */
20
+ export function oracleBinding(ref, verify) {
21
+ return Object.freeze({ kind: "oracle", ref, verify });
22
+ }
23
+ /** Compose a tool description from one lead sentence plus ordered guide sentences. */
24
+ export function describeTool(lead, guides) {
25
+ return [lead, ...guides]
26
+ .map((part) => part.trim())
27
+ .filter((part) => part.length > 0)
28
+ .join(" ");
29
+ }
30
+ /** Shared per-lane invariants; every lane's assertXxxContract() starts here. */
31
+ export function assertLaneContract(contract) {
32
+ if (!contract.lane.trim())
33
+ throw new Error("lane contract requires a lane name");
34
+ if (contract.family !== CONTRACT_FAMILY)
35
+ throw new Error(`${contract.lane} contract must target family ${CONTRACT_FAMILY}`);
36
+ if (contract.referenceVersion !== CONTRACT_REFERENCE_VERSION)
37
+ throw new Error(`${contract.lane} contract must anchor on ${CONTRACT_REFERENCE_VERSION}`);
38
+ const toolNames = Object.keys(contract.tools);
39
+ if (toolNames.length === 0)
40
+ throw new Error(`${contract.lane} contract must declare at least one tool`);
41
+ for (const name of toolNames) {
42
+ const tool = contract.tools[name];
43
+ if (!tool.scope.trim())
44
+ throw new Error(`${name} requires a coverage scope`);
45
+ if (!tool.description.trim())
46
+ throw new Error(`${name} requires a description`);
47
+ }
48
+ for (const [label, key] of Object.entries(contract.endpoints)) {
49
+ if (!(key in ENDPOINTS))
50
+ throw new Error(`${contract.lane} endpoint ${label} -> ${key} is not a live-proven ENDPOINTS key`);
51
+ }
52
+ if (contract.evidence.length === 0)
53
+ throw new Error(`${contract.lane} contract requires at least one evidence anchor`);
54
+ if (Object.keys(contract.wire).length === 0)
55
+ throw new Error(`${contract.lane} contract requires wire facts`);
56
+ }
57
+ /** Assert every listed wire token appears verbatim in a tool's contract description. */
58
+ export function assertDescriptionCarries(contract, toolName, tokens) {
59
+ const description = contract.tools[toolName]?.description ?? "";
60
+ for (const token of tokens) {
61
+ if (!description.includes(token))
62
+ throw new Error(`${toolName} description must carry the contract token ${JSON.stringify(token)}`);
63
+ }
64
+ }
65
+ //# sourceMappingURL=contract-support.js.map
@@ -0,0 +1,9 @@
1
+ /** Declare one tool's scope claim inside a capability. */
2
+ export function claim(tool, scope) {
3
+ return Object.freeze({ tool, scope });
4
+ }
5
+ /** Group a lane's capability declarations, injecting the lane id once. */
6
+ export function declareLaneCoverage(lane, capabilities) {
7
+ return Object.freeze(capabilities.map((capability) => Object.freeze({ lane, ...capability })));
8
+ }
9
+ //# sourceMappingURL=coverage-declaration.js.map