@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,818 @@
|
|
|
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 { ToolRefusalError } from "../tool-types.js";
|
|
6
|
+
import { writeTool } from "../write.js";
|
|
7
|
+
import { decodeJsonColumn, JSON_COLUMN_PASSTHROUGH } from "../json-column.js";
|
|
8
|
+
import { defineTool } from "../tool-types.js";
|
|
9
|
+
import { resolveDataRuleRow } from "../identity.js";
|
|
10
|
+
import { wireCall, wireRows, WireError } from "../wire.js";
|
|
11
|
+
import { runtimeWriteWithAppMapping } from "../runtime-support.js";
|
|
12
|
+
import { DATARULE_LANE_CONTRACT, DATARULE_TYPE_OPTION_KEYS } from "./datarule-contract.js";
|
|
13
|
+
import { CONDITION_PROTOCOL_EXAMPLES, dataRuleOptionsShapeError } from "./datarule-protocol.js";
|
|
14
|
+
const DATARULE_LANE = DATARULE_LANE_CONTRACT.lane;
|
|
15
|
+
const DATARULE_TOOLS = DATARULE_LANE_CONTRACT.tools;
|
|
16
|
+
const dataRuleTypeArg = z
|
|
17
|
+
.number()
|
|
18
|
+
.int()
|
|
19
|
+
.min(1)
|
|
20
|
+
.max(5)
|
|
21
|
+
.describe("field-level rule type: 1 校验 / 2 计算拼接 / 3 显示 / 4 必填 / 5 只读 (8 EXTENSION is out of scope)");
|
|
22
|
+
const dataRuleOptionsArg = z
|
|
23
|
+
.record(z.unknown())
|
|
24
|
+
.describe("rule options object, sent to the wire as a JSON string; type4 requires requiredFormula, type5 readonlyCondition, type3 displayFormula, type2 shortTextStitch|expression, type1 a check key keyed by checkType");
|
|
25
|
+
/**
|
|
26
|
+
* DataRuleCheckType (8.6.26 cloudpivot-shared DataRuleCheckType.java) — the
|
|
27
|
+
* `checkType` discriminator for a `dataRuleType:1` (CHECK_RULE / 提交校验规则) row.
|
|
28
|
+
* Each id maps to a validator under `engine/bo/datarule/impl/checkType/` and reads
|
|
29
|
+
* a distinct options key. `checkType` is ONLY meaningful with dataRuleType 1.
|
|
30
|
+
*
|
|
31
|
+
* - 1 REGEX_CHECK 正则校验 options {regexp:"/…/", regexpText:{zh,en}} (regexp is slash-wrapped; RegexCheckValidator strips the outer chars)
|
|
32
|
+
* - 2 SUBMIT_CHECK 提交校验 options {conditions:"<cond>[ &&| || <cond>]", verifyType:"submitDenied"? , prompt:"<plain text — the portal shows options.prompt verbatim, so do NOT URL-encode it>"} ← cross-field rules use a currentModel value object on the right (e.g. {end} 小于 {"valueType":"currentModel","value":"start"})
|
|
33
|
+
* - 3 EMPTY_ROW_CHECK 空行校验 options {isEmptyRow:true, isEmptyRowTips?}
|
|
34
|
+
* - 4 MAX_LENGTH_CHECK 最大长度 options {maxLength:<int>}
|
|
35
|
+
* - 5 REPEAT_CHECK 重复校验 options {properties:[<fieldCodes>], isSubTableValidationAllData?, prompt:[{code,isDataItem}]}
|
|
36
|
+
*/
|
|
37
|
+
export const DATARULE_CHECK_TYPES = Object.freeze({
|
|
38
|
+
1: "REGEX_CHECK 正则校验(options.regexp 需 /…/ 包裹 + regexpText)",
|
|
39
|
+
2: "SUBMIT_CHECK 提交校验(options.conditions 条件串 + verifyType:submitDenied 表示满足即拒绝 + prompt)",
|
|
40
|
+
3: "EMPTY_ROW_CHECK 空行校验(options.isEmptyRow + isEmptyRowTips)",
|
|
41
|
+
4: "MAX_LENGTH_CHECK 最大长度校验(options.maxLength)",
|
|
42
|
+
5: "REPEAT_CHECK 重复校验(options.properties 数组 + isSubTableValidationAllData + prompt)",
|
|
43
|
+
});
|
|
44
|
+
const checkTypeArg = z
|
|
45
|
+
.number()
|
|
46
|
+
.int()
|
|
47
|
+
.min(1)
|
|
48
|
+
.max(5)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe(`dataRuleType:1 的校验类别(DataRuleCheckType):${Object.entries(DATARULE_CHECK_TYPES).map(([id, text]) => `${id}=${text.split(" ")[0]}`).join(" / ")}。仅在 dataRuleType=1 时使用`);
|
|
51
|
+
/** `checkType` discriminates a dataRuleType 1 row; it is meaningless on any other type. */
|
|
52
|
+
export function assertDataRuleCheckType(dataRuleType, checkType) {
|
|
53
|
+
if (checkType === undefined)
|
|
54
|
+
return;
|
|
55
|
+
if (dataRuleType !== 1)
|
|
56
|
+
throw new ToolRefusalError({
|
|
57
|
+
status: "refused",
|
|
58
|
+
blocker: "datarule-check-type-invalid",
|
|
59
|
+
dataRuleType,
|
|
60
|
+
checkType,
|
|
61
|
+
message: `checkType=${checkType} 只用于 dataRuleType=1(提交校验规则);dataRuleType=${dataRuleType} 不接受 checkType`,
|
|
62
|
+
});
|
|
63
|
+
if (!(checkType in DATARULE_CHECK_TYPES))
|
|
64
|
+
throw new ToolRefusalError({
|
|
65
|
+
status: "refused",
|
|
66
|
+
blocker: "datarule-check-type-invalid",
|
|
67
|
+
checkType,
|
|
68
|
+
message: `checkType=${checkType} 不是已知 DataRuleCheckType(${Object.keys(DATARULE_CHECK_TYPES).join("/")})`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const propertyCodeArg = z.string().min(1).optional().describe("目标字段 code(单字段简写);与 propertyCodes 二选一,至少给一个");
|
|
72
|
+
const propertyCodesArg = z.array(z.string().min(1)).min(1).optional().describe("目标字段 code 数组(真实设计器形态,多字段用;此时 propertyCode 置空)");
|
|
73
|
+
/**
|
|
74
|
+
* The target field(s) a rule binds.
|
|
75
|
+
*
|
|
76
|
+
* Real designer rows (2026-09-23 observation) bind via `propertyCodes` (ARRAY;
|
|
77
|
+
* top-level `propertyCode` is null) and mirror them inside `options.propertyCodes`
|
|
78
|
+
* (comma-joined). The legacy single `propertyCode` column is kept for the
|
|
79
|
+
* one-field case, but the array is what the engine reads — writing only the
|
|
80
|
+
* singular column is the suspected root cause of a type-4 rule having no portal
|
|
81
|
+
* effect.
|
|
82
|
+
*/
|
|
83
|
+
function dataRuleTargetCodes(args) {
|
|
84
|
+
const codes = (args.propertyCodes && args.propertyCodes.length > 0 ? args.propertyCodes : [args.propertyCode]).map((code) => String(code ?? "")).filter((code) => code.length > 0);
|
|
85
|
+
return [...new Set(codes)];
|
|
86
|
+
}
|
|
87
|
+
/** Parse the stored options JSON string for readback; policy: non-strings and unparseable strings pass through verbatim. */
|
|
88
|
+
function parseOptions(value) {
|
|
89
|
+
return decodeJsonColumn(value, JSON_COLUMN_PASSTHROUGH);
|
|
90
|
+
}
|
|
91
|
+
/** Target field codes from a stored row (propertyCodes array preferred; legacy singular fallback). */
|
|
92
|
+
function storedTargetCodes(row) {
|
|
93
|
+
const raw = row.propertyCodes;
|
|
94
|
+
if (Array.isArray(raw))
|
|
95
|
+
return raw.map((code) => String(code)).filter((code) => code.length > 0);
|
|
96
|
+
const single = row.propertyCode;
|
|
97
|
+
return single !== undefined && single !== null && String(single).length > 0 ? [String(single)] : [];
|
|
98
|
+
}
|
|
99
|
+
function decodeDataRule(row) {
|
|
100
|
+
const options = parseOptions(row.options);
|
|
101
|
+
const codes = storedTargetCodes(row);
|
|
102
|
+
const decoded = {
|
|
103
|
+
id: row.id ?? null,
|
|
104
|
+
schemaCode: row.schemaCode ?? null,
|
|
105
|
+
propertyCode: row.propertyCode ?? (codes.length === 1 ? codes[0] : null),
|
|
106
|
+
propertyCodes: codes,
|
|
107
|
+
dataRuleType: row.dataRuleType ?? null,
|
|
108
|
+
checkType: row.checkType ?? null,
|
|
109
|
+
childRule: row.childRule === true,
|
|
110
|
+
name: row.name ?? null,
|
|
111
|
+
sortKey: row.sortKey ?? null,
|
|
112
|
+
options,
|
|
113
|
+
};
|
|
114
|
+
// Round 4 (ADR 0005): read-side annotation. The platform accepted and stored
|
|
115
|
+
// malformed option shapes for years; the same protocol validator that gates
|
|
116
|
+
// writes marks stored landmines here. Report-only — stored data is never
|
|
117
|
+
// auto-modified.
|
|
118
|
+
const type = Number(row.dataRuleType);
|
|
119
|
+
if ([3, 4, 5].includes(type) && options !== null && typeof options === "object" && !Array.isArray(options)) {
|
|
120
|
+
const error = dataRuleOptionsShapeError(type, options);
|
|
121
|
+
if (error !== null)
|
|
122
|
+
decoded.shapeWarning = `stored options fail the platform protocol (${error}); the portal form renderer may crash on this rule — fix with datarule.update using e.g. requiredFormula: "${CONDITION_PROTOCOL_EXAMPLES.conditional}"`;
|
|
123
|
+
}
|
|
124
|
+
else if (type === 1 && Number(row.checkType) === 2 && options !== null && typeof options === "object" && !Array.isArray(options)) {
|
|
125
|
+
// Live 2026-09-24: a bare {fieldCode} right-hand value in a cross-field submit
|
|
126
|
+
// condition stores fine but is never resolved to the peer field's value, so
|
|
127
|
+
// the rule silently does nothing. Mark it (fix with a currentModel value).
|
|
128
|
+
const conditions = options.conditions;
|
|
129
|
+
if (typeof conditions === "string" && SUBMIT_CROSS_FIELD_BARE_TOKEN.test(conditions))
|
|
130
|
+
decoded.shapeWarning = 'checkType 2 跨字段条件用了裸 {字段code} 右值,平台不会解析成字段值——该规则静默不生效;改用 {"valueType":"currentModel","value":"<字段code>"}';
|
|
131
|
+
else {
|
|
132
|
+
const prompt = options.prompt;
|
|
133
|
+
if (typeof prompt === "string" && isUrlEncoded(prompt))
|
|
134
|
+
decoded.shapeWarning = 'checkType 2 的 prompt 是 URL 编码的——门户对这个顶层 options.prompt 原样显示,验证弹窗会显示 %E5..乱码;用 datarule.update 改成纯文本';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (type === 2 && row.childRule === true && options !== null && typeof options === "object" && !Array.isArray(options)) {
|
|
138
|
+
// Round 7: a child-table formula with bare tokens. Without field context we
|
|
139
|
+
// cannot say whether each bare token is a child field (the 50000 landmine)
|
|
140
|
+
// or a legitimate main-record field, so this is an advisory, not a warning —
|
|
141
|
+
// it surfaces every bare token for review (the write gate is field-aware).
|
|
142
|
+
const formula = type2FormulaText(options);
|
|
143
|
+
if (formula !== null) {
|
|
144
|
+
const bare = [...formula.matchAll(/\{([^{}]+)\}/g)]
|
|
145
|
+
.map((match) => match[1].trim())
|
|
146
|
+
.filter((token) => /^[A-Za-z][A-Za-z0-9_]*$/.test(token));
|
|
147
|
+
if (bare.length > 0)
|
|
148
|
+
decoded.formulaHint = `子表公式含裸引用 ${[...new Set(bare)].map((token) => `{${token}}`).join("、")}:裸 {字段}=主记录字段,带点 {子表code.字段}=子表字段;若本意是同级子字段请带点,否则提交会因主记录字段缺失报 50000`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return decoded;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Fail closed when the options object cannot carry the key the engine reads
|
|
155
|
+
* for this type (RequiredDataRuleValidator / ReadOnlyValidator /
|
|
156
|
+
* CalculateRuleProcessor / list controller `parseFormula`).
|
|
157
|
+
*/
|
|
158
|
+
/**
|
|
159
|
+
* T-04 (round 2) — value-shape probe for the type-2 CALCULATE expression
|
|
160
|
+
* carriers. The 8.6.26 engine (`BizObjectSchemaManagerImpl.getCalRuleExpression`
|
|
161
|
+
* :842-875) reads `shortTextStitch` (falling back to `value`), JSON-parses it
|
|
162
|
+
* and then reads ITS `formula` key; `formulaType:"AGGREGATION"` instead reads
|
|
163
|
+
* `aggregation` (a JSON string) → `AggregationFormula.formula`. A bare
|
|
164
|
+
* non-JSON string makes `getCalRuleExpression` return null, which the
|
|
165
|
+
* dead-cycle check (`checkCalculateRulesDeadCycle`) then misreports as
|
|
166
|
+
* `305003 死循环`.
|
|
167
|
+
*/
|
|
168
|
+
function calcExpressionShapeError(carrier, value) {
|
|
169
|
+
if (value === undefined)
|
|
170
|
+
return `${carrier} is required`;
|
|
171
|
+
if (typeof value !== "string")
|
|
172
|
+
return `${carrier} must be a JSON *string* (received ${typeof value})`;
|
|
173
|
+
let parsed;
|
|
174
|
+
try {
|
|
175
|
+
parsed = JSON.parse(value);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return `${carrier} is not valid JSON (a bare "{field}" string fails JSONUtil.isJson → engine returns null → false 305003 死循环)`;
|
|
179
|
+
}
|
|
180
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
181
|
+
return `${carrier} must parse to a JSON object`;
|
|
182
|
+
const formula = parsed.formula;
|
|
183
|
+
if (typeof formula !== "string" || formula.trim().length === 0)
|
|
184
|
+
return `${carrier} JSON object is missing a non-empty "formula"`;
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Mirror the frontend formula editor's `parseFormulaFromEditorText`
|
|
189
|
+
* (admin bundle 45843…): every `<editorMark…>content</editorMark>` token is
|
|
190
|
+
* replaced by `{content}` — **the editor adds the braces itself**. `editorText`
|
|
191
|
+
* is the rich display the designer stores alongside the canonical `formula`;
|
|
192
|
+
* the UI re-derives the formula from it on load. A caller must therefore put the
|
|
193
|
+
* BARE token inside the tag (no braces); wrapping the brace-including token
|
|
194
|
+
* yields `<mark>{code}</mark>` → rebuilds `{{code}}` ≠ `formula` → the rule
|
|
195
|
+
* renders 已失效 (live 2026-10-08, SCM sandbox: `{inq_quotes.quote_price_incl}`
|
|
196
|
+
* inside the tag).
|
|
197
|
+
*/
|
|
198
|
+
export function formulaFromEditorText(editorText, editorMark) {
|
|
199
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
200
|
+
const re = new RegExp(`<${escape(editorMark)}[^>]*>(.*?)</${escape(editorMark)}>`, "g");
|
|
201
|
+
const flat = editorText.replace(/[\r]?\n/g, "");
|
|
202
|
+
let cursor = 0;
|
|
203
|
+
let out = "";
|
|
204
|
+
let match;
|
|
205
|
+
while ((match = re.exec(flat)) !== null) {
|
|
206
|
+
out += `${flat.slice(cursor, match.index)}{${match[1]}}`;
|
|
207
|
+
cursor = match.index + match[0].length;
|
|
208
|
+
}
|
|
209
|
+
if (cursor !== flat.length)
|
|
210
|
+
out += flat.slice(cursor, flat.length);
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
/** The `{formula,editorText,editorMark}` object of a type-2 rule, or null. */
|
|
214
|
+
function parseStitchObject(options) {
|
|
215
|
+
const raw = typeof options.shortTextStitch === "string" ? options.shortTextStitch : typeof options.value === "string" ? options.value : undefined;
|
|
216
|
+
if (raw === undefined)
|
|
217
|
+
return null;
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(raw);
|
|
220
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Fail closed when the rich `editorText` does not rebuild the canonical
|
|
228
|
+
* `formula`. The designer shows 已失效 otherwise; the platform `create` does not
|
|
229
|
+
* check this, so it settles as a broken rule. Skipped when editorText/editorMark
|
|
230
|
+
* are absent (the engine only reads `formula`).
|
|
231
|
+
*/
|
|
232
|
+
export function assertStitchEditorTextRoundTrips(options) {
|
|
233
|
+
const parsed = parseStitchObject(options);
|
|
234
|
+
if (!parsed)
|
|
235
|
+
return;
|
|
236
|
+
const formula = typeof parsed.formula === "string" ? parsed.formula : null;
|
|
237
|
+
const editorText = typeof parsed.editorText === "string" ? parsed.editorText : undefined;
|
|
238
|
+
const editorMark = typeof parsed.editorMark === "string" && parsed.editorMark.trim() ? parsed.editorMark : undefined;
|
|
239
|
+
if (!formula || !editorText || !editorMark)
|
|
240
|
+
return;
|
|
241
|
+
// Only enforce when editorText actually carries tagged references; a plain
|
|
242
|
+
// display string without tags is left as-is (it does not round-trip to a
|
|
243
|
+
// formula by construction, and the platform tolerates it).
|
|
244
|
+
const tag = new RegExp(`<${editorMark.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^>]*>`);
|
|
245
|
+
if (!tag.test(editorText))
|
|
246
|
+
return;
|
|
247
|
+
const rebuilt = formulaFromEditorText(editorText, editorMark);
|
|
248
|
+
if (rebuilt === formula)
|
|
249
|
+
return;
|
|
250
|
+
throw new ToolRefusalError({
|
|
251
|
+
status: "refused",
|
|
252
|
+
blocker: "datarule-stitch-editor-mismatch",
|
|
253
|
+
dataRuleType: 2,
|
|
254
|
+
message: `shortTextStitch 的 editorText 与 formula 不一致:用 editorMark「${editorMark}」把 editorText 反解为 "${rebuilt}",但 formula 是 "${formula}"。` +
|
|
255
|
+
`门户公式编辑器按 editorText 重新生成 formula(见 parseFormulaFromEditorText),不一致时该计算规则显示「已失效」。` +
|
|
256
|
+
`最常见错误:把带花括号的 token 包进标签(<${editorMark}>{字段code}</${editorMark}>)——编辑器会再加一层括号变成 {{字段code}}。` +
|
|
257
|
+
`正确写法:标签内只放**裸 token**(无花括号),如 formula「{a}+{b}」对应 editorText「<${editorMark}>a</${editorMark}>+<${editorMark}>b</${editorMark}>」。`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/** Require a target field binding (singular `propertyCode` or `propertyCodes`). */
|
|
261
|
+
export function assertDataRuleTarget(args) {
|
|
262
|
+
if (dataRuleTargetCodes(args).length === 0)
|
|
263
|
+
throw new ToolRefusalError({
|
|
264
|
+
status: "refused",
|
|
265
|
+
blocker: "datarule-target-missing",
|
|
266
|
+
message: "必须提供目标字段:propertyCode(单字段)或 propertyCodes(数组);真实设计器按字段数组绑定",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* A cross-field submit condition (dataRuleType 1 / checkType 2) must reference
|
|
271
|
+
* the peer field with `{"valueType":"currentModel","value":"<fieldCode>"}`. A
|
|
272
|
+
* bare `{fieldCode}` on the right of an operator is accepted and stored, but
|
|
273
|
+
* neither the portal form's `formSubmitValidate` nor the server
|
|
274
|
+
* `SubmitCheckValidator.getConditionValue` resolves it to the field value — only
|
|
275
|
+
* `currentModel` is mapped (backend SubmitCheckValidator:424-441). The rule is
|
|
276
|
+
* therefore a silent no-op. Live 2026-09-24: `{end} 小于 {start}` did not block
|
|
277
|
+
* while the same rule with a `currentModel` right-hand value did.
|
|
278
|
+
*/
|
|
279
|
+
const SUBMIT_CROSS_FIELD_BARE_TOKEN = /\}\s*(?:等于|不等于|大于等于|小于等于|大于|小于|包含|不包含|属于|不属于|介于|不介于|==|!=|>=|<=|>|<)\s*\{\s*[A-Za-z_][A-Za-z0-9_.]*\s*\}/;
|
|
280
|
+
/** True when a string is exactly a URL-encoded form of some other text (the designer's storage quirk). */
|
|
281
|
+
function isUrlEncoded(value) {
|
|
282
|
+
if (!/%[0-9A-Fa-f]{2}/.test(value))
|
|
283
|
+
return false;
|
|
284
|
+
try {
|
|
285
|
+
const decoded = decodeURIComponent(value);
|
|
286
|
+
return decoded !== value && encodeURIComponent(decoded) === value;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Fail closed on the silent no-op cross-field submit-condition form.
|
|
294
|
+
*
|
|
295
|
+
* Also rejects a URL-ENCODED `prompt`. The form designer stores a custom prompt
|
|
296
|
+
* as `encodeURIComponent(text)` and decodes it, but it only does so for its own
|
|
297
|
+
* nested `options.verifyFormula.prompt` shape. This lane writes the top-level
|
|
298
|
+
* `options.prompt` shape, and the portal uses that value RAW (`return
|
|
299
|
+
* t.options`; portal-app 34987.2913aa1b.js) — an encoded prompt renders as
|
|
300
|
+
* `%E5%AE%8C…` in the validation popup (live equipmaint_workorder). Store plain
|
|
301
|
+
* text; `decodeURIComponent` leaves plain text unchanged, so the designer still
|
|
302
|
+
* reads it.
|
|
303
|
+
*/
|
|
304
|
+
export function assertDataRuleSubmitConditions(dataRuleType, checkType, options) {
|
|
305
|
+
if (dataRuleType !== 1 || checkType !== 2)
|
|
306
|
+
return;
|
|
307
|
+
const prompt = options.prompt;
|
|
308
|
+
if (typeof prompt === "string" && isUrlEncoded(prompt)) {
|
|
309
|
+
let decoded = prompt;
|
|
310
|
+
try {
|
|
311
|
+
decoded = decodeURIComponent(prompt);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
/* unreachable: isUrlEncoded proved decodability */
|
|
315
|
+
}
|
|
316
|
+
throw new ToolRefusalError({
|
|
317
|
+
status: "refused",
|
|
318
|
+
blocker: "datarule-submit-prompt-encoded",
|
|
319
|
+
checkType,
|
|
320
|
+
message: `checkType=2 的 prompt 被 URL 编码了("${prompt.slice(0, 40)}…")——这种顶层 options.prompt 门户**原样显示**,会把乱码弹给用户。` +
|
|
321
|
+
`请存纯文本:prompt:"${decoded}"。(编码是设计器内部 verifyFormula 形态的约定,本工具不写那种形态。)`,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const conditions = options.conditions;
|
|
325
|
+
if (typeof conditions !== "string" || !SUBMIT_CROSS_FIELD_BARE_TOKEN.test(conditions))
|
|
326
|
+
return;
|
|
327
|
+
throw new ToolRefusalError({
|
|
328
|
+
status: "refused",
|
|
329
|
+
blocker: "datarule-submit-cross-field-bare",
|
|
330
|
+
dataRuleType,
|
|
331
|
+
checkType,
|
|
332
|
+
message: "checkType=2 提交校验跨字段比较的右值不能用裸 {字段code}——平台只把 " +
|
|
333
|
+
'{"valueType":"currentModel","value":"<字段code>"} 解析成字段值,裸 token 会原样落库但规则静默不生效。' +
|
|
334
|
+
'正确写法:conditions:"{end} 小于 {\\"valueType\\":\\"currentModel\\",\\"value\\":\\"start\\"}"(字段 vs 字面量仍可直接写值,如 {qty} 大于 100)。',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
export function assertDataRuleOptions(dataRuleType, options, checkType) {
|
|
338
|
+
assertDataRuleSubmitConditions(dataRuleType, checkType, options);
|
|
339
|
+
const required = DATARULE_TYPE_OPTION_KEYS[dataRuleType] ?? [];
|
|
340
|
+
if (Object.keys(options).length === 0 || !required.some((key) => options[key] !== undefined)) {
|
|
341
|
+
throw new ToolRefusalError({
|
|
342
|
+
status: "refused",
|
|
343
|
+
blocker: "datarule-options-invalid",
|
|
344
|
+
dataRuleType,
|
|
345
|
+
message: `dataRuleType ${dataRuleType} requires at least one of ${required.join(", ")} in options (those are the keys the 8.6.26 engine reads)`,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
// Round 4 (ADR 0005): types 3/4/5 gate on the reproduced condition protocol.
|
|
349
|
+
// The platform itself does not validate these shapes at create time, so an
|
|
350
|
+
// unguarded write stores a portal-renderer landmine that only fires when a
|
|
351
|
+
// user opens the form. Verified shapes only — see datarule-protocol.ts.
|
|
352
|
+
const protocolError = dataRuleOptionsShapeError(dataRuleType, options);
|
|
353
|
+
if (protocolError !== null) {
|
|
354
|
+
const example = dataRuleType === 5 ? CONDITION_PROTOCOL_EXAMPLES.conditional : dataRuleType === 4 ? `${CONDITION_PROTOCOL_EXAMPLES.conditional}(恒必填用字面量 "true")` : CONDITION_PROTOCOL_EXAMPLES.conditional;
|
|
355
|
+
throw new ToolRefusalError({
|
|
356
|
+
status: "refused",
|
|
357
|
+
blocker: "datarule-options-invalid",
|
|
358
|
+
dataRuleType,
|
|
359
|
+
message: `${protocolError}。平台 create 不校验该形状(checkDataRuleFormat 只查计算规则/部门条件),坏形状会原样落库并在门户表单渲染时崩溃(compareType 未定义族)。` +
|
|
360
|
+
`条件串协议:{字段code} 操作符 {"valueType":...,"value":...},三段单空格;多条件用 " && ",多组用 " || "(操作符:== != > < >= <= Contains NotContains IsEmpty IsNotEmpty)。` +
|
|
361
|
+
`正确形状示例:options:{${dataRuleType === 4 ? "requiredFormula" : dataRuleType === 5 ? "readonlyCondition" : "displayFormula"}:"${example}"}`,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (dataRuleType === 2) {
|
|
365
|
+
// T-04: a top-level `expression` is NEVER read by the engine — the carrier is
|
|
366
|
+
// the nested {formula,editorText,editorMark} JSON. Refuse it only when it is
|
|
367
|
+
// the SOLE carrier (a caller who thinks it holds the expression). The platform
|
|
368
|
+
// itself stores a harmless top-level `expression` mirror on some rules (e.g.
|
|
369
|
+
// formula-field type-2 rows), so a legit round-trip that also carries
|
|
370
|
+
// shortTextStitch/value/aggregation must be accepted (live bench40_order_item).
|
|
371
|
+
const hasCarrier = options.shortTextStitch !== undefined || options.value !== undefined || options.aggregation !== undefined;
|
|
372
|
+
if (options.expression !== undefined && !hasCarrier) {
|
|
373
|
+
throw new ToolRefusalError({
|
|
374
|
+
status: "refused",
|
|
375
|
+
blocker: "datarule-options-invalid",
|
|
376
|
+
dataRuleType,
|
|
377
|
+
message: 'dataRuleType 2 顶层没有 `expression` 键——引擎读的是 `shortTextStitch`(或 `value`),其值必须是含 `formula` 的 JSON 对象字符串;`expression` 只是该嵌套 JSON 内部的字段名。正确形状:options:{shortTextStitch:"{\\"formula\\":\\"{字段}\\",\\"editorText\\":\\"{字段名}\\",\\"editorMark\\":\\"{字段名}\\"}"}',
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
const formulaType = typeof options.formulaType === "string" ? options.formulaType : "";
|
|
381
|
+
const error = formulaType === "AGGREGATION"
|
|
382
|
+
? calcExpressionShapeError("aggregation", options.aggregation)
|
|
383
|
+
: calcExpressionShapeError(options.shortTextStitch !== undefined ? "shortTextStitch" : "value", options.shortTextStitch ?? options.value);
|
|
384
|
+
if (error) {
|
|
385
|
+
throw new ToolRefusalError({
|
|
386
|
+
status: "refused",
|
|
387
|
+
blocker: "datarule-options-invalid",
|
|
388
|
+
dataRuleType,
|
|
389
|
+
message: `dataRuleType 2 options 形状无效:${error}。裸串不是合法 JSON → 引擎 getCalRuleExpression 返回 null → 死循环校验误报 305003。` +
|
|
390
|
+
'正确形状:options:{shortTextStitch:"{\\"formula\\":\\"{字段code}\\",\\"editorText\\":\\"{字段名}\\",\\"editorMark\\":\\"{字段名}\\"}"}(editorText/editorMark 仅展示用;formula 是表达式)',
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
// editorText ↔ formula round-trip (display correctness). Skipped for the
|
|
394
|
+
// AGGREGATION carrier (its display object differs).
|
|
395
|
+
if (formulaType !== "AGGREGATION")
|
|
396
|
+
assertStitchEditorTextRoundTrips(options);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** Extract the expression of a type-2 CALCULATE rule (mirrors calcExpressionShapeError's carrier choice). */
|
|
400
|
+
function type2FormulaText(options) {
|
|
401
|
+
const raw = typeof options.shortTextStitch === "string" ? options.shortTextStitch : typeof options.value === "string" ? options.value : undefined;
|
|
402
|
+
if (raw === undefined)
|
|
403
|
+
return null;
|
|
404
|
+
try {
|
|
405
|
+
const parsed = JSON.parse(raw);
|
|
406
|
+
return typeof parsed.formula === "string" ? parsed.formula : null;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Bare references to sibling child fields in a CHILD-table formula.
|
|
414
|
+
*
|
|
415
|
+
* Evidence (round 3, HANDOFF-round5 §3): a formula token's meaning is
|
|
416
|
+
* positional — bare `{field}` resolves against the MAIN record, dotted
|
|
417
|
+
* `{childSchema.field}` against the child. So a CHILD-table formula
|
|
418
|
+
* (dataRuleType 2, childRule=true) that references a sibling child field with a
|
|
419
|
+
* BARE token reads a missing main field and the submit answers errcode 50000
|
|
420
|
+
* (bench40: `{partPrice}*{partQty}` on the child table; the corrected
|
|
421
|
+
* `{partUsages.partPrice}` lands). Field-aware, so only a bare token that
|
|
422
|
+
* actually exists on the (child) schema is flagged — a legitimate bare main
|
|
423
|
+
* field reference is left alone.
|
|
424
|
+
*/
|
|
425
|
+
function childFormulaBareReferences(formula, childFieldCodes) {
|
|
426
|
+
const hits = new Set();
|
|
427
|
+
for (const match of formula.matchAll(/\{([^{}]+)\}/g)) {
|
|
428
|
+
const token = match[1].trim();
|
|
429
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(token))
|
|
430
|
+
continue; // bare only; the dotted form is the correct one
|
|
431
|
+
if (childFieldCodes.has(token))
|
|
432
|
+
hits.add(token);
|
|
433
|
+
}
|
|
434
|
+
return [...hits];
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Fail closed on a child-table formula that bare-references a sibling child
|
|
438
|
+
* field (→ platform 50000 on submit). Field-aware via the child schema's
|
|
439
|
+
* published fields; a caller-provided set avoids a duplicate fieldList read.
|
|
440
|
+
*/
|
|
441
|
+
async function assertChildFormulaRefs(session, args, knownChildFields) {
|
|
442
|
+
if (args.dataRuleType !== 2 || args.childRule !== true)
|
|
443
|
+
return;
|
|
444
|
+
const formula = type2FormulaText(args.options);
|
|
445
|
+
if (formula === null)
|
|
446
|
+
return;
|
|
447
|
+
const codes = knownChildFields ??
|
|
448
|
+
new Set(wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data).map((row) => String(row.code ?? row.propertyCode ?? "")));
|
|
449
|
+
const bare = childFormulaBareReferences(formula, codes);
|
|
450
|
+
if (bare.length === 0)
|
|
451
|
+
return;
|
|
452
|
+
throw new ToolRefusalError({
|
|
453
|
+
status: "refused",
|
|
454
|
+
blocker: "datarule-formula-child-bare-reference",
|
|
455
|
+
schemaCode: args.schemaCode,
|
|
456
|
+
propertyCodes: bare,
|
|
457
|
+
message: `子表公式里 ${bare.map((token) => `{${token}}`).join("、")} 是子表字段的**裸引用**——平台按主记录字段解析(值缺失 → 提交 50000)。` +
|
|
458
|
+
`子表公式引用同级子字段必须带点:${bare.map((token) => `{${args.schemaCode}.${token}}`).join("、")}` +
|
|
459
|
+
`(裸 {字段}=主记录字段,带点 {子表code.字段}=子表字段)。`,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
/** Ensure the wire `options` carries `propertyCodes` (comma-joined), as the designer writes it. */
|
|
463
|
+
function withOptionsPropertyCodes(options, codes) {
|
|
464
|
+
if (codes.length === 0)
|
|
465
|
+
return options;
|
|
466
|
+
if (options.propertyCodes !== undefined)
|
|
467
|
+
return options;
|
|
468
|
+
return { propertyCodes: codes.join(","), ...options };
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Auto-fill the type-2 `editorText`/`editorMark` from `formula` when the caller
|
|
472
|
+
* supplied only a formula in `shortTextStitch`. The designer needs the rich
|
|
473
|
+
* `editorText` (else 标红); since the frontend derives the formula back from it
|
|
474
|
+
* (`parseFormulaFromEditorText`: tag content → `{content}`), OMC builds it the
|
|
475
|
+
* one correct way — each `{token}` wrapped as `<field-omc0001>token</field-omc0001>`
|
|
476
|
+
* (bare token, NO braces). A caller-supplied editorText is left untouched and
|
|
477
|
+
* then checked by `assertStitchEditorTextRoundTrips`.
|
|
478
|
+
*/
|
|
479
|
+
export function withStitchEditorText(options) {
|
|
480
|
+
const key = options.shortTextStitch !== undefined ? "shortTextStitch" : options.value !== undefined ? "value" : undefined;
|
|
481
|
+
if (key === undefined)
|
|
482
|
+
return options;
|
|
483
|
+
const raw = options[key];
|
|
484
|
+
if (typeof raw !== "string")
|
|
485
|
+
return options;
|
|
486
|
+
let parsed;
|
|
487
|
+
try {
|
|
488
|
+
parsed = JSON.parse(raw);
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
return options;
|
|
492
|
+
}
|
|
493
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
494
|
+
return options;
|
|
495
|
+
const obj = parsed;
|
|
496
|
+
if (typeof obj.formula !== "string" || !obj.formula.trim())
|
|
497
|
+
return options;
|
|
498
|
+
const hasEditorText = typeof obj.editorText === "string" && obj.editorText.trim().length > 0;
|
|
499
|
+
const hasMark = typeof obj.editorMark === "string" && obj.editorMark.trim().length > 0;
|
|
500
|
+
if (hasEditorText && hasMark)
|
|
501
|
+
return options;
|
|
502
|
+
const mark = hasMark ? String(obj.editorMark) : "field-omc0001";
|
|
503
|
+
const editorText = obj.formula.replace(/\{([^{}]+)\}/g, (_full, token) => `<${mark}>${token}</${mark}>`);
|
|
504
|
+
return { ...options, [key]: JSON.stringify({ ...obj, editorText, editorMark: mark }) };
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Live 2026-09-23: bizdatarule type 3/4/5 do NOT drive the portal form
|
|
508
|
+
* interaction layer. Reproduced on bench22 + a fresh probe: a type-4 required
|
|
509
|
+
* rule (Chinese OR symbol formula) never made the control required in the real
|
|
510
|
+
* portal, while the SAME condition in the field's `options.requiredFormula`
|
|
511
|
+
* (via field.update) did — and a SYMBOL type-4 rule additionally crashed the
|
|
512
|
+
* renderer through its `historyOpt` mirror. So form-interaction
|
|
513
|
+
* required/readonly/display belongs in field options; this lane is the
|
|
514
|
+
* field-level engine read elsewhere. The advisory makes the write-time
|
|
515
|
+
* consequence explicit so the caller stops reaching for datarule for form UI.
|
|
516
|
+
*
|
|
517
|
+
* Corrected 2026-09-23 (see evidence/datarule-designer-visibility-20260923.md):
|
|
518
|
+
* the row written here IS what the FORM DESIGNER's design-time Set/Unset reads
|
|
519
|
+
* — `20156.9cbebcd2.js:51036/111025` `isRequire` matches `(propertyCodes||
|
|
520
|
+
* [propertyCode]).includes(<field>)` over the `bizdatarule` list. A clean
|
|
521
|
+
* isolation (CLI `datarule.create` type-4 on an otherwise-untouched field, then
|
|
522
|
+
* a HARD page reload) shows the designer attribute panel flips to "Set". The
|
|
523
|
+
* earlier "设计器看不到" symptom was a stale SPA hash-route session: navigating
|
|
524
|
+
* `page.goto` to the SAME `#/...form-design/...` URL does not reload the SPA, so
|
|
525
|
+
* the Vuex rule list keeps the previous in-memory state. So:
|
|
526
|
+
* - design-time visibility → this row (hard-reload to observe);
|
|
527
|
+
* - portal runtime required → field `options.*` (field.update + configure_form).
|
|
528
|
+
* Two engines, two storage locations; neither substitutes for the other.
|
|
529
|
+
*/
|
|
530
|
+
const DATARULE_FORM_INTERACTION_ADVISORY = "bizdatarule type3/4/5 行是**表单设计器设计态** Set/Unset 的读面(设计器加载 bizdatarule/list);门户**运行时**则读**字段 options**。所以表单条件优先走 `field.update`:写 requiredFormula/readonlyCondition/displayFormula 时它会**自动配对**本行,readback.formConditionSync 报 created/updated/deleted/skipped,清空条件传空串会同时删行。本工具用于字段级引擎规则,或独立增删改既有行。设计器是 SPA——写入后须整页硬重载才观察得到(`goto` 同一 hash URL 不会重载)。符号式经 historyOpt 镜像在门户渲染会崩,运行时条件请用中文操作符。";
|
|
531
|
+
function dataRuleBody(args, options) {
|
|
532
|
+
const codes = dataRuleTargetCodes(args);
|
|
533
|
+
return {
|
|
534
|
+
schemaCode: args.schemaCode,
|
|
535
|
+
// Single field also fills the legacy column; multiple fields leave it null,
|
|
536
|
+
// matching the designer (propertyCodes is the authoritative binding).
|
|
537
|
+
propertyCode: codes.length === 1 ? codes[0] : null,
|
|
538
|
+
propertyCodes: codes,
|
|
539
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
540
|
+
dataRuleType: args.dataRuleType,
|
|
541
|
+
options,
|
|
542
|
+
...(args.checkType !== undefined ? { checkType: args.checkType } : {}),
|
|
543
|
+
...(args.childRule !== undefined ? { childRule: args.childRule } : {}),
|
|
544
|
+
...(args.sortKey !== undefined ? { sortKey: args.sortKey } : {}),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
const DATA_RULE_ROW_FIELDS = ["id", "schemaCode", "propertyCode", "propertyCodes", "dataRuleType", "options", "checkType", "childRule", "name", "sortKey"];
|
|
548
|
+
function fullDataRuleBody(current, args, options) {
|
|
549
|
+
const patch = dataRuleBody(args, options);
|
|
550
|
+
return Object.fromEntries(DATA_RULE_ROW_FIELDS.map((field) => [field, field === "options" ? options : field in patch ? patch[field] : current[field] ?? null]));
|
|
551
|
+
}
|
|
552
|
+
function dataRuleChanges(before, after) {
|
|
553
|
+
const changedFields = [];
|
|
554
|
+
for (const field of DATA_RULE_ROW_FIELDS)
|
|
555
|
+
if (JSON.stringify(before[field] ?? null) !== JSON.stringify(after[field] ?? null))
|
|
556
|
+
changedFields.push(field);
|
|
557
|
+
return { changedFields, unexpectedChanges: changedFields.filter((field) => !["options", "checkType", "childRule", "name", "sortKey", "propertyCode", "propertyCodes", "dataRuleType"].includes(field)) };
|
|
558
|
+
}
|
|
559
|
+
async function listDataRules(session, schemaCode, dataRuleType) {
|
|
560
|
+
const query = { schemaCode };
|
|
561
|
+
if (dataRuleType !== undefined)
|
|
562
|
+
query.dataRuleType = String(dataRuleType);
|
|
563
|
+
return wireRows((await wireCall(session, { path: ENDPOINTS.dataRuleList, query })).data);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Identity seam: resolve a data-rule row from either its physical `id` or the
|
|
567
|
+
* business locator `(propertyCode, dataRuleType)`. The latter removes the forced
|
|
568
|
+
* `datarule.list → id → datarule.update/delete` dance for the common "change this
|
|
569
|
+
* field's required/readonly rule" case.
|
|
570
|
+
*/
|
|
571
|
+
async function locateDataRuleId(session, locator, verb) {
|
|
572
|
+
if (locator.id)
|
|
573
|
+
return locator.id;
|
|
574
|
+
const result = await resolveDataRuleRow(session, locator.schemaCode, locator);
|
|
575
|
+
if (!result.ok) {
|
|
576
|
+
const where = [locator.propertyCode, ...(locator.propertyCodes ?? [])].filter(Boolean).join(",") || "(未给字段)";
|
|
577
|
+
throw new ToolRefusalError({
|
|
578
|
+
status: "refused",
|
|
579
|
+
blocker: result.reason === "ambiguous" ? "datarule-locator-ambiguous" : "datarule-absent",
|
|
580
|
+
message: result.reason === "ambiguous"
|
|
581
|
+
? `字段 ${locator.schemaCode}.${where} 有 ${result.count} 条数据规则,业务键不唯一;请补 dataRuleType 或直接传 id,拒绝 ${verb}`
|
|
582
|
+
: `按业务键在 ${locator.schemaCode} 找不到数据规则(propertyCode=${where}, dataRuleType=${locator.dataRuleType ?? "任意"});可用 datarule.list 核对,拒绝 ${verb}`,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return String(result.row.id);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Form-interaction option key → the `bizdatarule` dataRuleType that carries the
|
|
589
|
+
* SAME condition for the form DESIGNER. `readonlyFormula` (boolean, 恒只读) has
|
|
590
|
+
* no rule-row counterpart, so it is deliberately absent.
|
|
591
|
+
*/
|
|
592
|
+
const FORM_CONDITION_RULE_TYPES = Object.freeze({
|
|
593
|
+
displayFormula: 3,
|
|
594
|
+
requiredFormula: 4,
|
|
595
|
+
readonlyCondition: 5,
|
|
596
|
+
});
|
|
597
|
+
/**
|
|
598
|
+
* Pair a field's form-interaction condition into the `bizdatarule` projection.
|
|
599
|
+
*
|
|
600
|
+
* Evidence (2026-09-23, ADR 0005):
|
|
601
|
+
* - the form DESIGNER's design-time Set/Unset reads `bizdatarule/list` rows
|
|
602
|
+
* for dataRuleType 3/4/5 — network capture shows the designer loads
|
|
603
|
+
* `bizdatarule/list?schemaCode=`, while `bizproperty/list` carries NO
|
|
604
|
+
* model-level rule rows (see evidence/datarule-designer-visibility-20260923.md
|
|
605
|
+
* §4b). Frontend `isRequire` matches `(propertyCodes||[propertyCode])`.
|
|
606
|
+
* - the portal RUNTIME reads the field's `options.{requiredFormula|
|
|
607
|
+
* readonlyCondition|displayFormula}`.
|
|
608
|
+
* A condition is correct only when BOTH faces exist; `field.update` previously
|
|
609
|
+
* wrote only the options face, so CLI-authored conditions showed "Unset" in the
|
|
610
|
+
* designer. This helper mirrors the designer's own row shape exactly:
|
|
611
|
+
* `propertyCodes:[field]`, `options:{propertyCodes, functionType:"true", <key>}`.
|
|
612
|
+
*
|
|
613
|
+
* Only keys whose string value changed are touched; clearing a key deletes the
|
|
614
|
+
* matching row. Writes are full-row so no persisted column is dropped.
|
|
615
|
+
*/
|
|
616
|
+
export async function syncFormConditionDataRules(session, schemaCode, fieldCode, prevOptions, nextOptions) {
|
|
617
|
+
const result = { created: [], updated: [], deleted: [], skipped: [] };
|
|
618
|
+
const text = (options, key) => (typeof options[key] === "string" ? options[key].trim() : "");
|
|
619
|
+
const changedKeys = Object.keys(FORM_CONDITION_RULE_TYPES).filter((key) => text(prevOptions, key) !== text(nextOptions, key));
|
|
620
|
+
if (changedKeys.length === 0)
|
|
621
|
+
return result;
|
|
622
|
+
const existing = await listDataRules(session, schemaCode);
|
|
623
|
+
// The designer's rule rows live on the PUBLISHED face (like datarule.create's
|
|
624
|
+
// own guard); a draft-only field cannot carry a designer-visible row yet.
|
|
625
|
+
const published = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data)
|
|
626
|
+
.some((row) => String(row.code ?? row.propertyCode ?? "") === fieldCode);
|
|
627
|
+
for (const key of changedKeys) {
|
|
628
|
+
const dataRuleType = FORM_CONDITION_RULE_TYPES[key];
|
|
629
|
+
const next = text(nextOptions, key);
|
|
630
|
+
const current = existing.find((row) => Number(row.dataRuleType) === dataRuleType && storedTargetCodes(row).includes(fieldCode));
|
|
631
|
+
if (next.length === 0) {
|
|
632
|
+
if (current && typeof current.id === "string" && current.id.length > 0) {
|
|
633
|
+
await wireCall(session, { path: ENDPOINTS.dataRuleDelete, method: "POST", query: { id: current.id } });
|
|
634
|
+
result.deleted.push({ fieldCode, dataRuleType, key, id: current.id });
|
|
635
|
+
}
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (!published) {
|
|
639
|
+
result.skipped.push({ key, dataRuleType, reason: `字段 ${fieldCode} 未发布,无法在其上建设计器可见规则行;先 field.publish 再改条件` });
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const options = JSON.stringify({ propertyCodes: fieldCode, functionType: "true", [key]: next });
|
|
643
|
+
if (current && typeof current.id === "string" && current.id.length > 0) {
|
|
644
|
+
const body = Object.fromEntries(DATA_RULE_ROW_FIELDS.map((field) => [field, field === "options" ? options : field === "schemaCode" ? schemaCode : field === "propertyCode" ? fieldCode : field === "propertyCodes" ? [fieldCode] : field === "dataRuleType" ? dataRuleType : current[field] ?? null]));
|
|
645
|
+
await wireCall(session, { path: ENDPOINTS.dataRuleUpdate, method: "POST", body });
|
|
646
|
+
result.updated.push({ fieldCode, dataRuleType, key, id: current.id });
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
await wireCall(session, { path: ENDPOINTS.dataRuleCreate, method: "POST", body: { schemaCode, propertyCode: fieldCode, propertyCodes: [fieldCode], dataRuleType, options } });
|
|
650
|
+
result.created.push({ fieldCode, dataRuleType, key, id: null });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return result;
|
|
654
|
+
}
|
|
655
|
+
export function registerDataRuleLane(tools) {
|
|
656
|
+
tools.push(defineTool({
|
|
657
|
+
name: "datarule.list",
|
|
658
|
+
layer: "low",
|
|
659
|
+
kind: "read",
|
|
660
|
+
lane: DATARULE_LANE,
|
|
661
|
+
scope: DATARULE_TOOLS["datarule.list"].scope,
|
|
662
|
+
description: DATARULE_TOOLS["datarule.list"].description,
|
|
663
|
+
inputSchema: {
|
|
664
|
+
environment: environmentArg,
|
|
665
|
+
schemaCode: z.string().min(1),
|
|
666
|
+
dataRuleType: dataRuleTypeArg.optional(),
|
|
667
|
+
capability: capabilityArg,
|
|
668
|
+
},
|
|
669
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
670
|
+
const rows = await listDataRules(session, args.schemaCode, args.dataRuleType);
|
|
671
|
+
return {
|
|
672
|
+
schemaCode: args.schemaCode,
|
|
673
|
+
dataRuleType: args.dataRuleType ?? null,
|
|
674
|
+
rules: rows.map(decodeDataRule),
|
|
675
|
+
};
|
|
676
|
+
}),
|
|
677
|
+
}));
|
|
678
|
+
writeTool(tools, "datarule.create", DATARULE_LANE, DATARULE_TOOLS["datarule.create"].scope, DATARULE_TOOLS["datarule.create"].description, {
|
|
679
|
+
schemaCode: z.string().min(1),
|
|
680
|
+
propertyCode: propertyCodeArg,
|
|
681
|
+
propertyCodes: propertyCodesArg,
|
|
682
|
+
dataRuleType: dataRuleTypeArg,
|
|
683
|
+
options: dataRuleOptionsArg,
|
|
684
|
+
checkType: checkTypeArg,
|
|
685
|
+
childRule: z.boolean().optional(),
|
|
686
|
+
name: z.string().optional(),
|
|
687
|
+
sortKey: z.number().int().optional(),
|
|
688
|
+
}, async (session, args) => {
|
|
689
|
+
assertDataRuleOptions(args.dataRuleType, args.options, args.checkType);
|
|
690
|
+
assertDataRuleCheckType(args.dataRuleType, args.checkType);
|
|
691
|
+
assertDataRuleTarget(args);
|
|
692
|
+
const codes = dataRuleTargetCodes(args);
|
|
693
|
+
const fields = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
|
|
694
|
+
const published = new Set(fields.map((row) => String(row.code ?? row.propertyCode ?? "")));
|
|
695
|
+
await assertChildFormulaRefs(session, args, published);
|
|
696
|
+
const missing = codes.filter((code) => !published.has(code));
|
|
697
|
+
if (missing.length > 0)
|
|
698
|
+
throw new ToolRefusalError({ status: "refused", blocker: "datarule-property-not-published", schemaCode: args.schemaCode, propertyCodes: codes, missing, message: `字段 ${args.schemaCode}.${missing.join(",")} 不在发布面上;拒绝 datarule.create POST` });
|
|
699
|
+
const existingRules = await listDataRules(session, args.schemaCode);
|
|
700
|
+
// dataRuleType 1 (CHECK_RULE) fans out into 5 checkTypes that COEXIST on one
|
|
701
|
+
// field (the platform auto-creates a checkType 4 maxLength row per text
|
|
702
|
+
// field), so the clash key there is field + `checkType`, not field + type.
|
|
703
|
+
if (existingRules.some((row) => {
|
|
704
|
+
if (Number(row.dataRuleType) !== args.dataRuleType)
|
|
705
|
+
return false;
|
|
706
|
+
if (!storedTargetCodes(row).some((code) => codes.includes(code)))
|
|
707
|
+
return false;
|
|
708
|
+
return args.dataRuleType === 1 ? (row.checkType ?? null) === (args.checkType ?? null) : true;
|
|
709
|
+
}))
|
|
710
|
+
throw new ToolRefusalError({ status: "refused", blocker: "datarule-duplicate-property-type", schemaCode: args.schemaCode, propertyCodes: codes, dataRuleType: args.dataRuleType, ...(args.dataRuleType === 1 ? { checkType: args.checkType } : {}), message: `字段 ${args.schemaCode}.${codes.join(",")} 已存在同类型数据规则${args.dataRuleType === 1 ? `(checkType ${args.checkType})` : ""};拒绝重复 POST` });
|
|
711
|
+
const createRule = () => wireCall(session, {
|
|
712
|
+
path: ENDPOINTS.dataRuleCreate,
|
|
713
|
+
method: "POST",
|
|
714
|
+
body: dataRuleBody(args, JSON.stringify(withOptionsPropertyCodes(withStitchEditorText(args.options), codes))),
|
|
715
|
+
});
|
|
716
|
+
// Ticket 78: ONLY type2 CALCULATE has the corroborated unpublished-app
|
|
717
|
+
// FALSE dead-cycle 305003 (checkCalculateRulesDeadCycle mis-concatenates
|
|
718
|
+
// schemaCode.propertyCode when getAppFunctionByCode is null); types
|
|
719
|
+
// 3/4/5 keep the plain wire failure.
|
|
720
|
+
const performed = args.dataRuleType === 2
|
|
721
|
+
? await runtimeWriteWithAppMapping(session, args.schemaCode, 305003, "datarule.create 类型2 CALCULATE 报死循环(305003,误判)", createRule)
|
|
722
|
+
: await createRule();
|
|
723
|
+
const rules = (await listDataRules(session, args.schemaCode, args.dataRuleType)).filter((row) => storedTargetCodes(row).some((code) => codes.includes(code)));
|
|
724
|
+
return {
|
|
725
|
+
performed: { errcode: performed.errcode },
|
|
726
|
+
readback: {
|
|
727
|
+
schemaCode: args.schemaCode,
|
|
728
|
+
propertyCodes: codes,
|
|
729
|
+
dataRuleType: args.dataRuleType,
|
|
730
|
+
matchedCount: rules.length,
|
|
731
|
+
rules: rules.map(decodeDataRule),
|
|
732
|
+
...(args.dataRuleType >= 3 ? { advisory: DATARULE_FORM_INTERACTION_ADVISORY } : {}),
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
},
|
|
736
|
+
// T-04: the type-2 shape gate is static — refuse BEFORE the write permission gate.
|
|
737
|
+
(args) => {
|
|
738
|
+
assertDataRuleTarget(args);
|
|
739
|
+
assertDataRuleOptions(args.dataRuleType, args.options, args.checkType);
|
|
740
|
+
assertDataRuleCheckType(args.dataRuleType, args.checkType);
|
|
741
|
+
});
|
|
742
|
+
writeTool(tools, "datarule.update", DATARULE_LANE, DATARULE_TOOLS["datarule.update"].scope, DATARULE_TOOLS["datarule.update"].description, {
|
|
743
|
+
id: z.string().min(1).optional().describe("数据规则记录 id;省略时可改用业务键 schemaCode + propertyCode(+dataRuleType) 定位"),
|
|
744
|
+
schemaCode: z.string().min(1),
|
|
745
|
+
propertyCode: propertyCodeArg,
|
|
746
|
+
propertyCodes: propertyCodesArg,
|
|
747
|
+
dataRuleType: dataRuleTypeArg,
|
|
748
|
+
options: dataRuleOptionsArg,
|
|
749
|
+
checkType: checkTypeArg,
|
|
750
|
+
childRule: z.boolean().optional(),
|
|
751
|
+
name: z.string().optional(),
|
|
752
|
+
sortKey: z.number().int().optional(),
|
|
753
|
+
}, async (session, args) => {
|
|
754
|
+
assertDataRuleOptions(args.dataRuleType, args.options, args.checkType);
|
|
755
|
+
assertDataRuleCheckType(args.dataRuleType, args.checkType);
|
|
756
|
+
const id = await locateDataRuleId(session, args, "update POST");
|
|
757
|
+
const before = await listDataRules(session, args.schemaCode);
|
|
758
|
+
const current = before.find((row) => row.id === id);
|
|
759
|
+
if (!current)
|
|
760
|
+
throw new ToolRefusalError({ status: "refused", blocker: "datarule-absent", message: `数据规则 ${id} 不存在于 ${args.schemaCode};拒绝 update POST` });
|
|
761
|
+
// The stored childRule governs when the caller omits it (full-row update).
|
|
762
|
+
const patchArgs = { ...args, id };
|
|
763
|
+
await assertChildFormulaRefs(session, { ...patchArgs, childRule: args.childRule ?? current.childRule === true });
|
|
764
|
+
// Keep the existing binding when the caller omits a target (the platform
|
|
765
|
+
// update is a full-row overwrite — an empty patch would clear propertyCodes).
|
|
766
|
+
const requested = dataRuleTargetCodes(patchArgs);
|
|
767
|
+
const codes = requested.length > 0 ? requested : storedTargetCodes(current);
|
|
768
|
+
const boundArgs = { ...patchArgs, propertyCode: codes.length === 1 ? codes[0] : undefined, propertyCodes: codes };
|
|
769
|
+
if (before.some((row) => {
|
|
770
|
+
if (row.id === id)
|
|
771
|
+
return false;
|
|
772
|
+
if (Number(row.dataRuleType) !== args.dataRuleType)
|
|
773
|
+
return false;
|
|
774
|
+
if (!storedTargetCodes(row).some((code) => codes.includes(code)))
|
|
775
|
+
return false;
|
|
776
|
+
return args.dataRuleType === 1 ? (row.checkType ?? null) === (args.checkType ?? null) : true;
|
|
777
|
+
}))
|
|
778
|
+
throw new ToolRefusalError({ status: "refused", blocker: "datarule-duplicate-property-type", message: `字段 ${args.schemaCode}.${codes.join(",")} 已存在同类型数据规则${args.dataRuleType === 1 ? `(checkType ${args.checkType})` : ""};拒绝 update POST` });
|
|
779
|
+
const beforeDecoded = decodeDataRule(current);
|
|
780
|
+
const performed = await wireCall(session, {
|
|
781
|
+
path: ENDPOINTS.dataRuleUpdate,
|
|
782
|
+
method: "POST",
|
|
783
|
+
body: fullDataRuleBody(current, boundArgs, JSON.stringify(withOptionsPropertyCodes(withStitchEditorText(args.options), codes))),
|
|
784
|
+
});
|
|
785
|
+
const record = (await listDataRules(session, args.schemaCode, args.dataRuleType)).find((row) => row.id === id);
|
|
786
|
+
if (!record)
|
|
787
|
+
throw new WireError("business", `data rule ${id} disappeared after update`, 304001);
|
|
788
|
+
const afterDecoded = decodeDataRule(record);
|
|
789
|
+
const proof = dataRuleChanges(beforeDecoded, afterDecoded);
|
|
790
|
+
return {
|
|
791
|
+
performed: { errcode: performed.errcode },
|
|
792
|
+
readback: { id, present: true, before: beforeDecoded, after: afterDecoded, changedFields: proof.changedFields, unexpectedChanges: proof.unexpectedChanges, rule: afterDecoded },
|
|
793
|
+
};
|
|
794
|
+
}, (args) => {
|
|
795
|
+
assertDataRuleOptions(args.dataRuleType, args.options, args.checkType);
|
|
796
|
+
});
|
|
797
|
+
writeTool(tools, "datarule.delete", DATARULE_LANE, DATARULE_TOOLS["datarule.delete"].scope, DATARULE_TOOLS["datarule.delete"].description, {
|
|
798
|
+
id: z.string().min(1).optional().describe("数据规则记录 id;省略时可改用业务键 schemaCode + propertyCode(+dataRuleType) 定位"),
|
|
799
|
+
schemaCode: z.string().min(1),
|
|
800
|
+
propertyCode: propertyCodeArg,
|
|
801
|
+
propertyCodes: propertyCodesArg,
|
|
802
|
+
dataRuleType: z.number().int().min(1).max(5).optional().describe("业务键定位去歧义:同一字段可能有多条规则(如必填+只读),用它指定要删的类型"),
|
|
803
|
+
}, async (session, args) => {
|
|
804
|
+
const id = await locateDataRuleId(session, args, "delete POST");
|
|
805
|
+
const before = (await listDataRules(session, args.schemaCode)).find((row) => row.id === id);
|
|
806
|
+
const performed = await wireCall(session, {
|
|
807
|
+
path: ENDPOINTS.dataRuleDelete,
|
|
808
|
+
method: "POST",
|
|
809
|
+
query: { id },
|
|
810
|
+
});
|
|
811
|
+
const after = await listDataRules(session, args.schemaCode);
|
|
812
|
+
return {
|
|
813
|
+
performed: { errcode: performed.errcode },
|
|
814
|
+
readback: { id, existedBefore: before !== undefined, absent: !after.some((row) => row.id === id) },
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
//# sourceMappingURL=datarule.js.map
|