@stalfh233/omc-cli 0.0.0-stage → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +293 -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,545 @@
|
|
|
1
|
+
import { validateBusinessCode } from "../../code-rules.js";
|
|
2
|
+
import { fieldSemanticDescriptor, prepareFieldChange } from "../../field-change.js";
|
|
3
|
+
import { CHILD_TABLE_SCALAR_PROPERTY_TYPES, FIELD_PROPERTY_TYPES, OUT_OF_COVERAGE_FIELD_TYPES, isNumericalScale } from "../../field-families.js";
|
|
4
|
+
import { ENDPOINTS } from "../../endpoints.js";
|
|
5
|
+
import { wireCall, wireRows } from "../../wire.js";
|
|
6
|
+
import { canonicalJson } from "../dsl.js";
|
|
7
|
+
import { refuse } from "../capability.js";
|
|
8
|
+
import { describe, isScalarString, pushError } from "../validation.js";
|
|
9
|
+
import { toPlain } from "../yaml.js";
|
|
10
|
+
/** Keys a field item accepts. Relation fields may carry `relativeCode` /
|
|
11
|
+
* `displayField` at the top level too — that is the `field.create` row shape,
|
|
12
|
+
* and accepting it here means a caller can copy a working `field.create` args
|
|
13
|
+
* block straight into the intent instead of translating it (bench41: an agent
|
|
14
|
+
* wrote `relativeCode:` at the field level, got `intent-key-unknown`, and fell
|
|
15
|
+
* back to `field.update`). They are lifted into `options` at parse time. */
|
|
16
|
+
const FIELD_KEYS = new Set(["code", "name", "type", "choices", "subFields", "options", "relativeCode", "displayField", "absent"]);
|
|
17
|
+
/** Field options that callers habitually write at the top level instead of under
|
|
18
|
+
* `options` (2026-09-28 bench65: an agent declared `requiredFormula` beside
|
|
19
|
+
* `type`, got `intent-key-unknown`, and only then moved it into `options`). The
|
|
20
|
+
* message names `options` so the recovery is one edit, not a round. */
|
|
21
|
+
const FIELD_OPTION_KEYS = new Set(["requiredFormula", "readonlyCondition", "displayFormula", "shortTextStitch", "scale", "format"]);
|
|
22
|
+
/** Keys that describe a live field and are meaningless on a declared-absent one. */
|
|
23
|
+
const FIELD_PRESENT_ONLY_KEYS = ["name", "type", "choices", "subFields", "options", "relativeCode", "displayField"];
|
|
24
|
+
/** Keys a child-table column accepts. */
|
|
25
|
+
const SUB_FIELD_KEYS = new Set(["code", "name", "type", "choices", "relativeCode", "options"]);
|
|
26
|
+
/** Field families whose TEXT-CHOICE semantics accept `choices`. SELECTION(5) is a
|
|
27
|
+
* people/department mixed selector (control type 62), not a text choice, and is
|
|
28
|
+
* deliberately excluded (choices on it produced an unstorable field; 2026-09-27). */
|
|
29
|
+
const CHOICE_FAMILIES = new Set(["RADIO", "CHECKBOX", "DROPDOWN_BOX", "DROPDOWN_MULTI_BOX"]);
|
|
30
|
+
/** Relation families whose target model rides the row-level `relativeCode`. */
|
|
31
|
+
const RELATION_FAMILIES = new Set(["WORK_SHEET", "MULT_WORK_SHEET"]);
|
|
32
|
+
/**
|
|
33
|
+
* The row-level `relativeCode` for a TOP-LEVEL relation field. `field.create`
|
|
34
|
+
* wants the target on the row (and also `options.schemaCode`); the intent DSL
|
|
35
|
+
* carries it inside `options` (the field node has no top-level `relativeCode`
|
|
36
|
+
* key), so without this lift a declared WORK_SHEET created a field with NO
|
|
37
|
+
* relation target (dry-run summary "关联单选,挂在 X" instead of "…,关联 Y").
|
|
38
|
+
* Mirrors the child-column path (subFieldArgs already lifts `sub.relativeCode`).
|
|
39
|
+
*/
|
|
40
|
+
function relationCodeOf(field) {
|
|
41
|
+
if (!RELATION_FAMILIES.has(String(field.type)))
|
|
42
|
+
return undefined;
|
|
43
|
+
const raw = field.options?.relativeCode ?? field.options?.schemaCode;
|
|
44
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
|
|
45
|
+
}
|
|
46
|
+
const familyOf = (wireType) => Object.entries(FIELD_PROPERTY_TYPES).find(([, value]) => value === wireType)?.[0];
|
|
47
|
+
/** Explicit option keys the intent declares (relations lift `relativeCode` to the row). */
|
|
48
|
+
function declaredOptionKeys(field) {
|
|
49
|
+
const keys = Object.keys(field.options ?? {}).filter((key) => key !== "relativeCode" && key !== "schemaCode");
|
|
50
|
+
if ((field.choices?.length ?? 0) > 0)
|
|
51
|
+
keys.push("optionsSet");
|
|
52
|
+
return [...new Set(keys)];
|
|
53
|
+
}
|
|
54
|
+
/** The options the write seam would send for a declared field (its own normalizer). */
|
|
55
|
+
function expectedFieldOptions(schemaCode, field) {
|
|
56
|
+
try {
|
|
57
|
+
return prepareFieldChange({
|
|
58
|
+
schemaCode,
|
|
59
|
+
code: field.code,
|
|
60
|
+
name: field.name ?? "",
|
|
61
|
+
propertyType: field.type,
|
|
62
|
+
...(field.choices ? { choices: field.choices } : {}),
|
|
63
|
+
...(field.options ? { options: field.options } : {}),
|
|
64
|
+
}).options;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Compare only the option keys the intent explicitly declared against the live
|
|
72
|
+
* row (a projection, not deep equality: the platform stores many derived keys we
|
|
73
|
+
* never declared). Without this, a field whose options silently failed to land
|
|
74
|
+
* (observed 2026-09-27: `field.list` briefly reported options:null for a row
|
|
75
|
+
* that had them, and the final readback called the intent equivalent) is
|
|
76
|
+
* reported as converged — the agent then discovers it at runtime instead.
|
|
77
|
+
*/
|
|
78
|
+
function compareFieldOptions(schemaCode, field, live, emit) {
|
|
79
|
+
const keys = declaredOptionKeys(field);
|
|
80
|
+
if (keys.length === 0)
|
|
81
|
+
return;
|
|
82
|
+
const resource = `field:${schemaCode}.${field.code}`;
|
|
83
|
+
const expected = expectedFieldOptions(schemaCode, field);
|
|
84
|
+
const liveOptions = (live.options ?? {});
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
if (key === "optionsSet") {
|
|
87
|
+
const customCodes = (value) => {
|
|
88
|
+
const custom = value?.custom;
|
|
89
|
+
return Array.isArray(custom) ? custom.map((entry) => String(entry?.code ?? "")) : [];
|
|
90
|
+
};
|
|
91
|
+
const want = customCodes(expected?.optionsSet).sort();
|
|
92
|
+
const got = customCodes(liveOptions.optionsSet).sort();
|
|
93
|
+
if (canonicalJson(want) !== canonicalJson(got))
|
|
94
|
+
emit.difference("field-options-mismatch", resource, `字段 ${resource} 的 choices 与 live 不一致(live=${canonicalJson(got)} intent=${canonicalJson(want)})`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (canonicalJson(expected?.[key] ?? null) !== canonicalJson(liveOptions[key] ?? null))
|
|
98
|
+
emit.difference("field-options-mismatch", resource, `字段 ${resource} 的 options.${key} 与 live 不一致(live=${canonicalJson(liveOptions[key] ?? null)} intent=${canonicalJson(expected?.[key] ?? null)})`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Map declared child columns to the exact `field.create` args. */
|
|
102
|
+
function subFieldArgs(subs) {
|
|
103
|
+
return subs.map((sub) => ({
|
|
104
|
+
code: sub.code,
|
|
105
|
+
name: sub.name,
|
|
106
|
+
propertyType: sub.type,
|
|
107
|
+
...(sub.choices ? { choices: sub.choices } : {}),
|
|
108
|
+
...(sub.relativeCode ? { relativeCode: sub.relativeCode } : {}),
|
|
109
|
+
...(sub.options ? { options: sub.options } : {}),
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* CHILD_TABLE(8) child columns: the parent row's `subSchema.properties`
|
|
114
|
+
* (code/name/propertyType). System/default columns the platform injects are
|
|
115
|
+
* kept too — the compiler only diffs declared codes and treats the rest as
|
|
116
|
+
* ownership-ignored, so nothing is deleted or reported.
|
|
117
|
+
*/
|
|
118
|
+
function subFieldsOf(row) {
|
|
119
|
+
const subSchema = row.subSchema;
|
|
120
|
+
if (!subSchema || typeof subSchema !== "object")
|
|
121
|
+
return [];
|
|
122
|
+
const properties = subSchema.properties;
|
|
123
|
+
if (!Array.isArray(properties))
|
|
124
|
+
return [];
|
|
125
|
+
return properties
|
|
126
|
+
.filter((child) => child !== null && typeof child === "object")
|
|
127
|
+
.map((child) => ({ code: String(child.code ?? ""), name: String(child.name ?? child.code ?? ""), type: Number(child.propertyType ?? child.type) }))
|
|
128
|
+
.filter((child) => child.code.length > 0 && Number.isFinite(child.type));
|
|
129
|
+
}
|
|
130
|
+
async function readFields(session, schemaCode) {
|
|
131
|
+
const draftRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "false" } })).data);
|
|
132
|
+
const publishedRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
|
|
133
|
+
const codeOf = (row) => String(row.code ?? row.propertyCode ?? "");
|
|
134
|
+
const fields = new Map();
|
|
135
|
+
for (const row of draftRows) {
|
|
136
|
+
const code = codeOf(row);
|
|
137
|
+
if (!code || fields.has(code))
|
|
138
|
+
continue;
|
|
139
|
+
const options = fieldSemanticDescriptor(row).options;
|
|
140
|
+
fields.set(code, { code, name: String(row.name ?? code), type: Number(row.type ?? row.propertyType), published: false, ...(options !== undefined ? { options } : {}), subFields: subFieldsOf(row) });
|
|
141
|
+
}
|
|
142
|
+
for (const row of publishedRows) {
|
|
143
|
+
const code = codeOf(row);
|
|
144
|
+
if (!code)
|
|
145
|
+
continue;
|
|
146
|
+
const options = fieldSemanticDescriptor(row).options;
|
|
147
|
+
fields.set(code, { code, name: String(row.name ?? code), type: Number(row.type ?? row.propertyType), published: true, ...(options !== undefined ? { options } : {}), subFields: subFieldsOf(row) });
|
|
148
|
+
}
|
|
149
|
+
return [...fields.values()];
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The field capability (field families + CHILD_TABLE columns). It owns the
|
|
153
|
+
* whole field lifecycle on the intent face: declaration validation, the
|
|
154
|
+
* authoritative draft/published field read, the idempotent create/update/publish
|
|
155
|
+
* diff, and the wire->family decode. Child-column knowledge lives only here.
|
|
156
|
+
*/
|
|
157
|
+
export const fieldCapability = {
|
|
158
|
+
id: "field",
|
|
159
|
+
title: "字段(含子表)",
|
|
160
|
+
requires: ["model"],
|
|
161
|
+
operations: ["create", "update", "publish"],
|
|
162
|
+
validate(node, draft, ctx) {
|
|
163
|
+
const fieldsNode = node.entries.find((entry) => entry.key === "fields")?.value;
|
|
164
|
+
let fields = [];
|
|
165
|
+
if (fieldsNode !== undefined && !(fieldsNode.kind === "scalar" && fieldsNode.value === null)) {
|
|
166
|
+
if (fieldsNode.kind !== "seq") {
|
|
167
|
+
pushError(ctx, "intent-fields-not-a-list", `fields 必须是列表(得到 ${describe(fieldsNode)})`, `${ctx.base}.fields`, "value");
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const seenFieldCodes = new Set();
|
|
171
|
+
fields = fieldsNode.items
|
|
172
|
+
.map((fieldNode, fieldIndex) => {
|
|
173
|
+
const fieldBase = `${ctx.base}.fields[${fieldIndex}]`;
|
|
174
|
+
if (fieldNode.kind !== "map") {
|
|
175
|
+
pushError(ctx, "intent-field-not-a-map", `字段项必须是映射(得到 ${describe(fieldNode)})`, fieldBase, "value");
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
for (const entry of fieldNode.entries) {
|
|
179
|
+
if (!FIELD_KEYS.has(entry.key))
|
|
180
|
+
pushError(ctx, "intent-key-unknown", FIELD_OPTION_KEYS.has(entry.key)
|
|
181
|
+
? `字段不支持键 "${entry.key}";"${entry.key}" 是字段选项,必须写在 options 下(如 options.${entry.key})。顶层只接受 code / name / type / choices / subFields / options / relativeCode / displayField / absent`
|
|
182
|
+
: `字段不支持键 "${entry.key}"(只接受 code / name / type / choices / subFields / options / relativeCode / displayField / absent)`, `${fieldBase}.${entry.key}`);
|
|
183
|
+
}
|
|
184
|
+
const fieldCodeNode = fieldNode.entries.find((entry) => entry.key === "code")?.value;
|
|
185
|
+
const fieldNameNode = fieldNode.entries.find((entry) => entry.key === "name")?.value;
|
|
186
|
+
const typeNode = fieldNode.entries.find((entry) => entry.key === "type")?.value;
|
|
187
|
+
const choicesNode = fieldNode.entries.find((entry) => entry.key === "choices")?.value;
|
|
188
|
+
const absentNode = fieldNode.entries.find((entry) => entry.key === "absent")?.value;
|
|
189
|
+
const optionsNode = fieldNode.entries.find((entry) => entry.key === "options")?.value;
|
|
190
|
+
const relativeNode = fieldNode.entries.find((entry) => entry.key === "relativeCode")?.value;
|
|
191
|
+
const displayNode = fieldNode.entries.find((entry) => entry.key === "displayField")?.value;
|
|
192
|
+
if (optionsNode !== undefined && optionsNode.kind !== "map")
|
|
193
|
+
pushError(ctx, "intent-field-options-invalid", "字段 options 必须是映射(如 NUMERICAL: { scale: 2 })", `${fieldBase}.options`, "value");
|
|
194
|
+
if (relativeNode !== undefined && !isScalarString(relativeNode))
|
|
195
|
+
pushError(ctx, "intent-field-relativecode-invalid", "字段 relativeCode 必须是非空字符串(目标模型 code)", `${fieldBase}.relativeCode`, "value");
|
|
196
|
+
if (displayNode !== undefined && !isScalarString(displayNode))
|
|
197
|
+
pushError(ctx, "intent-field-displayfield-invalid", "字段 displayField 必须是非空字符串(目标模型展示字段 code)", `${fieldBase}.displayField`, "value");
|
|
198
|
+
let fieldCode = "";
|
|
199
|
+
let fieldType = "";
|
|
200
|
+
if (!isScalarString(fieldCodeNode))
|
|
201
|
+
pushError(ctx, "intent-field-code-invalid", "字段 code 必须是非空字符串", `${fieldBase}.code`, "value");
|
|
202
|
+
else {
|
|
203
|
+
fieldCode = fieldCodeNode.value.trim();
|
|
204
|
+
const violations = validateBusinessCode(fieldCode, { kind: "field" });
|
|
205
|
+
if (violations.length > 0)
|
|
206
|
+
pushError(ctx, "intent-field-code-invalid", `字段 code 未过出门前校验:${violations.map((violation) => violation.message).join(";")}`, `${fieldBase}.code`, "value", { validation: violations });
|
|
207
|
+
if (seenFieldCodes.has(fieldCode))
|
|
208
|
+
pushError(ctx, "intent-field-code-duplicate", `字段 code 在模型内重复:${fieldCode}`, `${fieldBase}.code`, "value");
|
|
209
|
+
seenFieldCodes.add(fieldCode);
|
|
210
|
+
}
|
|
211
|
+
if (absentNode !== undefined && (absentNode.kind !== "scalar" || typeof absentNode.value !== "boolean"))
|
|
212
|
+
pushError(ctx, "intent-field-absent-invalid", "字段 absent 必须是布尔", `${fieldBase}.absent`, "value");
|
|
213
|
+
if (absentNode?.kind === "scalar" && absentNode.value === true) {
|
|
214
|
+
// Declared-absent field: only its code is meaningful. Refuse shape
|
|
215
|
+
// keys rather than silently ignoring them.
|
|
216
|
+
for (const key of FIELD_PRESENT_ONLY_KEYS) {
|
|
217
|
+
if (fieldNode.entries.some((entry) => entry.key === key))
|
|
218
|
+
pushError(ctx, "intent-field-absent-with-shape", `字段声明 absent:true 表示删除该字段,不能再声明 ${key}`, `${fieldBase}.${key}`);
|
|
219
|
+
}
|
|
220
|
+
if (fieldCode.length === 0)
|
|
221
|
+
return null;
|
|
222
|
+
return { code: fieldCode, absent: true };
|
|
223
|
+
}
|
|
224
|
+
if (!isScalarString(fieldNameNode))
|
|
225
|
+
pushError(ctx, "intent-field-name-invalid", "字段 name 必须是非空字符串", `${fieldBase}.name`, "value");
|
|
226
|
+
if (!isScalarString(typeNode)) {
|
|
227
|
+
pushError(ctx, "intent-field-type-unknown", `字段 type 必须是字段族名字符串(得到 ${describe(typeNode)})`, `${fieldBase}.type`, "value");
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
fieldType = typeNode.value.trim();
|
|
231
|
+
if (fieldType in OUT_OF_COVERAGE_FIELD_TYPES) {
|
|
232
|
+
pushError(ctx, "intent-field-type-out-of-coverage", `字段族 ${fieldType} 在覆盖面之外(${OUT_OF_COVERAGE_FIELD_TYPES[fieldType]})——请换用受支持族或走 escape hatch(omc call)`, `${fieldBase}.type`, "value");
|
|
233
|
+
}
|
|
234
|
+
else if (!(fieldType in FIELD_PROPERTY_TYPES)) {
|
|
235
|
+
pushError(ctx, "intent-field-type-unknown", `未知字段族 "${fieldType}"——可用族见 omc commands --json 的 intent.fieldFamilies`, `${fieldBase}.type`, "value");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
let choices;
|
|
239
|
+
if (choicesNode !== undefined && !(choicesNode.kind === "scalar" && choicesNode.value === null)) {
|
|
240
|
+
const validChoices = choicesNode.kind === "seq" && choicesNode.items.every((item) => isScalarString(item) && item.value.trim().length > 0);
|
|
241
|
+
if (!validChoices) {
|
|
242
|
+
pushError(ctx, "intent-field-choices-invalid", "choices 必须是非空字符串列表", `${fieldBase}.choices`, "value");
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
choices = choicesNode.items.map((item) => item.value.trim());
|
|
246
|
+
if (!(fieldType in FIELD_PROPERTY_TYPES) || !CHOICE_FAMILIES.has(fieldType))
|
|
247
|
+
pushError(ctx, "intent-field-choices-unexpected", `choices 只用于选择族(${[...CHOICE_FAMILIES].join(" / ")});字段族 ${fieldType || "(未知)"}不接受`, `${fieldBase}.choices`, "value");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const subFieldsNode = fieldNode.entries.find((entry) => entry.key === "subFields")?.value;
|
|
251
|
+
let subFields;
|
|
252
|
+
if (subFieldsNode !== undefined && !(subFieldsNode.kind === "scalar" && subFieldsNode.value === null)) {
|
|
253
|
+
if (subFieldsNode.kind !== "seq") {
|
|
254
|
+
pushError(ctx, "intent-subfields-not-a-list", "subFields 必须是列表(仅 CHILD_TABLE 字段接受)", `${fieldBase}.subFields`, "value");
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
const seenSubCodes = new Set();
|
|
258
|
+
subFields = subFieldsNode.items
|
|
259
|
+
.map((subNode, subIndex) => {
|
|
260
|
+
const subBase = `${fieldBase}.subFields[${subIndex}]`;
|
|
261
|
+
if (subNode.kind !== "map") {
|
|
262
|
+
pushError(ctx, "intent-subfield-not-a-map", `子列项必须是映射(得到 ${describe(subNode)})`, subBase, "value");
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
for (const entry of subNode.entries) {
|
|
266
|
+
if (!SUB_FIELD_KEYS.has(entry.key))
|
|
267
|
+
pushError(ctx, "intent-key-unknown", `子列不支持键 "${entry.key}"(只接受 code / name / type / choices / relativeCode / options)`, `${subBase}.${entry.key}`);
|
|
268
|
+
}
|
|
269
|
+
const subCodeNode = subNode.entries.find((entry) => entry.key === "code")?.value;
|
|
270
|
+
const subNameNode = subNode.entries.find((entry) => entry.key === "name")?.value;
|
|
271
|
+
const subTypeNode = subNode.entries.find((entry) => entry.key === "type")?.value;
|
|
272
|
+
const subChoicesNode = subNode.entries.find((entry) => entry.key === "choices")?.value;
|
|
273
|
+
const subRelativeNode = subNode.entries.find((entry) => entry.key === "relativeCode")?.value;
|
|
274
|
+
const subOptionsNode = subNode.entries.find((entry) => entry.key === "options")?.value;
|
|
275
|
+
let subCode = "";
|
|
276
|
+
let subType = "";
|
|
277
|
+
if (!isScalarString(subCodeNode))
|
|
278
|
+
pushError(ctx, "intent-subfield-code-invalid", "子列 code 必须是非空字符串", `${subBase}.code`, "value");
|
|
279
|
+
else {
|
|
280
|
+
subCode = subCodeNode.value.trim();
|
|
281
|
+
const violations = validateBusinessCode(subCode, { kind: "field" });
|
|
282
|
+
if (violations.length > 0)
|
|
283
|
+
pushError(ctx, "intent-subfield-code-invalid", `子列 code 未过出门前校验:${violations.map((violation) => violation.message).join(";")}`, `${subBase}.code`, "value", { validation: violations });
|
|
284
|
+
if (seenSubCodes.has(subCode))
|
|
285
|
+
pushError(ctx, "intent-subfield-code-duplicate", `子列 code 在子表内重复:${subCode}`, `${subBase}.code`, "value");
|
|
286
|
+
seenSubCodes.add(subCode);
|
|
287
|
+
}
|
|
288
|
+
if (!isScalarString(subNameNode))
|
|
289
|
+
pushError(ctx, "intent-subfield-name-invalid", "子列 name 必须是非空字符串", `${subBase}.name`, "value");
|
|
290
|
+
if (!isScalarString(subTypeNode)) {
|
|
291
|
+
pushError(ctx, "intent-subfield-type-unknown", `子列 type 必须是字段族名字符串(得到 ${describe(subTypeNode)})`, `${subBase}.type`, "value");
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
subType = subTypeNode.value.trim();
|
|
295
|
+
if (subType === "CHILD_TABLE")
|
|
296
|
+
pushError(ctx, "intent-subfield-type-nested", "子表不能嵌套子表(CHILD_TABLE 不能作为子列)", `${subBase}.type`, "value");
|
|
297
|
+
else if (subType in OUT_OF_COVERAGE_FIELD_TYPES)
|
|
298
|
+
pushError(ctx, "intent-subfield-type-out-of-coverage", `子列族 ${subType} 在覆盖面之外(${OUT_OF_COVERAGE_FIELD_TYPES[subType]})`, `${subBase}.type`, "value");
|
|
299
|
+
else if (!(subType in FIELD_PROPERTY_TYPES))
|
|
300
|
+
pushError(ctx, "intent-subfield-type-unknown", `未知子列族 "${subType}"——可用族见 omc commands --json 的 intent.fieldFamilies`, `${subBase}.type`, "value");
|
|
301
|
+
else if (!CHILD_TABLE_SCALAR_PROPERTY_TYPES.has(FIELD_PROPERTY_TYPES[subType]))
|
|
302
|
+
pushError(ctx, "intent-subfield-type-unsupported", `子列族 ${subType} 不在子表覆盖内(MULT_WORK_SHEET/RELEVANCE_DATA/FORMULA 等见 omc capability field 的说明)`, `${subBase}.type`, "value");
|
|
303
|
+
}
|
|
304
|
+
// Relation child column (WORK_SHEET): relativeCode required.
|
|
305
|
+
// `options` is open to every family since round 4 — the
|
|
306
|
+
// write gate refuses family-invalid shapes (e.g. a
|
|
307
|
+
// NUMERICAL column without options.scale).
|
|
308
|
+
const isRelationChild = subType === "WORK_SHEET";
|
|
309
|
+
if (subRelativeNode !== undefined && !isScalarString(subRelativeNode))
|
|
310
|
+
pushError(ctx, "intent-subfield-relativecode-invalid", "子列 relativeCode 必须是非空字符串(目标模型 code)", `${subBase}.relativeCode`, "value");
|
|
311
|
+
if (subOptionsNode !== undefined && subOptionsNode.kind !== "map")
|
|
312
|
+
pushError(ctx, "intent-subfield-options-invalid", "子列 options 必须是映射", `${subBase}.options`, "value");
|
|
313
|
+
if (isRelationChild && !isScalarString(subRelativeNode))
|
|
314
|
+
pushError(ctx, "intent-subfield-relativecode-required", "子表关联列(WORK_SHEET)必须给 relativeCode(目标模型 code,如 customer)", `${subBase}.relativeCode`, "value");
|
|
315
|
+
if (!isRelationChild && subRelativeNode !== undefined)
|
|
316
|
+
pushError(ctx, "intent-subfield-relation-only", `relativeCode 只用于子表关联列(WORK_SHEET);子列族 ${subType || "(未知)"} 不接受`, subBase, "value");
|
|
317
|
+
let subChoices;
|
|
318
|
+
if (subChoicesNode !== undefined && !(subChoicesNode.kind === "scalar" && subChoicesNode.value === null)) {
|
|
319
|
+
const validSubChoices = subChoicesNode.kind === "seq" && subChoicesNode.items.every((item) => isScalarString(item) && item.value.trim().length > 0);
|
|
320
|
+
if (!validSubChoices)
|
|
321
|
+
pushError(ctx, "intent-subfield-choices-invalid", "choices 必须是非空字符串列表", `${subBase}.choices`, "value");
|
|
322
|
+
else {
|
|
323
|
+
subChoices = subChoicesNode.items.map((item) => item.value.trim());
|
|
324
|
+
if (!(subType in FIELD_PROPERTY_TYPES) || !CHOICE_FAMILIES.has(subType))
|
|
325
|
+
pushError(ctx, "intent-subfield-choices-unexpected", `choices 只用于选择族(${[...CHOICE_FAMILIES].join(" / ")});子列族 ${subType || "(未知)"}不接受`, `${subBase}.choices`, "value");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (subCode.length === 0 || subType.length === 0)
|
|
329
|
+
return null;
|
|
330
|
+
const subOptions = subOptionsNode?.kind === "map" ? toPlain(subOptionsNode) : {};
|
|
331
|
+
if (CHOICE_FAMILIES.has(subType)) {
|
|
332
|
+
for (const misplaced of ["items", "choices"]) {
|
|
333
|
+
if (subOptions[misplaced] !== undefined)
|
|
334
|
+
pushError(ctx, "intent-field-choices-misplaced", `选择族子列(${subType})的可选值写在子列顶层 choices: [A, B, C];options.${misplaced} 不是声明入口(原始形状用 options.optionsSet)`, `${subBase}.options.${misplaced}`, "value");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (subType === "NUMERICAL" && !isNumericalScale(subOptions.scale))
|
|
338
|
+
pushError(ctx, "intent-field-scale-required", "子列 NUMERICAL 必须给 options.scale(小数位数:0=整数,1=1位,2=2位…)", `${subBase}.options.scale`, "value");
|
|
339
|
+
const subName = isScalarString(subNameNode) ? subNameNode.value : "";
|
|
340
|
+
return {
|
|
341
|
+
code: subCode,
|
|
342
|
+
name: subName,
|
|
343
|
+
type: subType,
|
|
344
|
+
...(subChoices ? { choices: subChoices } : {}),
|
|
345
|
+
...(isRelationChild && isScalarString(subRelativeNode) ? { relativeCode: subRelativeNode.value.trim() } : {}),
|
|
346
|
+
...(subOptionsNode?.kind === "map" ? { options: subOptions } : {}),
|
|
347
|
+
};
|
|
348
|
+
})
|
|
349
|
+
.filter((sub) => sub !== null);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (fieldType === "CHILD_TABLE" && (subFields === undefined || subFields.length === 0))
|
|
353
|
+
pushError(ctx, "intent-subfield-required", "CHILD_TABLE 字段必须声明非空 subFields(子列)", `${fieldBase}.subFields`, "value");
|
|
354
|
+
if (fieldType !== "CHILD_TABLE" && subFields !== undefined)
|
|
355
|
+
pushError(ctx, "intent-subfields-unexpected", `subFields 只用于 CHILD_TABLE 字段;字段族 ${fieldType || "(未知)"}不接受`, `${fieldBase}.subFields`, "value");
|
|
356
|
+
// Top-level relation keys are relation-only; refuse them on other
|
|
357
|
+
// families rather than silently ignoring (matches the child column).
|
|
358
|
+
const relationField = RELATION_FAMILIES.has(fieldType);
|
|
359
|
+
const relativeValue = isScalarString(relativeNode) ? relativeNode.value.trim() : undefined;
|
|
360
|
+
const displayValue = isScalarString(displayNode) ? displayNode.value.trim() : undefined;
|
|
361
|
+
if (!relationField && relativeValue !== undefined)
|
|
362
|
+
pushError(ctx, "intent-field-relation-only", `relativeCode 只用于关联族(${[...RELATION_FAMILIES].join(" / ")});字段族 ${fieldType || "(未知)"}不接受`, `${fieldBase}.relativeCode`, "value");
|
|
363
|
+
if (!relationField && displayValue !== undefined)
|
|
364
|
+
pushError(ctx, "intent-field-relation-only", `displayField 只用于关联族(${[...RELATION_FAMILIES].join(" / ")});字段族 ${fieldType || "(未知)"}不接受`, `${fieldBase}.displayField`, "value");
|
|
365
|
+
if (relationField && relativeValue === undefined && !(optionsNode?.kind === "map" && toPlain(optionsNode).relativeCode !== undefined))
|
|
366
|
+
pushError(ctx, "intent-field-relativecode-required", `关联字段(${fieldType})必须给 relativeCode(目标模型 code);可写在字段顶层或 options.relativeCode`, `${fieldBase}.relativeCode`, "value");
|
|
367
|
+
const explicitOptions = optionsNode?.kind === "map" ? toPlain(optionsNode) : {};
|
|
368
|
+
// A choice family's allowed values live in TOP-LEVEL `choices` (the
|
|
369
|
+
// compiler synthesizes the platform's optionsSet). Callers habitually
|
|
370
|
+
// try `options.items` / `options.choices`, which the option passthrough
|
|
371
|
+
// carries along — so it passed validate/plan and only the live
|
|
372
|
+
// field.create refused it (bench40: a RADIO with `options.items` did a
|
|
373
|
+
// partial apply then failed on step 5). Refuse offline and name the
|
|
374
|
+
// real keys: top-level `choices` (compact) or `options.optionsSet`
|
|
375
|
+
// (raw platform shape), not `options.items`.
|
|
376
|
+
if (CHOICE_FAMILIES.has(fieldType)) {
|
|
377
|
+
for (const misplaced of ["items", "choices"]) {
|
|
378
|
+
if (explicitOptions[misplaced] !== undefined)
|
|
379
|
+
pushError(ctx, "intent-field-choices-misplaced", `选择族(${fieldType})的可选值写在字段顶层 choices: [A, B, C](编译器合成平台的 optionsSet);options.${misplaced} 不是声明入口(可选项也可直接用原始形状 options.optionsSet)`, `${fieldBase}.options.${misplaced}`, "value");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const mergedOptions = {
|
|
383
|
+
...explicitOptions,
|
|
384
|
+
...(relativeValue !== undefined ? { relativeCode: relativeValue } : {}),
|
|
385
|
+
...(displayValue !== undefined ? { displayField: displayValue } : {}),
|
|
386
|
+
};
|
|
387
|
+
// Offline parity with `field.create`'s write gate (assertNumericalScale):
|
|
388
|
+
// a NUMERICAL without a valid options.scale passes plan but is refused at
|
|
389
|
+
// apply, costing a wasted apply + replan (dogfood bench51). Catch it in
|
|
390
|
+
// plan instead.
|
|
391
|
+
if (fieldType === "NUMERICAL" && !isNumericalScale(mergedOptions.scale))
|
|
392
|
+
pushError(ctx, "intent-field-scale-required", "NUMERICAL 字段必须给 options.scale(小数位数:0=整数,1=1位,2=2位…);缺失时门户设计器显示为裸 decimal,且 apply 的 field.create 会拒绝", `${fieldBase}.options.scale`, "value");
|
|
393
|
+
if (fieldCode.length === 0 || fieldType.length === 0)
|
|
394
|
+
return null;
|
|
395
|
+
const fieldName = isScalarString(fieldNameNode) ? fieldNameNode.value : "";
|
|
396
|
+
return { code: fieldCode, name: fieldName, type: fieldType, ...(choices ? { choices } : {}), ...(Object.keys(mergedOptions).length > 0 ? { options: mergedOptions } : {}), ...(subFields ? { subFields } : {}) };
|
|
397
|
+
})
|
|
398
|
+
.filter((field) => field !== null);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
draft.fields = fields;
|
|
402
|
+
},
|
|
403
|
+
async read(session, _intent, draft) {
|
|
404
|
+
if (!draft.present)
|
|
405
|
+
return;
|
|
406
|
+
draft.fields = await readFields(session, draft.code);
|
|
407
|
+
},
|
|
408
|
+
plan(intent, baseline, emit) {
|
|
409
|
+
const existing = new Map(baseline.fields.map((field) => [field.code, field]));
|
|
410
|
+
let childChanged = false;
|
|
411
|
+
for (const field of intent.fields) {
|
|
412
|
+
const current = existing.get(field.code);
|
|
413
|
+
// Declared-absent field: delete it when present (draft OR published), and
|
|
414
|
+
// no-op when already gone — so a converged retry is zero-change.
|
|
415
|
+
if (field.absent === true) {
|
|
416
|
+
if (current !== undefined) {
|
|
417
|
+
emit.change("field.delete", { schemaCode: intent.code, code: field.code }, `field:${intent.code}.${field.code}`, "field-declared-absent");
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
emit.skip(`field:${intent.code}.${field.code}`, "field-already-absent");
|
|
421
|
+
}
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (current === undefined) {
|
|
425
|
+
const relativeCode = relationCodeOf(field);
|
|
426
|
+
// `relativeCode` is lifted to the row (see relationCodeOf) — drop it from
|
|
427
|
+
// the options payload so only the platform keys ride the wire.
|
|
428
|
+
const { relativeCode: _relativeCode, ...relationOptions } = (field.options ?? {});
|
|
429
|
+
const options = Object.keys(relationOptions).length > 0 ? relationOptions : undefined;
|
|
430
|
+
emit.change("field.create", { schemaCode: intent.code, code: field.code, name: field.name, propertyType: field.type, ...(field.choices ? { choices: field.choices } : {}), ...(relativeCode ? { relativeCode } : {}), ...(options ? { options } : {}), ...(field.subFields ? { subFields: subFieldArgs(field.subFields) } : {}) }, `field:${intent.code}.${field.code}`, "field-absent");
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const currentFamily = familyOf(current.type) ?? `wire:${current.type}`;
|
|
434
|
+
if (currentFamily !== field.type) {
|
|
435
|
+
refuse("intent-field-type-drift", `字段 ${intent.code}.${field.code} 已存在且类型为 ${currentFamily}(wire ${current.type}),意图声明 ${field.type}——字段族创建后不可迁移。修正意图类型,或走 escape hatch(omc call field.update 的受支持面)处理特殊场景`, { resource: `field:${intent.code}.${field.code}`, existingType: currentFamily, intentType: field.type });
|
|
436
|
+
}
|
|
437
|
+
// CHILD_TABLE(8) child columns are ordinary fields on the child schema
|
|
438
|
+
// (schemaCode = the parent field code). Diff them against the parent's
|
|
439
|
+
// authoritative subSchema; extra live columns are ownership-ignored.
|
|
440
|
+
if (field.type === "CHILD_TABLE") {
|
|
441
|
+
const liveChildren = new Map(current.subFields.map((child) => [child.code, child]));
|
|
442
|
+
for (const sub of field.subFields ?? []) {
|
|
443
|
+
const live = liveChildren.get(sub.code);
|
|
444
|
+
const resource = `subfield:${intent.code}.${field.code}.${sub.code}`;
|
|
445
|
+
if (live === undefined) {
|
|
446
|
+
emit.change("field.create", { schemaCode: field.code, code: sub.code, name: sub.name, propertyType: sub.type, ...(sub.choices ? { choices: sub.choices } : {}), ...(sub.options ? { options: sub.options } : {}) }, resource, "child-field-absent");
|
|
447
|
+
childChanged = true;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const liveFamily = familyOf(live.type) ?? `wire:${live.type}`;
|
|
451
|
+
if (liveFamily !== sub.type)
|
|
452
|
+
refuse("intent-child-field-type-drift", `子列 ${intent.code}.${field.code}.${sub.code} 已存在且类型为 ${liveFamily}(wire ${live.type}),意图声明 ${sub.type}——子列族创建后不可迁移;修正意图类型或走 escape hatch`, { resource, existingType: liveFamily, intentType: sub.type });
|
|
453
|
+
if (live.name !== sub.name) {
|
|
454
|
+
emit.change("field.update", { schemaCode: field.code, code: sub.code, name: sub.name }, resource, "child-field-name-drift");
|
|
455
|
+
childChanged = true;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (current.name !== field.name) {
|
|
460
|
+
emit.change("field.update", { schemaCode: intent.code, code: field.code, name: field.name }, `field:${intent.code}.${field.code}`, "field-name-drift");
|
|
461
|
+
}
|
|
462
|
+
else if (current.published) {
|
|
463
|
+
emit.skip(`field:${intent.code}.${field.code}`, "field-present-published");
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
emit.skip(`field:${intent.code}.${field.code}`, "field-present-draft");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Model-scoped publish: only when a requested code is missing from the
|
|
470
|
+
// published face, or a child-table column changed (child schemas publish
|
|
471
|
+
// with the model); the tool's own already-published short-circuit plus the
|
|
472
|
+
// codes list keeps retries idempotent. Declared-absent fields are excluded —
|
|
473
|
+
// they are being deleted, not published.
|
|
474
|
+
const requestedCodes = intent.fields.filter((field) => field.absent !== true).map((field) => field.code);
|
|
475
|
+
const publishedNow = new Set(baseline.fields.filter((field) => field.published).map((field) => field.code));
|
|
476
|
+
if (requestedCodes.length > 0 && (childChanged || requestedCodes.some((code) => !publishedNow.has(code)))) {
|
|
477
|
+
emit.change("field.publish", { schemaCode: intent.code, codes: requestedCodes }, `model:${intent.code}`, childChanged ? "child-columns-changed" : "fields-not-published");
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
decode(baseline, draft) {
|
|
481
|
+
draft.fields = baseline.fields.map((field) => ({
|
|
482
|
+
code: field.code,
|
|
483
|
+
name: field.name ?? null,
|
|
484
|
+
type: familyOf(field.type) ?? null,
|
|
485
|
+
published: field.published,
|
|
486
|
+
...(field.options !== undefined ? { options: field.options } : {}),
|
|
487
|
+
subFields: field.subFields.map((sub) => ({ code: sub.code, name: sub.name ?? null, type: familyOf(sub.type) ?? null, published: field.published })),
|
|
488
|
+
}));
|
|
489
|
+
},
|
|
490
|
+
compare(intent, live, emit) {
|
|
491
|
+
if (!live.present) {
|
|
492
|
+
// Whole model gone: declared-absent fields are satisfied (already gone);
|
|
493
|
+
// declared-present fields are missing with the model.
|
|
494
|
+
for (const field of intent.fields)
|
|
495
|
+
if (field.absent !== true)
|
|
496
|
+
emit.difference("field-missing", `field:${intent.code}.${field.code}`, `字段 ${intent.code}.${field.code} 随模型缺失`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const liveFields = new Map(live.fields.map((field) => [field.code, field]));
|
|
500
|
+
for (const field of intent.fields) {
|
|
501
|
+
const current = liveFields.get(field.code);
|
|
502
|
+
// Declared-absent field: equivalent only when it is really gone.
|
|
503
|
+
if (field.absent === true) {
|
|
504
|
+
if (current !== undefined)
|
|
505
|
+
emit.difference("field-present-unexpected", `field:${intent.code}.${field.code}`, `字段 ${intent.code}.${field.code} 声明 absent:true 但仍存在`);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (current === undefined) {
|
|
509
|
+
emit.difference("field-missing", `field:${intent.code}.${field.code}`, `字段 ${intent.code}.${field.code} 不存在`);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (current.type !== null && current.type !== field.type)
|
|
513
|
+
emit.difference("field-type-mismatch", `field:${intent.code}.${field.code}`, `字段类型 live=${current.type} intent=${field.type}`);
|
|
514
|
+
if (current.name !== null && current.name !== field.name)
|
|
515
|
+
emit.difference("field-name-mismatch", `field:${intent.code}.${field.code}`, `字段名 live=${current.name} intent=${field.name}`);
|
|
516
|
+
compareFieldOptions(intent.code, field, current, emit);
|
|
517
|
+
if (!current.published)
|
|
518
|
+
emit.difference("field-not-published", `field:${intent.code}.${field.code}`, `字段 ${intent.code}.${field.code} 未发布`);
|
|
519
|
+
if (field.type === "CHILD_TABLE" && (field.subFields?.length ?? 0) > 0) {
|
|
520
|
+
const liveChildren = new Map(current.subFields.map((sub) => [sub.code, sub]));
|
|
521
|
+
const declaredChildren = new Set(field.subFields.map((sub) => sub.code));
|
|
522
|
+
for (const sub of field.subFields) {
|
|
523
|
+
const liveChild = liveChildren.get(sub.code);
|
|
524
|
+
const resource = `subfield:${intent.code}.${field.code}.${sub.code}`;
|
|
525
|
+
if (liveChild === undefined) {
|
|
526
|
+
emit.difference("child-field-missing", resource, `子列 ${intent.code}.${field.code}.${sub.code} 不存在`);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (liveChild.type !== null && liveChild.type !== sub.type)
|
|
530
|
+
emit.difference("child-field-type-mismatch", resource, `子列类型 live=${liveChild.type} intent=${sub.type}`);
|
|
531
|
+
if (liveChild.name !== null && liveChild.name !== sub.name)
|
|
532
|
+
emit.difference("child-field-name-mismatch", resource, `子列名 live=${liveChild.name} intent=${sub.name}`);
|
|
533
|
+
}
|
|
534
|
+
for (const liveChild of current.subFields)
|
|
535
|
+
if (!declaredChildren.has(liveChild.code))
|
|
536
|
+
emit.ignored(`subfield:${intent.code}.${field.code}.${liveChild.code}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const intentFieldCodes = new Set(intent.fields.map((field) => field.code));
|
|
540
|
+
for (const current of live.fields)
|
|
541
|
+
if (!intentFieldCodes.has(current.code))
|
|
542
|
+
emit.ignored(`field:${intent.code}.${current.code}`);
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
//# sourceMappingURL=field.js.map
|