omc-core 0.4.37 → 0.4.39

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.
@@ -0,0 +1,154 @@
1
+ export const CAPABILITY_CONTROL_TYPES = Object.freeze({
2
+ 0: 1,
3
+ 1: 2,
4
+ 2: 4,
5
+ 3: 3,
6
+ 4: 8,
7
+ 5: 62,
8
+ 6: 9,
9
+ 8: 201,
10
+ 9: 80,
11
+ 10: 14,
12
+ 11: 81,
13
+ 12: 5,
14
+ 13: 6,
15
+ 14: 7,
16
+ 15: 19,
17
+ 16: 82,
18
+ 17: 17,
19
+ 28: 28,
20
+ 50: 50,
21
+ 51: 51,
22
+ 60: 60,
23
+ 61: 61,
24
+ 100: 89,
25
+ });
26
+ const commonEvidence = [
27
+ "8.6.26 BizPropertyType/BizFormPropertyType",
28
+ "desensitized-config-shapes.md §3.2",
29
+ "frontend-dist/admin/js/4280.*.js designer bundle",
30
+ ];
31
+ const shape = (propertyType, field, control, evidence = commonEvidence) => ({ propertyType, controlType: CAPABILITY_CONTROL_TYPES[propertyType], field, control, evidence });
32
+ const face = (requiredKeys, defaults, evidence = commonEvidence, nonEmptyKeys) => ({ requiredKeys, defaults, evidence, ...(nonEmptyKeys ? { nonEmptyKeys } : {}) });
33
+ export const FIELD_CAPABILITY_SHAPES = Object.freeze({
34
+ 0: shape(0, face([], { dictionaryData: "" }), face(["defaultValue", "maxLength"], { defaultValue: null, placeholder: "请输入", maxLength: 200, currentMaxLength: 2000000, noRepeat: false, enableEncrypt: false })),
35
+ 1: shape(1, face([], { dictionaryData: "" }), face(["defaultValue", "maxLength"], { defaultValue: null, textAreaType: "longText", maxLength: 2000, placeholder: "请输入" })),
36
+ 2: shape(2, face([], { dictionaryData: "" }), face(["defaultValue", "format"], { defaultValue: null, placeholder: "请输入", computeFormula: "", format1: "decimal", format: "decimal", verifyFormula: "", isFlag: false })),
37
+ 3: shape(3, face(["format"], { dictionaryData: "", format: "YYYY-MM-DD" }), face(["format", "format1"], { defaultValue: null, dataItemDefault: "", format1: "YYYY-MM-DD", format: "YYYY-MM-DD", minDate: "", maxDate: "", verifyFormula: "" })),
38
+ 4: shape(4, face(["defaultValue"], { defaultValue: false }), face(["defaultValue"], { defaultValue: true })),
39
+ 6: shape(6, face([], {}), face(["displayTemplate", "limitNumber", "fileNumber", "batch"], { displayTemplate: true, limitNumber: "unlimited", fileNumber: 1, batch: true, displayFormula: "", requiredFormula: "", readonlyFormula: false, limit: "200M", onUpload: "", onDelete: "" }), ["cp-sources/frontend-dist/admin/js/12404.1675d362.js attachment schema", "cp-sources/frontend-dist/admin/js/24187.6e75bd30.js uploadOptions/baseUploadOptions"]),
40
+ // CHILD_TABLE(8) — scalar child table. Lifted from blocked on 2026-09-18:
41
+ // the wire shape is live-accepted (omc-core `childtable-field-contract.ts`,
42
+ // live record `childtable-execute-20260908070635135.md`, 2026-09-08) and the
43
+ // Sheet201 form control is live-proven (omc-core
44
+ // `childtable-sheet201-contract.ts`). The field row carries no `options`; its
45
+ // definition is the `subSchema` ({code:<parent field code>, properties:[...]})
46
+ // and the form control is Sheet(201) with `columns`. Nested CHILD_TABLE (a
47
+ // child of a child) remains out of coverage and is refused in
48
+ // `assertFieldFamilyAllowed`.
49
+ 8: shape(8, face([], {}), face([], {}), ["omc-core childtable-field-contract.ts (live 2026-09-08)", "omc-core childtable-sheet201-contract.ts", "8.6.26 BizPropertyType CHILD_TABLE=8 / BizFormPropertyType Sheet=201"]),
50
+ 9: shape(9, face([], { dictionaryData: "" }), face([], { schemaCode: "", displayField: "name", relativePropertyMatchCode: "name", queryCode: "", linkMode: true, showStyle: "", selectMode: "popup", isAuthorize: true, treeView: false, treeStartFromRoot: false, mappings: "", searchFormula: "", conditions: "" })),
51
+ 10: shape(10, face(["defaultValue"], { defaultValue: "" }), face(["locationType", "addressDetail", "showEmpty", "emptyValue", "autoGetLocation"], { locationType: "pp-cc-aa", addressDetail: "true", showEmpty: "false", emptyValue: "请选择", autoGetLocation: "false" }), [".scratch/omc-app-management/research/desensitized-config-shapes.md §3.2 ADDRESS", "cp-sources/frontend-dist/admin/js/12404.1675d362.js address schema", ".scratch/omc-app-management/evidence/field-families-fwb-20260914.md:113-119 (fwb field row {\"defaultValue\":\"\",\"virtualField\":false})"]),
52
+ 11: shape(11, face([], { dictionaryData: "" }), face([], { schemaCode: "", displayField: "name", relativePropertyMatchCode: "name", queryCode: "", linkMode: true, showStyle: "", selectMode: "popup", isAuthorize: true, treeView: false, treeStartFromRoot: false, mappings: "", searchFormula: "", conditions: "" })),
53
+ // Ticket 108 determination: selection families (12/13/14/15) emit NO
54
+ // dictionaryData — designer-produced rows carry none of it (fwb §12-15
55
+ // outer key unions) and live 8.6.26 proves the platform merely
56
+ // accepts-and-persists the extra key without requiring it. The other
57
+ // text/date/relation families keep the designer-carried empty-string
58
+ // default; ATTACHMENT(6) stays intentionally empty (system rows often
59
+ // have no options). The wider 2026-09-14 key unions are multi-field
60
+ // unions of designer rows — platform auto-fill territory, never required
61
+ // for creation (minimal default + caller passthrough is the boundary).
62
+ 12: shape(12, face([], {}), face([], { defaultValue: "", options: "", hasLabel: false })),
63
+ 13: shape(13, face([], {}), face([], { defaultValue: "", options: "", hasLabel: false, direction: "horizontal", displaySetting: "showSelected" })),
64
+ 14: shape(14, face([], {}), face([], { displayEmpty: true, emptyValue: "请选择", multi: false })),
65
+ 15: shape(15, face([], {}), face([], { displayEmpty: true, emptyValue: "请选择", multi: true })),
66
+ 16: shape(16, face([], { dictionaryData: "" }), face([], {})),
67
+ 17: shape(17, face(["defaultValue", "dictionaryData", "maxLength"], { defaultValue: "", dictionaryData: "", maxLength: 200 }), face(["defaultValue", "placeholder", "maxLength"], { defaultValue: "", placeholder: "请输入", maxLength: 200 }), [".scratch/omc-app-management/research/desensitized-config-shapes.md §3.2 HYPER_LINK", "cp-sources/frontend-dist/admin/js/12404.1675d362.js URL schema"]),
68
+ 28: shape(28, face(["format"], { defaultValue: "", format: "HH:mm" }), face(["format", "format1"], { defaultValue: "none", dataItemDefault: "", format: "HH:mm", format1: "HH:mm", syncFormate: true }), [".scratch/omc-app-management/research/desensitized-config-shapes.md §3.2 TIME", "cp-sources/frontend-dist/admin/js/46948.4825f4e2.js Time1760153573819"]),
69
+ // Org-selector families. Evidence: the yst/fwb published-face dumps
70
+ // (origin/research/yst-examples, 2656 rows of types 5/50/51/60/61) show the
71
+ // FIELD row (bizproperty) options as null or `{defaultValue, mappings,
72
+ // virtualField, noRepeat}` — deptVisible/multi NEVER appear there. The
73
+ // deptVisible/multi discriminator is FORM-CONTROL-face only (source:
74
+ // DataModelInitUtils.updateSelectionType / BizPropertyTypeUpdateUtils.
75
+ // disposeUpdateType read `control.options`; ticket 69 already writes them on
76
+ // the control face). Field faces below mirror the observed publish-safe
77
+ // shapes (single: defaultValue ""; multi: defaultValue []).
78
+ 5: shape(5, face([], { defaultValue: [], mappings: "", virtualField: false }), face(["defaultValue", "deptVisible", "multi"], { defaultValue: [], defaultValueType: "", deptVisible: "all", multi: "true" })),
79
+ 50: shape(50, face([], { defaultValue: "", mappings: "", virtualField: false }), face(["deptVisible", "multi"], { deptVisible: "user", multi: "false" })),
80
+ 51: shape(51, face([], { defaultValue: [], mappings: "", virtualField: false }), face(["deptVisible", "multi"], { deptVisible: "user", multi: "true" })),
81
+ 60: shape(60, face([], { defaultValue: [], mappings: "", virtualField: false }), face(["deptVisible", "multi"], { deptVisible: "org", multi: "false" })),
82
+ 61: shape(61, face([], { defaultValue: [], mappings: "", virtualField: false }), face(["deptVisible", "multi"], { deptVisible: "org", multi: "true" })),
83
+ 100: shape(100, face([], {}), face([], { defaultValue: null })),
84
+ });
85
+ export const BLOCKED_CAPABILITY_SHAPES = Object.freeze({});
86
+ /**
87
+ * Ticket 101 — control codes observed in real forms but with NO field-family
88
+ * emission path. PHONE(24) and PHOTO(10) are authoritative 8.6.26 control
89
+ * codes (BizFormPropertyType.java:23-51), but BizPropertyType.java:27-54 has
90
+ * no PHONE/PHOTO entry, so no accepted field family can scaffold them and no
91
+ * options shape is evidenced for a write. Declared blocked so the absence
92
+ * reads as policy (fail-closed), not omission; caller-provided explicit
93
+ * layouts are unaffected. Adding an emission path requires new family
94
+ * evidence AND consciously lifting this declaration.
95
+ */
96
+ export const BLOCKED_CONTROL_CODES = Object.freeze({
97
+ 24: "PHONE control has no BizPropertyType mapping (BizPropertyType.java:27-54) — no field family emits it; creation not offered until a family + options shape are evidenced",
98
+ 10: "PHOTO control has no BizPropertyType mapping (BizPropertyType.java:27-54) — no field family emits it; creation not offered until a family + options shape are evidenced",
99
+ });
100
+ export const CAPABILITY_SHAPE_ACCEPTED_TYPES = Object.freeze(Object.keys(FIELD_CAPABILITY_SHAPES).map(Number));
101
+ export function capabilityShapeFor(type) {
102
+ return FIELD_CAPABILITY_SHAPES[type] ?? BLOCKED_CAPABILITY_SHAPES[type];
103
+ }
104
+ export function capabilityShapeError(type, faceName, options) {
105
+ const current = capabilityShapeFor(type);
106
+ if (!current)
107
+ return `propertyType ${type} has no capability shape`;
108
+ if (current.blocked)
109
+ return `${current.citation ?? "blocked capability"} (propertyType ${type})`;
110
+ const face = current[faceName];
111
+ const missing = face.requiredKeys.filter((key) => options[key] === undefined || ((face.nonEmptyKeys ?? []).includes(key) && options[key] === ""));
112
+ return missing.length > 0 ? `capability shape ${faceName} missing required keys for propertyType ${type}: ${missing.join(", ")}` : undefined;
113
+ }
114
+ export function assertCapabilityShapeContract() {
115
+ for (const type of CAPABILITY_SHAPE_ACCEPTED_TYPES) {
116
+ const current = FIELD_CAPABILITY_SHAPES[type];
117
+ if (!current)
118
+ throw new Error(`capability shape missing for propertyType ${type}`);
119
+ if (current.blocked && !current.citation)
120
+ throw new Error(`blocked capability shape ${type} requires citation`);
121
+ if (current.controlType !== CAPABILITY_CONTROL_TYPES[type])
122
+ throw new Error(`capability shape control type mismatch for ${type}`);
123
+ for (const key of current.field.requiredKeys)
124
+ if (!(key in current.field.defaults))
125
+ throw new Error(`field shape ${type} missing default for ${key}`);
126
+ for (const key of current.control.requiredKeys)
127
+ if (!(key in current.control.defaults))
128
+ throw new Error(`control shape ${type} missing default for ${key}`);
129
+ }
130
+ }
131
+ export function assertCapabilityFace(type, faceName, options) {
132
+ const current = capabilityShapeFor(type);
133
+ if (!current)
134
+ throw new Error(`capability shape missing for propertyType ${type}`);
135
+ if (current.blocked)
136
+ throw new Error(`propertyType ${type} is blocked: ${current.citation ?? "missing evidence"}`);
137
+ const face = current[faceName];
138
+ const missing = face.requiredKeys.filter((key) => options[key] === undefined || ((face.nonEmptyKeys ?? []).includes(key) && options[key] === ""));
139
+ if (missing.length > 0)
140
+ throw new Error(`capability shape ${faceName} missing required keys for propertyType ${type}: ${missing.join(", ")}`);
141
+ }
142
+ export function deriveFieldOptions(type, explicit) {
143
+ const current = capabilityShapeFor(type);
144
+ if (!current || current.blocked)
145
+ throw new Error(`propertyType ${type} is blocked or has no capability shape`);
146
+ return { ...current.field.defaults, ...(explicit ?? {}) };
147
+ }
148
+ export function deriveControlOptions(type, explicit) {
149
+ const current = capabilityShapeFor(type);
150
+ if (!current || current.blocked)
151
+ throw new Error(`propertyType ${type} is blocked or has no capability shape`);
152
+ return { ...current.control.defaults, ...(explicit ?? {}) };
153
+ }
154
+ //# sourceMappingURL=capability-shape.js.map
package/dist/index.js CHANGED
@@ -1,3 +1,10 @@
1
+ export * from "./zip.js";
2
+ export * from "./capability-shape.js";
3
+ export * from "./json-column.js";
4
+ export * from "./refusal.js";
5
+ export * from "./wire-facts.js";
6
+ export * from "./relation-shape.js";
7
+ export * from "./list-view-wire.js";
1
8
  export * from "./ordinary-synthesis-router.js";
