@stalfh233/omc-cli 0.0.0-stage → 0.4.0
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.
- package/LICENSE +21 -0
- package/README.md +304 -3
- package/coverage/m1-coverage-manifest-v1.json +1278 -0
- package/dist/approval-token.js +102 -0
- package/dist/args.js +25 -0
- package/dist/artifacts.js +119 -0
- package/dist/bench/call-face-eval.js +256 -0
- package/dist/bench/context-attribution.js +151 -0
- package/dist/bench/discovery-cost-eval.js +230 -0
- package/dist/bench/driver.js +81 -0
- package/dist/bench/evals.js +236 -0
- package/dist/bench/fake-http-server.js +65 -0
- package/dist/bench/instrument.js +87 -0
- package/dist/bench/intent-face-eval.js +343 -0
- package/dist/bench/run.js +166 -0
- package/dist/bench/scenario.js +343 -0
- package/dist/bench/types.js +76 -0
- package/dist/bi-wire.js +41 -0
- package/dist/bizservice-config.js +581 -0
- package/dist/call.js +153 -0
- package/dist/capability-absences.js +23 -0
- package/dist/capability-overview.js +492 -0
- package/dist/capability-shape.js +154 -0
- package/dist/cli-contract.js +70 -0
- package/dist/cli-output.js +73 -0
- package/dist/cli.js +1418 -0
- package/dist/code-rules.js +69 -0
- package/dist/command-transport.js +155 -0
- package/dist/config-store.js +195 -0
- package/dist/context.js +21 -0
- package/dist/contract-consistency.js +66 -0
- package/dist/contract-resources.js +62 -0
- package/dist/coverage-consistency.js +62 -0
- package/dist/coverage-registry.js +76 -0
- package/dist/coverage.js +130 -0
- package/dist/data-list-filter.js +114 -0
- package/dist/discovery.js +390 -0
- package/dist/endpoints.js +154 -0
- package/dist/environment-policy.js +26 -0
- package/dist/execution-metadata.js +1092 -0
- package/dist/fake/app.js +45 -0
- package/dist/fake/b2-registration.js +594 -0
- package/dist/fake/businessrule.js +242 -0
- package/dist/fake/datarule.js +72 -0
- package/dist/fake/dictionary.js +108 -0
- package/dist/fake/environment.js +30 -0
- package/dist/fake/field.js +167 -0
- package/dist/fake/form.js +101 -0
- package/dist/fake/index.js +121 -0
- package/dist/fake/list-view.js +289 -0
- package/dist/fake/model.js +136 -0
- package/dist/fake/online-js.js +18 -0
- package/dist/fake/report.js +253 -0
- package/dist/fake/routes.js +47 -0
- package/dist/fake/rule-lifecycle.js +37 -0
- package/dist/fake/runtime-data.js +367 -0
- package/dist/fake/state.js +67 -0
- package/dist/fake/workflow.js +364 -0
- package/dist/field-change.js +200 -0
- package/dist/field-families.js +896 -0
- package/dist/form-layout.js +111 -0
- package/dist/form-support.js +821 -0
- package/dist/goal-routes.js +468 -0
- package/dist/governed-execution.js +87 -0
- package/dist/human-summary.js +212 -0
- package/dist/identity.js +62 -0
- package/dist/intent/baseline.js +57 -0
- package/dist/intent/capabilities/bizservice.js +274 -0
- package/dist/intent/capabilities/businessrule.js +493 -0
- package/dist/intent/capabilities/datarule.js +187 -0
- package/dist/intent/capabilities/field.js +545 -0
- package/dist/intent/capabilities/form.js +136 -0
- package/dist/intent/capabilities/index.js +64 -0
- package/dist/intent/capabilities/listview.js +157 -0
- package/dist/intent/capabilities/model.js +100 -0
- package/dist/intent/capabilities/onlinejs.js +108 -0
- package/dist/intent/capabilities/report.js +355 -0
- package/dist/intent/capabilities/workflow.js +458 -0
- package/dist/intent/capability.js +6 -0
- package/dist/intent/cli.js +91 -0
- package/dist/intent/compare.js +56 -0
- package/dist/intent/compiler.js +79 -0
- package/dist/intent/dsl.js +129 -0
- package/dist/intent/plan-file.js +63 -0
- package/dist/intent/readback.js +65 -0
- package/dist/intent/schema.js +158 -0
- package/dist/intent/validation.js +30 -0
- package/dist/intent/yaml.js +315 -0
- package/dist/json-column.js +68 -0
- package/dist/lanes/app-contract.js +95 -0
- package/dist/lanes/app-coverage.js +16 -0
- package/dist/lanes/app.js +174 -0
- package/dist/lanes/apply-changes.js +231 -0
- package/dist/lanes/b2-registration-contract.js +292 -0
- package/dist/lanes/b2-registration-coverage.js +48 -0
- package/dist/lanes/b2-registration.js +1187 -0
- package/dist/lanes/businessrule-contract.js +232 -0
- package/dist/lanes/businessrule-coverage.js +16 -0
- package/dist/lanes/businessrule.js +221 -0
- package/dist/lanes/contract-support.js +65 -0
- package/dist/lanes/coverage-declaration.js +9 -0
- package/dist/lanes/datarule-contract.js +155 -0
- package/dist/lanes/datarule-coverage.js +19 -0
- package/dist/lanes/datarule-protocol.js +308 -0
- package/dist/lanes/datarule.js +818 -0
- package/dist/lanes/dictionary-contract.js +78 -0
- package/dist/lanes/dictionary-coverage.js +24 -0
- package/dist/lanes/dictionary.js +235 -0
- package/dist/lanes/environment-contract.js +70 -0
- package/dist/lanes/environment-coverage.js +14 -0
- package/dist/lanes/environment.js +163 -0
- package/dist/lanes/field-contract.js +223 -0
- package/dist/lanes/field-coverage.js +27 -0
- package/dist/lanes/field.js +374 -0
- package/dist/lanes/form-contract.js +97 -0
- package/dist/lanes/form-coverage.js +16 -0
- package/dist/lanes/form.js +185 -0
- package/dist/lanes/lane-ids.js +34 -0
- package/dist/lanes/list-view-contract.js +175 -0
- package/dist/lanes/list-view-coverage.js +20 -0
- package/dist/lanes/list-view-shapes.js +1207 -0
- package/dist/lanes/list-view.js +578 -0
- package/dist/lanes/meta-contract.js +85 -0
- package/dist/lanes/meta.js +255 -0
- package/dist/lanes/model-contract.js +168 -0
- package/dist/lanes/model-coverage.js +20 -0
- package/dist/lanes/model.js +986 -0
- package/dist/lanes/online-js-contract.js +99 -0
- package/dist/lanes/online-js-coverage.js +28 -0
- package/dist/lanes/online-js.js +127 -0
- package/dist/lanes/report-contract.js +144 -0
- package/dist/lanes/report-coverage.js +21 -0
- package/dist/lanes/report.js +476 -0
- package/dist/lanes/rule-graph.js +1846 -0
- package/dist/lanes/rule-lifecycle-contract.js +92 -0
- package/dist/lanes/rule-lifecycle-coverage.js +20 -0
- package/dist/lanes/rule-lifecycle.js +176 -0
- package/dist/lanes/runtime-data-contract.js +264 -0
- package/dist/lanes/runtime-data-coverage.js +25 -0
- package/dist/lanes/runtime-data.js +1054 -0
- package/dist/lanes/workflow-contract.js +223 -0
- package/dist/lanes/workflow-coverage.js +40 -0
- package/dist/lanes/workflow.js +1813 -0
- package/dist/online-js-layout.js +58 -0
- package/dist/online-js-source.js +276 -0
- package/dist/package-tool.js +51 -0
- package/dist/package.js +73 -0
- package/dist/plan.js +73 -0
- package/dist/read.js +144 -0
- package/dist/redact.js +28 -0
- package/dist/rule-support.js +587 -0
- package/dist/runtime-support.js +134 -0
- package/dist/server.js +92 -0
- package/dist/session-manager.js +30 -0
- package/dist/session.js +149 -0
- package/dist/skills.js +112 -0
- package/dist/support.js +98 -0
- package/dist/tool-types.js +127 -0
- package/dist/tools.js +59 -0
- package/dist/usage-log.js +197 -0
- package/dist/wire.js +287 -0
- package/dist/workflow-support.js +99 -0
- package/dist/write-lease.js +26 -0
- package/dist/write-lock.js +109 -0
- package/dist/write.js +176 -0
- package/dist/zip.js +156 -0
- package/docs/tool-surface-map.md +42 -0
- package/package.json +71 -6
- package/skills/omc-acceptance-criteria.md +47 -0
- package/skills/omc-business-configuration.md +257 -0
- package/skills/omc-capabilities.md +197 -0
- package/skills/omc-capability-scouting.md +55 -0
- package/skills/omc-config-draft-review.md +171 -0
- package/skills/omc-five-piece-flow.md +35 -0
- package/skills/omc-glossary.md +86 -0
- package/skills/omc-refusals.md +110 -0
- package/skills/omc-requirement-analysis.md +161 -0
- package/skills/omc-requirement-vocabulary.md +51 -0
- package/skills/omc-start-here.md +82 -0
- package/skills/omc-tool-selection.md +119 -0
- package/skills/omc-write-hazards.md +87 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { assertDescriptionCarries, assertLaneContract, behaviorBinding, CONTRACT_FAMILY, CONTRACT_REFERENCE_VERSION, describeTool, emitterBinding } from "./contract-support.js";
|
|
2
|
+
import { LANES } from "./lane-ids.js";
|
|
3
|
+
/**
|
|
4
|
+
* G1 field-level data-rule wire facts (8.6.26 `BizDataRuleController` /
|
|
5
|
+
* `BizDataRuleModel` / `DataRuleType`, decompiled launcher source). These are
|
|
6
|
+
* the single source for the handler descriptions and the coverage declaration.
|
|
7
|
+
*
|
|
8
|
+
* This is a distinct engine from the form's per-control options: the
|
|
9
|
+
* `attributes[].historyOpt` snapshot and this `bizdatarule` table are the
|
|
10
|
+
* same field-level rule source, while the field-control "visible/readonly/
|
|
11
|
+
* required per form" flags are a separate engine (CONTEXT: 两套引擎).
|
|
12
|
+
*/
|
|
13
|
+
export const DATARULE_WIRE = Object.freeze({
|
|
14
|
+
list: "GET bizdatarule/list?schemaCode=<schemaCode>[&dataRuleType=<1..5>] -> BizDataRuleModel[]",
|
|
15
|
+
create: "POST bizdatarule/create body BizDataRuleModel {schemaCode,propertyCodes (array; real designer binding),propertyCode (legacy single),dataRuleType,options (JSON string),checkType?,childRule?,name?,sortKey?}; create requires schemaCode + dataRuleType + a target field; options.propertyCodes mirrors the binding (real 2026-09-23 rows bind via the ARRAY, not the singular column)",
|
|
16
|
+
update: "POST bizdatarule/update body BizDataRuleModel; requires id + schemaCode + dataRuleType; the full-row overwrite preserves the existing propertyCodes when the caller omits a target",
|
|
17
|
+
delete: "bizdatarule/delete?id=<ruleId> (@RequestParam String id) with list exact-absence readback",
|
|
18
|
+
dataRuleTypes: "DataRuleType: 1 CHECK_RULE (校验), 2 CALCULATE_RULE (计算/拼接), 3 DISPLAY_RULE (显示), 4 OTHER_RULE (必填), 5 READ_ONLY_RULE (只读); 8 EXTENSION_RULE is out of scope",
|
|
19
|
+
dataRuleCheckTypes: "dataRuleType 1 (CHECK_RULE 提交校验规则) 的 checkType 判别(cloudpivot-shared DataRuleCheckType.java)——**校验类需求走这里,不要写在线 JS**:1 REGEX_CHECK 正则校验 / 2 SUBMIT_CHECK 提交校验(含跨字段比较)/ 3 EMPTY_ROW_CHECK 空行校验 / 4 MAX_LENGTH_CHECK 最大长度校验 / 5 REPEAT_CHECK 重复校验;checkType 只配 dataRuleType 1,缺省或非法值由工具拒绝",
|
|
20
|
+
optionsByCheckType: "各 checkType 的 options(engine/bo/datarule/impl/checkType/*):1 REGEX_CHECK{regexp:'/…/',regexpText:{zh,en}}(regexp 必须斜杠包裹,RegexCheckValidator 会去掉首尾字符);2 SUBMIT_CHECK{conditions:'<字段code> 操作符 <值> [&&| || 连接]', verifyType:'submitDenied'?(满足条件即拒绝提交,缺省为满足才允许), prompt:'<提示,**纯文本**,勿 URL 编码——这种顶层 options.prompt 门户原样显示,编码过会显示成 %E5..乱码>' }(操作符 等于/不等于/大于/小于/大于等于/小于等于/为空/不为空…;**字段 vs 字面量**直接写值如 {qty} 大于 100;**跨字段**右值必须写实为值对象 {\"valueType\":\"currentModel\",\"value\":\"<字段code>\"},如「结束日期不早于开始日期」= {end_date} 小于 {\"valueType\":\"currentModel\",\"value\":\"start_date\"}——裸 {字段code} 右值会原样落库但前端/服务端都不解析成字段值,规则静默不生效,工具拒绝);3 EMPTY_ROW_CHECK{isEmptyRow,isEmptyRowTips};4 MAX_LENGTH_CHECK{maxLength};5 REPEAT_CHECK{properties:[<字段code>],isSubTableValidationAllData?,prompt:[{isDataItem,code}]}(重复提示逐项/数组解析,RepeatCheckValidator:258-272:**isDataItem=0 → code 是字面文案**,如 [{\"isDataItem\":0,\"code\":\"单号重复\"}];**isDataItem=1 → code 是字段 code**,提示取该字段(值/名)。写成 isDataItem:1 + 字段code 会把提示显示成字段值而不是你要的文案;`promptText` 等未识别键平台会忽略,勿用)",
|
|
21
|
+
optionsByType: "options is a JSON string read per type: 1 check keys (verifyFormula/regexp/maxLength/isEmptyRow/noRepeat/conditions), 2 FormulaConfig — `shortTextStitch` (falling back to `value`) MUST be a JSON OBJECT STRING whose `formula` key holds the expression, e.g. {\"shortTextStitch\":\"{\\\"formula\\\":\\\"{field}\\\",\\\"editorText\\\":\\\"{字段名}\\\"}\"}; a bare \"{field}\" string fails JSONUtil.isJson → getCalRuleExpression returns null → false 305003 dead-cycle; `formulaType:\"AGGREGATION\"` instead reads `aggregation`; a top-level `expression` is NEVER read (it is only a key INSIDE that nested JSON), 3 displayFormula, 4 requiredFormula, 5 readonlyCondition",
|
|
22
|
+
formulaReference: "类型2 公式引用按位置解析:裸 {字段}=**主记录**字段;带点 {子表code.字段}=**子表**字段(FormulaProcessor.constructSubExprParamsValueMap 要求点前等于子表 schema)。所以**主表公式**用裸 {字段}(带 {主表code.字段} 会按子表解析 → 值空/静默 null),**子表公式**(childRule:true)引用同级子字段必须带点 {子表code.字段}(裸引用被当主记录字段 → 值缺失 → 提交 50000)。工具在写前按子表已发布字段判定并拒绝裸引用(blocker datarule-formula-child-bare-reference),读回对存量子表公式加 formulaHint 提示复核。",
|
|
23
|
+
optionsEncoding: "the engine JSON-parses the `options` column and reads requiredFormula / readonlyCondition / displayFormula / verifyFormula / shortTextStitch (whose VALUE is itself a JSON string carrying `formula`); send options as a stringified object",
|
|
24
|
+
expressionSyntax: "field references are `{fieldCode}` (e.g. {amount}>5000). This CLI does NOT publish or validate an expression grammar: operators, functions, escaping, and coercion are unspecified here, and rule-action EXPRESSION must NOT be assumed to share FORMULA(100)'s parser without live evidence. A constant `\\\"true\\\"` is the known always-on form. Treat a saved expression as unverified until the portal form proves it.",
|
|
25
|
+
acceptanceBoundary: "types 3/4/5 are form-interaction rules; `data.*` does NOT run the form validation layer, so the CLI cannot prove a required/readonly/display rule true or false. Acceptance is a portal-form manual check — never report a saved rule as verified from `data.*` readback alone.",
|
|
26
|
+
formHistoryOpt: "same field-level rule engine as form attributes[].historyOpt; distinct from the per-form field-control rule engine (dataRuleType 3/4/5 must not be confused with form-control visible/readonly/required)",
|
|
27
|
+
});
|
|
28
|
+
const DATARULE_SCOPE = "create update delete readback";
|
|
29
|
+
export const DATARULE_LANE_CONTRACT = Object.freeze({
|
|
30
|
+
lane: LANES.dataRule,
|
|
31
|
+
family: CONTRACT_FAMILY,
|
|
32
|
+
referenceVersion: CONTRACT_REFERENCE_VERSION,
|
|
33
|
+
endpoints: Object.freeze({
|
|
34
|
+
list: "dataRuleList",
|
|
35
|
+
create: "dataRuleCreate",
|
|
36
|
+
update: "dataRuleUpdate",
|
|
37
|
+
delete: "dataRuleDelete",
|
|
38
|
+
}),
|
|
39
|
+
wire: Object.freeze({
|
|
40
|
+
list: DATARULE_WIRE.list,
|
|
41
|
+
create: DATARULE_WIRE.create,
|
|
42
|
+
update: DATARULE_WIRE.update,
|
|
43
|
+
delete: DATARULE_WIRE.delete,
|
|
44
|
+
dataRuleTypes: DATARULE_WIRE.dataRuleTypes,
|
|
45
|
+
dataRuleCheckTypes: DATARULE_WIRE.dataRuleCheckTypes,
|
|
46
|
+
optionsByType: DATARULE_WIRE.optionsByType,
|
|
47
|
+
formulaReference: DATARULE_WIRE.formulaReference,
|
|
48
|
+
optionsByCheckType: DATARULE_WIRE.optionsByCheckType,
|
|
49
|
+
optionsEncoding: DATARULE_WIRE.optionsEncoding,
|
|
50
|
+
formHistoryOpt: DATARULE_WIRE.formHistoryOpt,
|
|
51
|
+
}),
|
|
52
|
+
wireBindings: Object.freeze({
|
|
53
|
+
list: Object.freeze([behaviorBinding("test/datarule.test.ts#datarule type4", assertDataRuleContract)]),
|
|
54
|
+
create: Object.freeze([behaviorBinding("test/datarule.test.ts#datarule.create refuses before the wire", assertDataRuleContract)]),
|
|
55
|
+
update: Object.freeze([behaviorBinding("test/datarule.test.ts#datarule.update preserves full current row", assertDataRuleContract)]),
|
|
56
|
+
delete: Object.freeze([behaviorBinding("test/datarule.test.ts#datarule type5", assertDataRuleContract)]),
|
|
57
|
+
dataRuleTypes: Object.freeze([emitterBinding("lanes/datarule-contract.DATARULE_TYPE_OPTION_KEYS", assertDataRuleContract)]),
|
|
58
|
+
dataRuleCheckTypes: Object.freeze([emitterBinding("lanes/datarule.DATARULE_CHECK_TYPES", assertDataRuleContract)]),
|
|
59
|
+
optionsByType: Object.freeze([emitterBinding("lanes/datarule-contract.DATARULE_TYPE_OPTION_KEYS", assertDataRuleContract)]),
|
|
60
|
+
formulaReference: Object.freeze([behaviorBinding("test/datarule.test.ts#datarule refuses a child-table formula that bare-references a sibling child field", assertDataRuleContract)]),
|
|
61
|
+
optionsByCheckType: Object.freeze([emitterBinding("lanes/datarule.DATARULE_CHECK_TYPES", assertDataRuleContract)]),
|
|
62
|
+
optionsEncoding: Object.freeze([emitterBinding("lanes/datarule-contract.DATARULE_TYPE_OPTION_KEYS", assertDataRuleContract)]),
|
|
63
|
+
formHistoryOpt: Object.freeze([behaviorBinding("test/datarule.test.ts#datarule type5", assertDataRuleContract)]),
|
|
64
|
+
}),
|
|
65
|
+
evidence: Object.freeze([
|
|
66
|
+
"cp-sources/backend/decompiled-full/launcher-8.6.26/cloudpivot-engine-8.6.26/.../controller/app/BizDataRuleController.java:156-364",
|
|
67
|
+
"cp-sources/backend/decompiled-models/com/authine/cloudpivot/metadata/bizmodel/BizDataRuleModel.java",
|
|
68
|
+
"cp-sources/backend/decompiled-models/com/authine/cloudpivot/metadata/bizmodel/DataRuleType.java (CHECK=1,CALCULATE=2,DISPLAY=3,OTHER=4,READ_ONLY=5,EXTENSION=8)",
|
|
69
|
+
"cp-sources/backend/.../engine/bo/datarule/impl/ruleType/RequiredDataRuleValidator.java reads options.requiredFormula",
|
|
70
|
+
"cp-sources/backend/.../engine/bo/datarule/impl/ruleType/ReadOnlyValidator.java reads options.readonlyCondition",
|
|
71
|
+
"cp-sources/backend/.../engine/bo/calculate/impl/CalculateRuleProcessor.java reads options.shortTextStitch",
|
|
72
|
+
".scratch/omc-app-management/research/desensitized-config-shapes.md §4",
|
|
73
|
+
".scratch/omc-app-management/research/configuration-gap-report.md §3 P0/G1, §5.1 Spec A",
|
|
74
|
+
]),
|
|
75
|
+
tools: Object.freeze({
|
|
76
|
+
"datarule.list": Object.freeze({
|
|
77
|
+
scope: DATARULE_SCOPE,
|
|
78
|
+
description: describeTool("List field-level data rules of a model (bizdatarule/list), optionally filtered by `dataRuleType` 1–5.", [
|
|
79
|
+
"`schemaCode` is required; `dataRuleType` omits every type when absent.",
|
|
80
|
+
"Each row decodes to {id, schemaCode, propertyCode, dataRuleType, checkType, childRule, name, sortKey, options}.",
|
|
81
|
+
"**子表规则用主表 schemaCode 查**:平台对 `schemaCode=<子表>` 只返回部分行——公式字段(FORMULA t100)的 type2 镜像行不返回(真机 8.6.26:bench40_order_item 按子表查 2 行、缺 subtotal;按主表 bench40_order 查 16 行、含该 type2 行)。公式字段的公式读 `field.list` 的 `options.formulaConfig`(字段属性,不是独立规则)。普通数值字段的子表 type2 规则按子表查是有的。",
|
|
82
|
+
]),
|
|
83
|
+
}),
|
|
84
|
+
"datarule.create": Object.freeze({
|
|
85
|
+
scope: DATARULE_SCOPE,
|
|
86
|
+
description: describeTool("Create a field-level data rule (bizdatarule/create) and read it back from the authoritative list.", [
|
|
87
|
+
"`dataRuleType`: 1 校验 / 2 计算拼接 / 3 显示 / 4 必填 / 5 只读 (8 EXTENSION is out of scope).",
|
|
88
|
+
"**⚠️ type3/4/5 行的两个读面**:门户**运行时**读**字段 options**(`requiredFormula`/`readonlyCondition`/`readonlyFormula`/`displayFormula`,`field.create/update` 写);表单设计器**设计态** Set/Unset 读 **bizdatarule/list**(本行,network capture 2026-09-23)。**实测:type3/4/5 规则单独不会让门户表单控件变必填/只读**(中文或符号操作符都一样),符号式经 `historyOpt` 镜像还会让表单渲染崩溃——运行时条件请用中文操作符。表单条件优先走 `field.update`:它会**自动配对**本行(readback.formConditionSync)。本工具用于字段级引擎规则或独立增删改。",
|
|
89
|
+
"**类型3/4/5 条件串协议(平台 create 不校验,坏形状落库即雷,工具按此协议拒绝)**:`{字段code} 操作符 [值]`。操作符取 **bizdatarule 词表**(与字段 options 的表单交互引擎不同——那是符号式):**中文** 等于/不等于/大于/小于/大于等于/小于等于/包含/不包含/包含全部/不包含全部/包含任意一个/不包含任意一个/等于任意一个/属于/不属于/介于/为空/不为空(实测 146 条真实规则以此为主),也接受符号 `==` `!=` `>` `<` `>=` `<=` `Contains` `IsEmpty` 等。为空/不为空**不带值**(`{field} 不为空`);其余带值(JSON 对象或标量)。多条件 ` && `、多组 ` || `。示例 type4:`options:{requiredFormula:\"{amount} 等于 {\\\"valueType\\\":\\\"specifyOption\\\",\\\"value\\\":\\\"A\\\"}\"}`;无条件必填用字面量 \"true\"。**字段绑定用 `propertyCodes`(数组)**,工具自动把 `options.propertyCodes` 也补上;存量的坏规则由 datarule.list 以 shapeWarning 标出。",
|
|
90
|
+
"`options` is a JSON object sent to the wire as a JSON string; the engine reads `requiredFormula` (4), `readonlyCondition` (5), `displayFormula` (3), `verifyFormula` (1) and `shortTextStitch` (2).",
|
|
91
|
+
"类型2(计算/拼接):`options.shortTextStitch`(或 `value`)的值**必须是含 `formula` 的 JSON 对象字符串**(如 `{\"formula\":\"{字段}\"}`);裸串会被工具拒绝(否则引擎返回 null → 误报 305003 死循环)。顶层没有 `expression` 键——它只是该嵌套 JSON 内部的字段名。",
|
|
92
|
+
"Missing type or schemaCode is refused before any write.",
|
|
93
|
+
"根因提示(ticket 78):类型2 CALCULATE 在未发布应用上会报假死循环 305003(checkCalculateRulesDeadCycle 在 getAppFunctionByCode 为 null 时按 schemaCode.propertyCode 拼接、口径不一致)——工具反推后拒 app-not-published;类型 3/4/5 不受影响。",
|
|
94
|
+
]),
|
|
95
|
+
}),
|
|
96
|
+
"datarule.update": Object.freeze({
|
|
97
|
+
scope: DATARULE_SCOPE,
|
|
98
|
+
description: describeTool("Update an existing field-level data rule (bizdatarule/update) by `id`; requires `schemaCode` + `dataRuleType`.", [
|
|
99
|
+
"The rule is located by its `id` from datarule.list; duplicate property/type and absent ids refuse before POST. Because the platform update is full-row overwrite, the body is an explicit persisted-column whitelist plus the patch; readback returns before/after, changedFields, and unexpectedChanges.",
|
|
100
|
+
]),
|
|
101
|
+
}),
|
|
102
|
+
"datarule.delete": Object.freeze({
|
|
103
|
+
scope: DATARULE_SCOPE,
|
|
104
|
+
description: describeTool("Delete a field-level data rule (bizdatarule/delete) by `id` with exact-absence readback from the authoritative list.", [
|
|
105
|
+
"Absence is proven by a post-delete datarule/list that no longer carries the id.",
|
|
106
|
+
]),
|
|
107
|
+
}),
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
/** Per-type required option keys the engine actually reads (fail-closed gate). */
|
|
111
|
+
export const DATARULE_TYPE_OPTION_KEYS = Object.freeze({
|
|
112
|
+
1: Object.freeze(["verifyFormula", "regexp", "regexpText", "maxLength", "isEmptyRow", "isEmptyRowTips", "noRepeat", "properties", "conditions"]),
|
|
113
|
+
2: Object.freeze(["shortTextStitch", "expression", "aggregation"]),
|
|
114
|
+
3: Object.freeze(["displayFormula"]),
|
|
115
|
+
4: Object.freeze(["requiredFormula"]),
|
|
116
|
+
5: Object.freeze(["readonlyCondition"]),
|
|
117
|
+
});
|
|
118
|
+
export function assertDataRuleContract() {
|
|
119
|
+
assertLaneContract(DATARULE_LANE_CONTRACT);
|
|
120
|
+
if (DATARULE_TYPE_OPTION_KEYS[4][0] !== "requiredFormula")
|
|
121
|
+
throw new Error("datarule contract pins type 4 to requiredFormula");
|
|
122
|
+
if (DATARULE_TYPE_OPTION_KEYS[5][0] !== "readonlyCondition")
|
|
123
|
+
throw new Error("datarule contract pins type 5 to readonlyCondition");
|
|
124
|
+
assertDescriptionCarries(DATARULE_LANE_CONTRACT, "datarule.list", ["bizdatarule/list", "schemaCode", "dataRuleType"]);
|
|
125
|
+
assertDescriptionCarries(DATARULE_LANE_CONTRACT, "datarule.create", ["bizdatarule/create", "requiredFormula", "readonlyCondition", "displayFormula", "JSON string"]);
|
|
126
|
+
assertDescriptionCarries(DATARULE_LANE_CONTRACT, "datarule.update", ["bizdatarule/update", "id", "full-row", "changedFields"]);
|
|
127
|
+
assertDescriptionCarries(DATARULE_LANE_CONTRACT, "datarule.delete", ["bizdatarule/delete", "absence"]);
|
|
128
|
+
if (!DATARULE_WIRE.list.includes("bizdatarule/list"))
|
|
129
|
+
throw new Error("datarule contract must pin the list endpoint");
|
|
130
|
+
if (!DATARULE_WIRE.create.includes("BizDataRuleModel"))
|
|
131
|
+
throw new Error("datarule contract must pin the create body DTO");
|
|
132
|
+
if (!DATARULE_WIRE.update.includes("id + schemaCode + dataRuleType"))
|
|
133
|
+
throw new Error("datarule contract must pin the update required keys");
|
|
134
|
+
if (!DATARULE_WIRE.delete.includes("id=<ruleId>"))
|
|
135
|
+
throw new Error("datarule contract must pin the delete query param");
|
|
136
|
+
if (!DATARULE_WIRE.dataRuleTypes.includes("CHECK_RULE") || !DATARULE_WIRE.dataRuleTypes.includes("EXTENSION_RULE"))
|
|
137
|
+
throw new Error("datarule contract must pin the DataRuleType enum (1..5 in scope, 8 out)");
|
|
138
|
+
if (!DATARULE_WIRE.optionsByType.includes("shortTextStitch"))
|
|
139
|
+
throw new Error("datarule contract must pin the type-2 shortTextStitch option");
|
|
140
|
+
if (!DATARULE_WIRE.formulaReference.includes("裸 {字段}") || !DATARULE_WIRE.formulaReference.includes("datarule-formula-child-bare-reference"))
|
|
141
|
+
throw new Error("datarule contract must pin the type-2 formula reference semantics (bare=main / dotted=child)");
|
|
142
|
+
if (!DATARULE_WIRE.dataRuleCheckTypes.includes("SUBMIT_CHECK") || !DATARULE_WIRE.dataRuleCheckTypes.includes("REPEAT_CHECK") || !DATARULE_WIRE.dataRuleCheckTypes.includes("REGEX_CHECK"))
|
|
143
|
+
throw new Error("datarule contract must pin the DataRuleCheckType enum (regex/submit/repeat)");
|
|
144
|
+
if (!DATARULE_WIRE.optionsByCheckType.includes("submitDenied") || !DATARULE_WIRE.optionsByCheckType.includes("regexp"))
|
|
145
|
+
throw new Error("datarule contract must pin the per-checkType option shapes");
|
|
146
|
+
if (!DATARULE_WIRE.optionsByCheckType.includes("纯文本"))
|
|
147
|
+
throw new Error("datarule contract must pin the checkType-2 prompt as plain text (the portal renders options.prompt raw)");
|
|
148
|
+
if (!DATARULE_WIRE.optionsByCheckType.includes("isDataItem=0 → code 是字面文案"))
|
|
149
|
+
throw new Error("datarule contract must pin the checkType-5 repeat prompt isDataItem semantics (RepeatCheckValidator)");
|
|
150
|
+
if (!DATARULE_WIRE.optionsEncoding.includes("requiredFormula"))
|
|
151
|
+
throw new Error("datarule contract must pin the stringified options encoding");
|
|
152
|
+
if (!DATARULE_WIRE.formHistoryOpt.includes("historyOpt"))
|
|
153
|
+
throw new Error("datarule contract must pin the field-level vs form-control engine distinction");
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=datarule-contract.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { claim, declareLaneCoverage } from "./coverage-declaration.js";
|
|
2
|
+
import { DATARULE_LANE_CONTRACT } from "./datarule-contract.js";
|
|
3
|
+
const T = DATARULE_LANE_CONTRACT.tools;
|
|
4
|
+
export const DATARULE_COVERAGE = declareLaneCoverage(DATARULE_LANE_CONTRACT.lane, [
|
|
5
|
+
{
|
|
6
|
+
capability: "G1 field-level data rules (bizdatarule) lifecycle",
|
|
7
|
+
evidence: [
|
|
8
|
+
"packages/omc-cli/src/lanes/datarule.ts",
|
|
9
|
+
".scratch/omc-app-management/research/configuration-gap-report.md",
|
|
10
|
+
],
|
|
11
|
+
tools: [
|
|
12
|
+
claim("datarule.list", T["datarule.list"].scope),
|
|
13
|
+
claim("datarule.create", T["datarule.create"].scope),
|
|
14
|
+
claim("datarule.update", T["datarule.update"].scope),
|
|
15
|
+
claim("datarule.delete", T["datarule.delete"].scope),
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
]);
|
|
19
|
+
//# sourceMappingURL=datarule-coverage.js.map
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datarule condition-string protocol (round 4).
|
|
3
|
+
*
|
|
4
|
+
* Evidence (ADR 0005): the 8.6.26 platform does NOT validate data-rule
|
|
5
|
+
* option shapes at create time (`BizDataRuleRuntimeFacadeImpl.checkDataRuleFormat`
|
|
6
|
+
* checks only calculate-rule compilation and org conditions), so malformed
|
|
7
|
+
* conditions store verbatim and break the portal form renderer
|
|
8
|
+
* (`conditionGroup.compareType.undefined` family — reproduced live on the
|
|
9
|
+
* dogfood environment 2026-09-22: `readonlyCondition:"true"` crashed the
|
|
10
|
+
* renderer and the target control never rendered; the protocol-correct
|
|
11
|
+
* condition restored it).
|
|
12
|
+
*
|
|
13
|
+
* The protocol itself is reproduced from three independent sources:
|
|
14
|
+
* 1. backend readback parser `BizDataRuleController.parseFormula`
|
|
15
|
+
* (decompiled-engine .../app/BizDataRuleController.java:156-176): splits
|
|
16
|
+
* on " || " then " && "; a condition carrying `valueType` must split
|
|
17
|
+
* `(" ", 3)` into `{field}` / operator / JSON.
|
|
18
|
+
* 2. frontend designer `initModel` (frontend-dist/admin/js/11993.a661fd05.js):
|
|
19
|
+
* `requiredType = e && "true" !== e ? 2 : 1` — literal "true" is the
|
|
20
|
+
* unconditional-required marker; fields serialize as `"{"+code+"}"`.
|
|
21
|
+
* 3. frontend renderer mapping (frontend-dist/admin/js/19407.d285b567.js
|
|
22
|
+
* compareTypeMapping): operators == != > < >= <= Contains NotContains
|
|
23
|
+
* IsEmpty IsNotEmpty.
|
|
24
|
+
* 4. runtime validators RequiredDataRuleValidator.java:526-627 /
|
|
25
|
+
* ReadOnlyValidator.java:497-597 / SubmitCheckValidator.java:390-490
|
|
26
|
+
* (three independent implementations of the same value-type set).
|
|
27
|
+
*
|
|
28
|
+
* Per ADR 0005 this gate enforces only reproduced behavior: unconditional
|
|
29
|
+
* readonly (`readonlyFormula` / `functionType:"true"`) is NOT accepted until
|
|
30
|
+
* a live experiment reproduces it — the reproduced unconditional form for
|
|
31
|
+
* readonly is a tautological condition.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Operators — the SERIALIZED symbol vocabulary the runtime actually executes.
|
|
35
|
+
*
|
|
36
|
+
* Live correction 2026-09-23: the earlier `eq/ne/gt/…` set was the condition
|
|
37
|
+
* BUILDER's internal enum, not what is stored. The filled-form runtime compiles
|
|
38
|
+
* the stored string with `new Function` after `parseConditionExp` rewrites the
|
|
39
|
+
* symbols (`==`→this.equal, `>`→this.greaterThan, …), so a stored `eq` throws
|
|
40
|
+
* `SyntaxError: Unexpected identifier 'eq'`, aborts the form render, and the
|
|
41
|
+
* control never appears (reproduced live on bench20: the whole form went blank).
|
|
42
|
+
*
|
|
43
|
+
* Evidence:
|
|
44
|
+
* 1. a designer-authored row in the same environment stores a SYMBOL:
|
|
45
|
+
* `r4_auto_purchase_apply6.apply_description.requiredFormula =
|
|
46
|
+
* {apply_amount} > {"valueType":"specifyOption","value":5000}`.
|
|
47
|
+
* 2. runtime operator→label map (frontend chunk 19497):
|
|
48
|
+
* `{"==":"等于","!=":"不等于",">":"大于","<":"小于",">=":"大于等于",
|
|
49
|
+
* "<=":"小于等于",Contains:"包含",NotContains:"不包含",
|
|
50
|
+
* IsEmpty:"为空",IsNotEmpty:"不为空"}`
|
|
51
|
+
* 3. the condition builder's serialized `value` set (same chunk):
|
|
52
|
+
* `== != > < >= <= Contains NotContains BETWEEN NOTBETWEEN IsEmpty
|
|
53
|
+
* IsNotEmpty CT_ALL NCT_ALL CT_ANY NCT_ANY EQ_ANY NEQ_ANY`.
|
|
54
|
+
*/
|
|
55
|
+
const CONDITION_OPERATORS = new Set([
|
|
56
|
+
// CHINESE operators — the CANONICAL serialized vocabulary. Production
|
|
57
|
+
// observation 2026-09-23: a real designer condition in a form control's
|
|
58
|
+
// `options` is `{caseNo} 不为空`; the form-options scan found 等于 11,
|
|
59
|
+
// 等于任意一个 3, 不等于任意一个 2, 不等于 2, 不为空 4, 为空 4. The condition
|
|
60
|
+
// builder serializes to these words and the runtime parses them.
|
|
61
|
+
"等于", "不等于", "大于", "小于", "大于等于", "小于等于",
|
|
62
|
+
"包含", "不包含", "包含全部", "不包含全部", "包含任意一个", "不包含任意一个",
|
|
63
|
+
"等于任意一个", "不等于任意一个", "属于", "不属于", "介于", "不介于",
|
|
64
|
+
"为空", "不为空",
|
|
65
|
+
// SYMBOLS — also accepted by the runtime (parseConditionExp rewrites them);
|
|
66
|
+
// kept so symbol-authored conditions (bench20/21, verified working) stay valid.
|
|
67
|
+
"==", "!=", ">", "<", ">=", "<=",
|
|
68
|
+
"Contains", "NotContains", "IsEmpty", "IsNotEmpty",
|
|
69
|
+
"BETWEEN", "NOTBETWEEN",
|
|
70
|
+
"CT_ALL", "NCT_ALL", "CT_ANY", "NCT_ANY", "EQ_ANY", "NEQ_ANY",
|
|
71
|
+
]);
|
|
72
|
+
/** Null-check operators take no value: `{field} 不为空` / `{field} IsEmpty`. */
|
|
73
|
+
const CONDITION_UNARY_OPERATORS = new Set(["为空", "不为空", "IsEmpty", "IsNotEmpty"]);
|
|
74
|
+
/** valueJson.valueType strings evidenced by the three runtime validators. */
|
|
75
|
+
const CONDITION_VALUE_TYPES = new Set([
|
|
76
|
+
"custom", "specifyDate", "specifyUser", "specifyDepartment", "EXPRESSION", "currentModel",
|
|
77
|
+
"specifyOption", "specifyRange", "yesterday", "tomorrow", "today", "true", "false", "dynamicRange",
|
|
78
|
+
]);
|
|
79
|
+
/**
|
|
80
|
+
* A single condition: `{fieldCode} operator {"valueType":...}`. The operator is
|
|
81
|
+
* a serialized symbol; the value is JSON (which itself may contain spaces), so
|
|
82
|
+
* parse with a regex rather than a plain split.
|
|
83
|
+
*/
|
|
84
|
+
function conditionShapeError(condition) {
|
|
85
|
+
const match = /^(\{[A-Za-z0-9_.]+\})\s+(\S+)(?:\s+([\s\S]+))?$/.exec(condition.trim());
|
|
86
|
+
if (match === null)
|
|
87
|
+
return `条件「${condition}」必须是「{字段} 操作符 [值]」(如 {amount} 不为空 / {amount} 等于 {"valueType":"specifyOption","value":500})`;
|
|
88
|
+
const [, , operator, value] = match;
|
|
89
|
+
if (!CONDITION_OPERATORS.has(operator)) {
|
|
90
|
+
return `操作符「${operator}」不在平台条件词表内(合法:中文 等于/不等于/大于/大于等于/包含/包含全部/等于任意一个/为空/不为空…,也接受符号 == != > >= Contains IsEmpty…);**不是** eq/gt/isnull 那套内部枚举(会让门户表单渲染 SyntaxError、整表空白)`;
|
|
91
|
+
}
|
|
92
|
+
if (CONDITION_UNARY_OPERATORS.has(operator)) {
|
|
93
|
+
return value === undefined ? null : `「${operator}」是不带值的操作符,不应带值(收到:${value.slice(0, 30)})`;
|
|
94
|
+
}
|
|
95
|
+
if (value === undefined)
|
|
96
|
+
return `「${operator}」需要值(JSON 对象)`;
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = JSON.parse(value);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return `值段必须是 JSON 对象(如 {"valueType":"specifyOption","value":500}),收到:${value.slice(0, 40)}`;
|
|
103
|
+
}
|
|
104
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
105
|
+
return "值段必须解析为 JSON 对象";
|
|
106
|
+
const valueType = parsed.valueType;
|
|
107
|
+
if (typeof valueType !== "string" || !CONDITION_VALUE_TYPES.has(valueType)) {
|
|
108
|
+
return `值段 JSON 的 valueType「${String(valueType)}」不被平台校验器接受(合法:${[...CONDITION_VALUE_TYPES].join("/")})`;
|
|
109
|
+
}
|
|
110
|
+
// `specifyOption` means the value is one of the TARGET FIELD's own option
|
|
111
|
+
// codes — always a STRING. A number/boolean here is a literal the author meant
|
|
112
|
+
// to compare (`custom`); worse, the designer's rule formatter maps the target
|
|
113
|
+
// field's options and crashes (`t.map is not a function`) when a non-option
|
|
114
|
+
// field carries specifyOption, taking the WHOLE form blank (live 2026-09-24
|
|
115
|
+
// bench33/bench36). Refuse the mistaken valueType at write time.
|
|
116
|
+
if (valueType === "specifyOption") {
|
|
117
|
+
const value = parsed.value;
|
|
118
|
+
if (typeof value !== "string")
|
|
119
|
+
return `specifyOption 的 value 必须是字符串(选项 code);收到 ${JSON.stringify(value)}。字面量(数值/布尔/文本)比较请用 {\"valueType\":\"custom\",\"value\":${JSON.stringify(value)}}——specifyOption 用在无选项字段会让设计器规则格式化器崩溃、整个表单空白`;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Validate a condition string: groups joined by " || ", conditions within a
|
|
125
|
+
* group by " && ", each condition the three-part shape above. `allowTrue`
|
|
126
|
+
* admits the literal `"true"` marker (unconditional REQUIRED, per the
|
|
127
|
+
* designer's `requiredType` check; only reproduced for type 4).
|
|
128
|
+
*/
|
|
129
|
+
export function conditionProtocolError(formula, options = {}) {
|
|
130
|
+
if (typeof formula !== "string" || formula.trim().length === 0)
|
|
131
|
+
return "必须是非空字符串";
|
|
132
|
+
if (options.allowTrue === true && formula.trim() === "true")
|
|
133
|
+
return null;
|
|
134
|
+
for (const group of formula.split(/\s*\|\|\s*/)) {
|
|
135
|
+
for (const condition of group.split(/\s*&&\s*/)) {
|
|
136
|
+
const error = conditionShapeError(condition);
|
|
137
|
+
if (error !== null)
|
|
138
|
+
return error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The protocol-correct shapes, embedded in refusal messages and describes.
|
|
145
|
+
*
|
|
146
|
+
* `valueType` follows the COMPARED VALUE, not the field kind:
|
|
147
|
+
* - `custom` — a literal typed by the author (数/日期/文本字面量). Use
|
|
148
|
+
* this for a numeric/date/text comparison, e.g. 金额 大于等于 10000.
|
|
149
|
+
* - `specifyOption` — the value must be one of the TARGET FIELD's own options
|
|
150
|
+
* (单选/复选/下拉). Only valid when that field actually has an options list.
|
|
151
|
+
*
|
|
152
|
+
* Live 2026-09-24 (bench33): putting `specifyOption` on a field with NO options
|
|
153
|
+
* (金额 is a number) makes the FORM DESIGNER's rule formatter throw
|
|
154
|
+
* `TypeError: t.map is not a function` while listing the rule, so the rule row
|
|
155
|
+
* SILENTLY DISAPPEARS from the Required/Read-only Condition dialog — even though
|
|
156
|
+
* the runtime honors the condition. Switching the literal to `custom` renders
|
|
157
|
+
* the row again. So the examples below use `custom` for literals.
|
|
158
|
+
*/
|
|
159
|
+
export const CONDITION_PROTOCOL_EXAMPLES = Object.freeze({
|
|
160
|
+
conditional: '{leave_amount} 大于等于 {"valueType":"custom","value":500}',
|
|
161
|
+
unary: "{leave_reason} 不为空",
|
|
162
|
+
and: '{leave_amount} 大于等于 {"valueType":"custom","value":500} && {leave_reason} 为空',
|
|
163
|
+
or: '{a} 等于 {"valueType":"specifyOption","value":"选项A"} || {b} 等于 {"valueType":"specifyOption","value":"选项B"}',
|
|
164
|
+
unconditionalRequired: "true",
|
|
165
|
+
alwaysTrueReadonly: '{leave_amount} 大于 {"valueType":"custom","value":-999999999}',
|
|
166
|
+
});
|
|
167
|
+
/**
|
|
168
|
+
* Form-interaction conditions live in the FIELD's `options`, not in a
|
|
169
|
+
* bizdatarule row. Reproduced live (2026-09-22, Playwright, dogfood `local`):
|
|
170
|
+
* a `datarule.create` type-4 rule did NOT block the portal form, while
|
|
171
|
+
* `field.update` with `options.requiredFormula` set the red required marker and
|
|
172
|
+
* blocked submit. Keys, from the form designer schema
|
|
173
|
+
* (frontend-dist/admin/js/12404.1675d362.js):
|
|
174
|
+
* displayFormula String — 显示条件
|
|
175
|
+
* requiredFormula String — 是否必填("true" = 恒必填)
|
|
176
|
+
* readonlyCondition String — 只读条件
|
|
177
|
+
* readonlyFormula Boolean — 是否只读(true = 恒只读)
|
|
178
|
+
*/
|
|
179
|
+
export const FORM_CONDITION_OPTION_KEYS = Object.freeze({
|
|
180
|
+
requiredFormula: "required",
|
|
181
|
+
readonlyCondition: "readonly",
|
|
182
|
+
displayFormula: "display",
|
|
183
|
+
readonlyFormula: "readonly-bool",
|
|
184
|
+
});
|
|
185
|
+
/**
|
|
186
|
+
* Validate the form-interaction condition keys when they appear in field options.
|
|
187
|
+
*
|
|
188
|
+
* An EMPTY STRING is the platform's "no condition" value (real designer rows
|
|
189
|
+
* store `requiredFormula:""`), so it is accepted as a CLEAR — field.update
|
|
190
|
+
* passes it through and the paired bizdatarule row is deleted.
|
|
191
|
+
*/
|
|
192
|
+
export function formConditionOptionsError(options) {
|
|
193
|
+
if ("requiredFormula" in options) {
|
|
194
|
+
if (typeof options.requiredFormula !== "string")
|
|
195
|
+
return "options.requiredFormula 必须是字符串(恒必填用字面量 \"true\";清除用空串)";
|
|
196
|
+
if (options.requiredFormula.trim() !== "") {
|
|
197
|
+
const error = conditionProtocolError(options.requiredFormula, { allowTrue: true });
|
|
198
|
+
if (error !== null)
|
|
199
|
+
return `options.requiredFormula ${error};示例:${CONDITION_PROTOCOL_EXAMPLES.conditional}`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if ("readonlyCondition" in options) {
|
|
203
|
+
if (typeof options.readonlyCondition !== "string")
|
|
204
|
+
return "options.readonlyCondition 必须是字符串;恒只读请用 options.readonlyFormula:true,清除用空串";
|
|
205
|
+
if (options.readonlyCondition.trim() !== "") {
|
|
206
|
+
const error = conditionProtocolError(options.readonlyCondition);
|
|
207
|
+
if (error !== null)
|
|
208
|
+
return `options.readonlyCondition ${error};示例:${CONDITION_PROTOCOL_EXAMPLES.conditional}`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if ("displayFormula" in options) {
|
|
212
|
+
if (typeof options.displayFormula !== "string")
|
|
213
|
+
return "options.displayFormula 必须是字符串;清除用空串";
|
|
214
|
+
if (options.displayFormula.trim() !== "") {
|
|
215
|
+
const error = conditionProtocolError(options.displayFormula);
|
|
216
|
+
if (error !== null)
|
|
217
|
+
return `options.displayFormula ${error};示例:${CONDITION_PROTOCOL_EXAMPLES.conditional}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if ("readonlyFormula" in options && typeof options.readonlyFormula !== "boolean")
|
|
221
|
+
return "options.readonlyFormula 必须是布尔(true = 恒只读)";
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* `bizdatarule` condition operators — a DIFFERENT vocabulary from the
|
|
226
|
+
* form-interaction (field options) engine.
|
|
227
|
+
*
|
|
228
|
+
* Live correction 2026-09-23, from 146 real production type-3/4/5 rows:
|
|
229
|
+
* `bizdatarule` conditions are authored with CHINESE operator words —
|
|
230
|
+
* 等于(76), 包含全部(30), 包含(30), 为空(20), 不为空(19), 等于任意一个(15),
|
|
231
|
+
* 不等于(11), 大于(1) — e.g. `{supplementFile} 不为空`. The form runtime's
|
|
232
|
+
* symbol set (`==`, `>`, `Contains`, …) is the field-options engine, not this
|
|
233
|
+
* one. Accept BOTH here (symbols were already in tests and are harmless) so a
|
|
234
|
+
* valid production rule is never falsely refused.
|
|
235
|
+
*/
|
|
236
|
+
const DATARULE_CONDITION_OPERATORS = new Set([
|
|
237
|
+
// Chinese (real production vocabulary)
|
|
238
|
+
"等于", "不等于", "大于", "小于", "大于等于", "小于等于",
|
|
239
|
+
"包含", "不包含", "包含全部", "不包含全部", "包含任意一个", "不包含任意一个",
|
|
240
|
+
"等于任意一个", "不等于任意一个", "属于", "不属于", "介于", "不介于",
|
|
241
|
+
"为空", "不为空",
|
|
242
|
+
// Symbols (form-engine vocabulary; kept for back-compat)
|
|
243
|
+
"==", "!=", ">", "<", ">=", "<=",
|
|
244
|
+
"Contains", "NotContains", "IsEmpty", "IsNotEmpty",
|
|
245
|
+
"BETWEEN", "NOTBETWEEN", "CT_ALL", "NCT_ALL", "CT_ANY", "NCT_ANY", "EQ_ANY", "NEQ_ANY",
|
|
246
|
+
]);
|
|
247
|
+
/** Null-check operators take no value: `{field} 不为空`. */
|
|
248
|
+
const DATARULE_UNARY_OPERATORS = new Set(["为空", "不为空", "IsEmpty", "IsNotEmpty"]);
|
|
249
|
+
/**
|
|
250
|
+
* A single `bizdatarule` condition. Grammar: `{field} 操作符 [值]`.
|
|
251
|
+
* - unary operators (为空/不为空/IsEmpty/IsNotEmpty) carry no value;
|
|
252
|
+
* - others carry a value: a JSON object (`{"valueType":…,"value":…}` or `{...}`)
|
|
253
|
+
* OR a bare scalar (real rows also use plain values).
|
|
254
|
+
*/
|
|
255
|
+
function dataRuleConditionError(condition) {
|
|
256
|
+
const trimmed = condition.trim();
|
|
257
|
+
if (trimmed.length === 0)
|
|
258
|
+
return "条件为空";
|
|
259
|
+
const match = /^(\{[A-Za-z0-9_.]+\})\s+(\S+)(?:\s+([\s\S]+))?$/.exec(trimmed);
|
|
260
|
+
if (match === null)
|
|
261
|
+
return `条件「${condition}」必须是「{字段} 操作符 [值]」(如 {字段} 不为空 / {字段} 等于 {"valueType":"specifyOption","value":"A"})`;
|
|
262
|
+
const [, , operator, value] = match;
|
|
263
|
+
if (!DATARULE_CONDITION_OPERATORS.has(operator))
|
|
264
|
+
return `操作符「${operator}」不在 bizdatarule 词表内(中文:等于/不等于/大于/小于/大于等于/小于等于/包含/包含全部/包含任意一个/等于任意一个/属于/为空/不为空…;也接受符号 == != > < >= <= Contains IsEmpty 等)`;
|
|
265
|
+
const unary = DATARULE_UNARY_OPERATORS.has(operator);
|
|
266
|
+
if (unary)
|
|
267
|
+
return value === undefined ? null : `「${operator}」是不带值的操作符,不应带值(收到:${value.slice(0, 30)})`;
|
|
268
|
+
if (value === undefined)
|
|
269
|
+
return `「${operator}」需要值(JSON 对象或标量)`;
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
/** Validate a full bizdatarule condition string (multi via && / ||). */
|
|
273
|
+
export function dataRuleConditionProtocolError(formula, options = {}) {
|
|
274
|
+
if (typeof formula !== "string" || formula.trim().length === 0)
|
|
275
|
+
return "必须是非空字符串";
|
|
276
|
+
if (options.allowTrue === true && formula.trim() === "true")
|
|
277
|
+
return null;
|
|
278
|
+
for (const group of formula.split(/\s*\|\|\s*/)) {
|
|
279
|
+
for (const condition of group.split(/\s*&&\s*/)) {
|
|
280
|
+
const error = dataRuleConditionError(condition);
|
|
281
|
+
if (error !== null)
|
|
282
|
+
return error;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
/** Per-type option-shape validation for datarule.create/update. Returns a refusal message or null. */
|
|
288
|
+
export function dataRuleOptionsShapeError(dataRuleType, options) {
|
|
289
|
+
if (dataRuleType === 4) {
|
|
290
|
+
if (typeof options.requiredFormula !== "string")
|
|
291
|
+
return "type4 必填规则必须有 options.requiredFormula";
|
|
292
|
+
return dataRuleConditionProtocolError(options.requiredFormula, { allowTrue: true });
|
|
293
|
+
}
|
|
294
|
+
if (dataRuleType === 5) {
|
|
295
|
+
if (typeof options.readonlyCondition !== "string")
|
|
296
|
+
return "type5 只读规则必须有 options.readonlyCondition";
|
|
297
|
+
// Unconditional readonly ("true"/functionType) is NOT reproduced yet — see
|
|
298
|
+
// the module comment; a tautological condition is the verified form.
|
|
299
|
+
return dataRuleConditionProtocolError(options.readonlyCondition);
|
|
300
|
+
}
|
|
301
|
+
if (dataRuleType === 3) {
|
|
302
|
+
if (typeof options.displayFormula !== "string")
|
|
303
|
+
return "type3 显示规则必须有 options.displayFormula";
|
|
304
|
+
return dataRuleConditionProtocolError(options.displayFormula);
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=datarule-protocol.js.map
|