2
9
  export * from "./cloudpivot-adapter.js";
3
10
  export * from "./cloudpivot-fingerprints.js";
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Ticket 52 — one explicit home for "a platform JSON column may arrive as a
3
+ * string (or an object)" decoding/encoding. Previously every lane carried a
4
+ * local copy with subtly different behaviour; the deliberate differences are
5
+ * now NAMED POLICIES chosen at each call site:
6
+ *
7
+ * - `JSON_COLUMN_PASSTHROUGH` — non-strings pass through untouched
8
+ * (undefined stays undefined); unparseable strings return verbatim.
9
+ * (datarule options, field.list readback)
10
+ * - `JSON_COLUMN_NULL_COALESCE` — non-strings null-coalesce (undefined →
11
+ * null); blank/unparseable strings return verbatim ("" stays "").
12
+ * (businessrule node/route/schedulerSetting, b2 configJson/parameter faces,
13
+ * rule-support parameter faces, list-view condition options)
14
+ * - `{ invalid: "throw" }` — a corrupt column is a hard error
15
+ * (field.update merge base reads the stored options strictly).
16
+ *
17
+ * Blank strings ("", " ") are not valid JSON, so they always take the
18
+ * `invalid` branch; at every previous site that meant passthrough.
19
+ */
20
+ export const JSON_COLUMN_PASSTHROUGH = { nonString: "passthrough", invalid: "passthrough" };
21
+ export const JSON_COLUMN_NULL_COALESCE = { nonString: "null", invalid: "passthrough" };
22
+ export function decodeJsonColumn(value, policy) {
23
+ if (typeof value !== "string")
24
+ return policy.nonString === "null" ? value ?? null : value;
25
+ try {
26
+ return JSON.parse(value);
27
+ }
28
+ catch (error) {
29
+ if (policy.invalid === "throw")
30
+ throw error;
31
+ return value;
32
+ }
33
+ }
34
+ /** Wire encode: a string is sent as-is, anything else is JSON-stringified. */
35
+ export function encodeJsonColumn(value) {
36
+ return typeof value === "string" ? value : JSON.stringify(value);
37
+ }
38
+ /**
39
+ * Decode an options face that may be a JSON string or already an object into
40
+ * a plain record (or undefined). Policies preserve the previous per-site
41
+ * semantics: relation options accept strings/objects/arrays loosely; the
42
+ * FORMULA formulaConfig validator only accepts strings that parse to a
43
+ * non-array object; field.update/form-scaffold strict reads throw on corrupt
44
+ * strings.
45
+ */
46
+ export function decodeJsonObjectColumn(value, policy) {
47
+ const asRecord = (parsed) => {
48
+ if (!parsed || typeof parsed !== "object")
49
+ return undefined;
50
+ if (Array.isArray(parsed))
51
+ return policy.array === "reject" ? undefined : parsed;
52
+ return parsed;
53
+ };
54
+ if (typeof value === "string") {
55
+ try {
56
+ return asRecord(JSON.parse(value));
57
+ }
58
+ catch (error) {
59
+ if (policy.invalid === "throw")
60
+ throw error;
61
+ return undefined;
62
+ }
63
+ }
64
+ if (policy.object === "reject")
65
+ return undefined;
66
+ return asRecord(value);
67
+ }
68
+ //# sourceMappingURL=json-column.js.map
@@ -0,0 +1,101 @@
1
+ /** 列表视图按钮 wire 契约(真 lane 与假平台共用):动作枚举、按钮校验。原居 cli list-view-shapes;C1 下沉。 */
2
+ import { ToolRefusalError } from "./refusal.js";
3
+ export function wireEnum(key, index) {
4
+ return {
5
+ key,
6
+ names: Object.keys(index),
7
+ index,
8
+ name: Object.fromEntries(Object.entries(index).map(([name, value]) => [value, name])),
9
+ };
10
+ }
11
+ /**
12
+ * BizQueryActionModel enums. `QueryActionType` (QueryActionType.java:13-35) and
13
+ * `QueryActionRelativeType` (BIZSHEET 0/WORKFLOW 1) both carry
14
+ * `@JsonValue getIndex()` so the wire body is numeric; the tool speaks the enum
15
+ * name and decodes on readback. `associationType`/`associationCode` are the
16
+ * JSON names of the DTO fields `queryActionRelativeType`/`relativeCode`
17
+ * (`@JsonProperty`, BizQueryActionModel.java:31-35).
18
+ */
19
+ export const QUERY_ACTION_TYPE = wireEnum("QueryActionType", {
20
+ QUERY: 1,
21
+ ADD: 2,
22
+ EDIT: 3,
23
+ DELETE: 4,
24
+ IMPORT: 5,
25
+ EXPORT: 6,
26
+ CUSTOM: 7,
27
+ QR_CODE: 8,
28
+ EDITOWNER: 9,
29
+ BATCH_PRINT: 10,
30
+ BATCH_UPDATE: 11,
31
+ CUSTOMIZE_BTN: 12,
32
+ PRINT: 13,
33
+ COPY: 14,
34
+ TEMPORARY: 15,
35
+ SUBMIT: 16,
36
+ CLEAR_ROW: 17,
37
+ ADD_BEFORE: 18,
38
+ ADD_AFTER: 19,
39
+ MANUAL_SYNC: 20,
40
+ IMPORT_ATTACHMENT: 21,
41
+ EXPORT_ATTACHMENT: 22,
42
+ BATCH_SUBMIT: 23,
43
+ });
44
+ export const QUERY_ACTION_RELATIVE = wireEnum("QueryActionRelativeType", { BIZSHEET: 0, WORKFLOW: 1 });
45
+ function decodeButtonActionConfig(value, code) {
46
+ if (value === undefined)
47
+ return undefined;
48
+ if (typeof value === "string") {
49
+ try {
50
+ value = JSON.parse(value);
51
+ }
52
+ catch (error) {
53
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: actionConfig must be valid JSON (${error.message})` });
54
+ }
55
+ }
56
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
57
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: actionConfig must decode to an object` });
58
+ }
59
+ return value;
60
+ }
61
+ function requireButtonConfig(config, code, bindAction) {
62
+ const required = bindAction === "SCRIPT" ? ["scriptData"] : bindAction === "BUSINESS_RULE" || bindAction === "FLOW" ? ["targetCode"] : bindAction === "ADD_DATA" || bindAction === "FORM" ? ["schemaCodeVal", "formListData", "configureList"] : [];
63
+ for (const key of required) {
64
+ if (config?.[key] === undefined || config[key] === null) {
65
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: ${bindAction} actionConfig requires ${key}` });
66
+ }
67
+ }
68
+ if (bindAction === "SCRIPT" && typeof config?.scriptData !== "string") {
69
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: SCRIPT actionConfig.scriptData must be a string` });
70
+ }
71
+ if ((bindAction === "ADD_DATA" || bindAction === "FORM") && (typeof config?.schemaCodeVal !== "object" || config.schemaCodeVal === null || Array.isArray(config.schemaCodeVal) || !Array.isArray(config.formListData) || !Array.isArray(config.configureList))) {
72
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: ${bindAction} actionConfig requires object schemaCodeVal and array formListData/configureList` });
73
+ }
74
+ }
75
+ export function validateButtonActionConfig(entry) {
76
+ const bindAction = typeof entry.bindAction === "string" ? entry.bindAction : "";
77
+ const code = String(entry.code ?? "");
78
+ const config = decodeButtonActionConfig(entry.actionConfig, code);
79
+ if (bindAction === "BUSINESS_RULE") {
80
+ if (typeof entry.targetCode !== "string" || entry.targetCode.length === 0) {
81
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: BUSINESS_RULE requires targetCode` });
82
+ }
83
+ return;
84
+ }
85
+ if (bindAction === "FLOW") {
86
+ if (typeof entry.targetCode !== "string" || entry.targetCode.length === 0) {
87
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: FLOW requires targetCode (workflow code)` });
88
+ }
89
+ // A caller-supplied FLOW actionConfig must carry WorkflowListData; the
90
+ // runtime indexes [0].workflowCode and iterates configureList. Omitted is
91
+ // fine — expandButtonActionConfigs derives both from targetCode.
92
+ if (config !== undefined && (typeof config !== "object" || config === null || !Array.isArray(config.WorkflowListData))) {
93
+ throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-invalid", message: `buttons ${code}: FLOW actionConfig requires a WorkflowListData array (or omit actionConfig and the tool derives it from targetCode)` });
94
+ }
95
+ return;
96
+ }
97
+ if ((bindAction === "ADD_DATA" || bindAction === "FORM") && (entry.targetModel !== undefined || entry.targetForm !== undefined || entry.configureList !== undefined) && entry.actionConfig === undefined)
98
+ return;
99
+ requireButtonConfig(config, code, bindAction);
100
+ }
101
+ //# sourceMappingURL=list-view-wire.js.map
@@ -0,0 +1,9 @@
1
+ /** 受治理拒收(跨适配器契约):真 lane 与假平台抛同一类、同一实例。原居 cli tool-types;C1 下沉。 */
2
+ export class ToolRefusalError extends Error {
3
+ payload;
4
+ constructor(payload) {
5
+ super(String(payload["blocker"] ?? "tool refused"));
6
+ this.payload = payload;
7
+ }
8
+ }
9
+ //# sourceMappingURL=refusal.js.map
@@ -0,0 +1,25 @@
1
+ /** 关联字段目标模型解析(真 lane 与假平台共用)。原居 cli field-families;C1 下沉。 */
2
+ import { decodeJsonObjectColumn } from "./json-column.js";
3
+ /** relation options face decode; policy: string-or-object accepted, garbage → undefined. */
4
+ export function relationOptions(options) {
5
+ return decodeJsonObjectColumn(options, { invalid: "undefined", array: "accept", object: "accept" });
6
+ }
7
+ /**
8
+ * The relation target model of a bizproperty row. Relation families
9
+ * (WORK_SHEET / MULT_WORK_SHEET) carry the target model code both as the
10
+ * row-level `relativeCode` and as the `schemaCode` key inside the options JSON
11
+ * (field.create writes both). RELEVANCE_DATA(16) is deliberately excluded: it
12
+ * references another *field* (relevanceField/referenceField), not a model, so
13
+ * it adds no inbound model reference of its own — the local WORK_SHEET field it
14
+ * resolves through already carries that reference. Either face counts as an
15
+ * inbound reference for the model.delete precheck (ticket 23: the server's
16
+ * deleteFunctions dispatcher refuses asynchronously with
17
+ * "xxx被模型 yyy 引用,不能删除" — the tool surfaces that check at call time).
18
+ */
19
+ export function relationTargetOf(row) {
20
+ if (typeof row.relativeCode === "string" && row.relativeCode.trim())
21
+ return row.relativeCode.trim();
22
+ const schemaCode = relationOptions(row.options)?.schemaCode;
23
+ return typeof schemaCode === "string" && schemaCode.trim() ? schemaCode.trim() : undefined;
24
+ }
25
+ //# sourceMappingURL=relation-shape.js.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * CloudPivot 平台 wire 事实(真机实证):规则状态/动态源键/模型互删码/运行面查询提交契约与算子表。
3
+ * 原居 omc-cli 契约文件;C1 拆分后真 lane 与假平台(@stalfh233/omc-fake-platform)共同取用——一张表喂两个适配器。
4
+ */
5
+ export const RULE_STATUS = Object.freeze({ enabled: "ENABLE", disabled: "DISABLED" });
6
+ export const DYNAMIC_CONDITION_SOURCE_KEY = "currentSchemaDataItem";
7
+ export const DYNAMIC_ACTION_SOURCE_KEY = "currentDataItemValue";
8
+ export const MODEL_REFERENCED_DELETE_ERRCODE = 50000;
9
+ /**
10
+ * M2 runtime-data wire facts (live-proven). The submit chain, the replayToken
11
+ * gate, the three-layer queryCondition, and the propertyType injection are the
12
+ * single source for the handler, the arg schemas, and the fake.
13
+ */
14
+ export const RUNTIME_DATA_WIRE = Object.freeze({
15
+ queryConditionShape: "List<List<List<FilterModel>>>(level 1 AND across groups, level 2 OR across and-groups, level 3 AND across leaves)",
16
+ dslShape: "or>and>叶子",
17
+ queryBodyCanon: "filters:[], queryVersion:1, queryCode=schemaCode, showTotal",
18
+ typeCastErrcode: 10009,
19
+ emptyModelErrcode: 301005,
20
+ absenceErrcode: 304001,
21
+ formTypeNoWorkflow: "2",
22
+ formTypeWorkflow: "1",
23
+ replayTokenEndpoint: "/api/api/runtime/form/getReplayToken",
24
+ replayTokenErrcode: 100005,
25
+ approvalNpeErrcode: 50000,
26
+ workflowStartDuplicateErrcode: 40000,
27
+ submitChain: "submit 自持久化(ticket 18 源码证据):form/submit 持久化 bizObject.data——formType=2 submitBizSheet→saveBizObject(置 COMPLETED,mergeObject 合并);formType=1 workItem→SubmitDataChangeHandlerImpl.updateBizObject;payload {id,schemaCode,sheetCode,ownerId?,data}+actionCode:\"submit\",owner 服务端以库内行回填,data 剥 engine 键(sequenceStatus/sequenceNo)",
28
+ submitInPlace: "无流程且 bizObjectId 有值时先 form/load 预检(缺席 304001 拒绝),再以预检行+formData 合成最小对象直提 runtime/form/submit(submit 自持久化,无预 save/构建 load);submit 返回 objectId 必须仍是目标 id,否则拒绝;仅无 workflowCode/workItemId/workflowInstanceId 的无 id 便捷链先 form/save 建稿再直提(新建记录);带 workItemId 或 workflowInstanceId 而缺 bizObjectId 时 fail-closed 拒绝(submit-biz-object-id-required);workflowCode + 既有无实例 bizObjectId 在 8.6.26 无 REST 就地面(见 workflowStartCreateOnly),持续拒绝并指向 workflowDraftSave 两段式",
29
+ workflowDraftSave: "门户暂存形状(ticket 13):POST runtime/form/save 顶层带 workflowCode(无 workflowInstanceId/workItemId)→ FormRuntimeController.saveWorkflow→startWorkflow(finishStart=FALSE):同 id 建 DRAFT 行 + DRAFT 流程实例 + 发起 workItem,FormSaveVO 返回 {objectId,workflowInstanceId,workItem:{id}};必须先 GET getReplayToken(isStartWf 分支 verifyReplay,否则 100005),且 data 显式 sequenceStatus:\"DRAFT\"(createAndGetBizObject 空缺省按 PROCESSING 建实例);提交该草稿 = form/submit formType=1 + workItemId + workflowInstanceId + bizObject.id(submitWorkflowByInstanceIdAndItemId),同 id 就地 PROCESSING,绝不走 workflowCode 分支",
30
+ workflowStartCreateOnly: "form/submit formType=1 + workflowCode(无 workItemId)→ submitWorkflowByWorkflowCode→startWorkflowInstance(create)→BizObjectFacadeImpl.createAndGetBizObject 纯 INSERT(BizObjectInsertModel 无存在性检查)→ 既有 id 必 40000 Duplicate entry(TID 771f64ee 全栈:ExecutorBase INSERT→BizObjectServiceImplCompanion:275→40000);8.6.26 唯一 load+update 型就地发起是 WorkflowInstanceFacade 5 参重载(908 行,facade 层供二开扩展消费),无 REST 面",
31
+ sheetResolution: "explicit caller arg > default-form sheetCode from bizsheet/get > schemaCode fallback",
32
+ updateBranch: "POST runtime/form/save 更新既有记录:目标 id 必须放进 bizObject.data[\"id\"](平台 BizObjectModel.getId() 读 data 的 id 键;顶层 bizObject.id 会被忽略并退化为新建);命中既有记录时 FormRuntimeController.saveBizSheet→BizObjectFacadeImpl.updateBizObject,未命中则 createBizObject 插入",
33
+ updateStatusRule: "既有记录的 DRAFT/COMPLETED 状态在未显式传 sequenceStatus 时原样保留(saveBizSheet 分支),COMPLETED 记录可直接 update,无需流程",
34
+ updateMergeRule: "updateBizObject.mergeObject 以库内旧值打底、以本次 data 覆盖 → 部分字段(patch)更新,未传字段保持不变",
35
+ childRowStatusNote: "子表行(CHILD_TABLE(8))在 form-save 路径按每行 `rowStatus` 分派(BizObjectUpdateModel.convertBizObjectOpItem):Added=插入 / Modified=更新 / Deleted=删除;**缺失或其它值=immutable(静默忽略)**。门户前端会为改动行标 `rowStatus:\"Modified\"`;手拼载荷漏了它,子表改动不落库、但写仍返回 errcode 0(真机 2026-10-08:data.update 改子表行无 rowStatus,process_name/计划时间读回全不变)。OMC 自动补:带自身 id 的行→\"Modified\",无 id 的行→\"Added\",显式 rowStatus 原样保留。",
36
+ updateAbsentGuard: "平台对\"带 id 但库内不存在\"不报错而是静默插入(createBizObject),故 data.update 先 form/load 预检,命中缺席则以 304001 拒绝,绝不静默转新增",
37
+ projectionHint: "所选列在默认视图未配置(值可能存在但列表投影为 null);先用 listview.configure 配置列,或用 data.load 读单条权威值",
38
+ formulaFieldNote: "计算字段(FORMULA / 表单「计算」控件、子表计算规则)**不在服务端 API 路径回填/重算**:门户是**前端**算好再提交,API save/update/submit 都不会替它算(真机 2026-09-25:子表计算规则字段落库仍 null;纯主表表达式会算)。OMC 的定位是「字段配好即默认它在前台会算」——**写测试数据时应在 formData 里显式带上预算好的计算值**(让数据与预期自洽),**不要指望平台在 API 保存时替它算,也不要用 API 回读值去断言公式/计算是否正确**。**这类纯前台行为最终由人工在前台(配表单→填表)确认。** 另:带**子表计算字段**的模型,API data.save/data.update 可能因服务端重算(CalculateHandler→FormulaProcessor)NPE 而 50000(真机 2026-10-07,前台路径正常)——此类模型请改用门户录入或避开该字段,别据此判定配置错误。",
39
+ });
40
+ function encodeScalar(value) {
41
+ if (value === null || value === undefined)
42
+ return "";
43
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
44
+ return String(value);
45
+ if (value instanceof Date)
46
+ return value.toISOString();
47
+ return JSON.stringify(value);
48
+ }
49
+ function encodeList(value) {
50
+ if (Array.isArray(value))
51
+ return value.map(encodeScalar).join(";");
52
+ if (typeof value === "string")
53
+ return value;
54
+ return encodeScalar(value);
55
+ }
56
+ const numeric = (value) => Number(value);
57
+ function betweenBounds(expected) {
58
+ const [low, high] = expected.split(";").map(Number);
59
+ return [low ?? 0, high ?? 0];
60
+ }
61
+ export const RUNTIME_QUERY_OPERATORS = Object.freeze({
62
+ Eq: { encode: encodeScalar, matches: (actual, expected) => String(actual ?? "") === expected },
63
+ NotEq: { encode: encodeScalar, matches: (actual, expected) => String(actual ?? "") !== expected },
64
+ Gt: { encode: encodeScalar, matches: (actual, expected) => numeric(actual) > numeric(expected) },
65
+ Lt: { encode: encodeScalar, matches: (actual, expected) => numeric(actual) < numeric(expected) },
66
+ Gte: { encode: encodeScalar, matches: (actual, expected) => numeric(actual) >= numeric(expected) },
67
+ Lte: { encode: encodeScalar, matches: (actual, expected) => numeric(actual) <= numeric(expected) },
68
+ Like: { encode: encodeScalar, matches: (actual, expected) => String(actual ?? "").includes(expected) },
69
+ NotLike: { encode: encodeScalar, matches: (actual, expected) => !String(actual ?? "").includes(expected) },
70
+ In: { encode: encodeList, matches: (actual, expected) => expected.split(";").includes(String(actual ?? "")) },
71
+ NotIn: { encode: encodeList, matches: (actual, expected) => !expected.split(";").includes(String(actual ?? "")) },
72
+ Between: { encode: encodeList, matches: (actual, expected) => { const [low, high] = betweenBounds(expected); return numeric(actual) >= low && numeric(actual) <= high; } },
73
+ NotBetween: { encode: encodeList, matches: (actual, expected) => { const [low, high] = betweenBounds(expected); return !(numeric(actual) >= low && numeric(actual) <= high); } },
74
+ IsNull: { encode: () => "", matches: (actual) => actual === null || actual === undefined },
75
+ IsNotNull: { encode: () => "", matches: (actual) => actual !== null && actual !== undefined },
76
+ });
77
+ export const ENGINE_OWNED_KEYS = Object.freeze(["sequenceStatus", "2147483647_sequenceNo"]);
78
+ export const SUBMIT_STATUS_TRANSITIONS = Object.freeze({
79
+ /** Start submit / start-item finish moves the row DRAFT -> PROCESSING. */
80
+ workflowStart: "PROCESSING",
81
+ /** Approval submit with agree!==false completes the row. */
82
+ approvalAgree: "COMPLETED",
83
+ /** Approval submit with agree===false cancels the row. */
84
+ approvalReject: "CANCELED",
85
+ /** formType=2 submitBizSheet -> saveBizObject completes in place. */
86
+ inPlaceSubmit: "COMPLETED",
87
+ });
88
+ //# sourceMappingURL=wire-facts.js.map
package/dist/zip.js ADDED
@@ -0,0 +1,156 @@
1
+ import { inflateRawSync } from "node:zlib";
2
+ /**
3
+ * Ticket 106 — a minimal, dependency-free ZIP reader/writer for the bizservice
4
+ * IE package face.
5
+ *
6
+ * The platform export answers a ZIP with manifest.json + version.md
7
+ * (BizServiceIEController.java:124-138). Java's ZipOutputStream typically
8
+ * writes local headers with zeroed sizes plus a trailing data descriptor
9
+ * (general-purpose bit 3), so the READER resolves entries through the central
10
+ * directory (whose sizes/CRCs are authoritative) and only uses each local
11
+ * header to locate the data offset. The WRITER emits stored (method 0)
12
+ * entries — real zips for fake routes and tests; the platform reader accepts
13
+ * both stored and deflated entries.
14
+ */
15
+ const EOCD_SIGNATURE = 0x06054b50;
16
+ const CENTRAL_SIGNATURE = 0x02014b50;
17
+ const LOCAL_SIGNATURE = 0x04034b50;
18
+ const EOCD_SCAN_LIMIT = 65_557; // fixed EOCD (22) + max comment (65_535)
19
+ export class ZipFormatError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "ZipFormatError";
23
+ }
24
+ }
25
+ /** CRC-32 (IEEE 802.3), table-driven. */
26
+ const CRC_TABLE = (() => {
27
+ const table = new Array(256);
28
+ for (let n = 0; n < 256; n += 1) {
29
+ let c = n;
30
+ for (let k = 0; k < 8; k += 1)
31
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
32
+ table[n] = c >>> 0;
33
+ }
34
+ return table;
35
+ })();
36
+ function crc32(buffer) {
37
+ let crc = 0xffffffff;
38
+ for (const byte of buffer)
39
+ crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
40
+ return (crc ^ 0xffffffff) >>> 0;
41
+ }
42
+ /** Locate the End Of Central Directory record; returns its byte offset. */
43
+ function locateEocd(buffer) {
44
+ const start = Math.max(0, buffer.length - EOCD_SCAN_LIMIT);
45
+ for (let offset = buffer.length - 22; offset >= start; offset -= 1) {
46
+ if (buffer.readUInt32LE(offset) === EOCD_SIGNATURE) {
47
+ const commentLength = buffer.readUInt16LE(offset + 20);
48
+ if (offset + 22 + commentLength === buffer.length)
49
+ return offset;
50
+ }
51
+ }
52
+ throw new ZipFormatError("no end-of-central-directory record found");
53
+ }
54
+ /** Read every central-directory entry as name → decompressed Buffer. */
55
+ export function readZipEntries(buffer) {
56
+ const eocd = locateEocd(buffer);
57
+ const entryCount = buffer.readUInt16LE(eocd + 10);
58
+ let cursor = buffer.readUInt32LE(eocd + 16);
59
+ const entries = new Map();
60
+ for (let index = 0; index < entryCount; index += 1) {
61
+ if (cursor + 46 > buffer.length || buffer.readUInt32LE(cursor) !== CENTRAL_SIGNATURE)
62
+ throw new ZipFormatError(`central directory entry ${index} is malformed`);
63
+ const method = buffer.readUInt16LE(cursor + 10);
64
+ const compressedSize = buffer.readUInt32LE(cursor + 20);
65
+ const nameLength = buffer.readUInt16LE(cursor + 28);
66
+ const extraLength = buffer.readUInt16LE(cursor + 30);
67
+ const commentLength = buffer.readUInt16LE(cursor + 32);
68
+ const localOffset = buffer.readUInt32LE(cursor + 42);
69
+ const name = buffer.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
70
+ // The local header carries its OWN name/extra lengths (they may differ
71
+ // from the central copies); the data starts right after them.
72
+ if (localOffset + 30 > buffer.length || buffer.readUInt32LE(localOffset) !== LOCAL_SIGNATURE)
73
+ throw new ZipFormatError(`local header for "${name}" is malformed`);
74
+ const localNameLength = buffer.readUInt16LE(localOffset + 26);
75
+ const localExtraLength = buffer.readUInt16LE(localOffset + 28);
76
+ const dataStart = localOffset + 30 + localNameLength + localExtraLength;
77
+ const data = buffer.subarray(dataStart, dataStart + compressedSize);
78
+ if (method === 0)
79
+ entries.set(name, Buffer.from(data));
80
+ else if (method === 8)
81
+ entries.set(name, inflateRawSync(data));
82
+ else
83
+ throw new ZipFormatError(`entry "${name}" uses unsupported compression method ${method}`);
84
+ cursor += 46 + nameLength + extraLength + commentLength;
85
+ }
86
+ return entries;
87
+ }
88
+ /** Build a stored-entry ZIP (method 0, UTF-8 names flagged via bit 11). */
89
+ export function buildZip(files) {
90
+ const chunks = [];
91
+ const central = [];
92
+ let offset = 0;
93
+ for (const file of files) {
94
+ const nameBytes = Buffer.from(file.name, "utf8");
95
+ const crc = crc32(file.data);
96
+ const local = Buffer.alloc(30);
97
+ local.writeUInt32LE(LOCAL_SIGNATURE, 0);
98
+ local.writeUInt16LE(0x0800, 6); // UTF-8 name flag
99
+ local.writeUInt16LE(0, 8); // stored
100
+ local.writeUInt16LE(0, 10); // time
101
+ local.writeUInt16LE(0x2100, 12); // date (1996-01-01, deterministic)
102
+ local.writeUInt32LE(crc, 14);
103
+ local.writeUInt32LE(file.data.length, 18);
104
+ local.writeUInt32LE(file.data.length, 22);
105
+ local.writeUInt16LE(nameBytes.length, 26);
106
+ local.writeUInt16LE(0, 28);
107
+ chunks.push(local, nameBytes, file.data);
108
+ const entry = Buffer.alloc(46);
109
+ entry.writeUInt32LE(CENTRAL_SIGNATURE, 0);
110
+ entry.writeUInt16LE(0x031e, 4); // made by UNIX/ZIP 3.0
111
+ entry.writeUInt16LE(0x0014, 6); // extract by DOS/ZIP 2.0
112
+ entry.writeUInt16LE(0x0800, 8);
113
+ entry.writeUInt16LE(0, 10);
114
+ entry.writeUInt16LE(0, 12);
115
+ entry.writeUInt16LE(0x2100, 14);
116
+ entry.writeUInt32LE(crc, 16);
117
+ entry.writeUInt32LE(file.data.length, 20);
118
+ entry.writeUInt32LE(file.data.length, 24);
119
+ entry.writeUInt16LE(nameBytes.length, 28);
120
+ entry.writeUInt16LE(0, 30);
121
+ entry.writeUInt16LE(0, 32);
122
+ entry.writeUInt16LE(0, 34);
123
+ entry.writeUInt16LE(0, 36);
124
+ entry.writeUInt32LE(0, 38);
125
+ entry.writeUInt32LE(offset, 42);
126
+ central.push(entry, nameBytes);
127
+ offset += 30 + nameBytes.length + file.data.length;
128
+ }
129
+ const centralBuffer = Buffer.concat(central);
130
+ const eocd = Buffer.alloc(22);
131
+ eocd.writeUInt32LE(EOCD_SIGNATURE, 0);
132
+ eocd.writeUInt16LE(0, 4);
133
+ eocd.writeUInt16LE(0, 6);
134
+ eocd.writeUInt16LE(files.length, 8);
135
+ eocd.writeUInt16LE(files.length, 10);
136
+ eocd.writeUInt32LE(centralBuffer.length, 12);
137
+ eocd.writeUInt32LE(offset, 16);
138
+ eocd.writeUInt16LE(0, 20);
139
+ return Buffer.concat([...chunks, centralBuffer, eocd]);
140
+ }
141
+ /**
142
+ * Resolve an entry by BASENAME: the platform export compresses the staging
143
+ * DIRECTORY, so entries carry a `bizService-<uuid>/` prefix
144
+ * (live 2026-09-15: bizService-59d72d4a…/manifest.json).
145
+ */
146
+ export function zipEntryByBasename(entries, basename) {
147
+ const direct = entries.get(basename);
148
+ if (direct !== undefined)
149
+ return direct;
150
+ const suffix = `/${basename}`;
151
+ for (const [name, data] of entries)
152
+ if (name.endsWith(suffix))
153
+ return data;
154
+ return undefined;
155
+ }
156
+ //# sourceMappingURL=zip.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omc-core",
3
- "version": "0.4.37",
3
+ "version": "0.4.39",
4
4
  "description": "Shared OMC capability kernel: capability contracts, governance, and decoders. Zero DSH dependencies.",
5
5
  "type": "module",
6
6
  "files": [
@@ -12,7 +12,17 @@
12
12
  "!dist/**/*.d.ts.map"
13
13
  ],
14
14
  "exports": {
15
- ".": "./dist/index.js",
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ },
19
+ "./zip": "./dist/zip.js",
20
+ "./capability-shape": "./dist/capability-shape.js",
21
+ "./json-column": "./dist/json-column.js",
22
+ "./refusal": "./dist/refusal.js",
23
+ "./wire-facts": "./dist/wire-facts.js",
24
+ "./relation-shape": "./dist/relation-shape.js",
25
+ "./list-view-wire": "./dist/list-view-wire.js",
16
26
  "./package.json": "./package.json"
17
27
  },
18
28
  "scripts": {
@@ -34,5 +44,6 @@
34
44
  "type": "git",
35
45
  "url": "https://gitlab.h3yun.com/fuwenbin/oh-my-cpagents.git",
36
46
  "directory": "packages/omc-core"
37
- }
47
+ },
48
+ "types": "./dist/index.d.ts"
38
49
  }