@stalfh233/omc-cli 0.6.17 → 0.6.19
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/dist/batch-call.js +21 -2
- package/dist/cli-stdout.js +1 -0
- package/dist/cli.js +24 -14
- package/dist/field-change.js +7 -1
- package/dist/field-families.js +67 -2
- package/dist/graph.js +31 -7
- package/dist/lanes/app.js +17 -4
- package/dist/lanes/field.js +120 -5
- package/dist/lanes/folder.js +29 -2
- package/dist/lanes/model-contract.js +1 -1
- package/dist/lanes/model.js +51 -29
- package/dist/lanes/online-js-contract.js +1 -1
- package/dist/lanes/online-js.js +41 -9
- package/dist/lanes/programming.js +67 -14
- package/dist/lanes/runtime-data.js +153 -16
- package/dist/mcp-graph.js +32 -13
- package/dist/programming-workflow.js +16 -3
- package/dist/read-shapes.js +3 -3
- package/dist/residue.js +116 -0
- package/dist/rollback.js +54 -19
- package/dist/write.js +6 -2
- package/package.json +3 -3
|
@@ -7,6 +7,7 @@ import { writeTool } from "../write.js";
|
|
|
7
7
|
import { defineTool, ToolRefusalError } from "../tool-types.js";
|
|
8
8
|
import { wireCall, WireError } from "../wire.js";
|
|
9
9
|
import { PROGRAMMING_LANE_CONTRACT, PROGRAMMING_WIRE } from "./programming-contract.js";
|
|
10
|
+
import { readModelRecord } from "../model-record.js";
|
|
10
11
|
const LANE = PROGRAMMING_LANE_CONTRACT.lane;
|
|
11
12
|
const TOOLS = PROGRAMMING_LANE_CONTRACT.tools;
|
|
12
13
|
/** metadata.app.CodeType is the NUMERIC index on the wire (8.6.26 source). */
|
|
@@ -60,7 +61,8 @@ async function readSource(session, args) {
|
|
|
60
61
|
const model = asRecord(response.data);
|
|
61
62
|
if (model === null)
|
|
62
63
|
return { present: false, provisioned: false, model: null };
|
|
63
|
-
|
|
64
|
+
// get_source can return a generated template without a persisted source row.
|
|
65
|
+
return { present: typeof model.version === "number", provisioned: typeof model.version === "number", model };
|
|
64
66
|
}
|
|
65
67
|
catch (error) {
|
|
66
68
|
if (error instanceof WireError && error.kind === "business" && /PROGRAMMING_SOURCE_NONEXISTENT|源码不存在|source.*nonexistent/i.test(error.message))
|
|
@@ -102,6 +104,13 @@ function assertTemplateShape(content, code) {
|
|
|
102
104
|
});
|
|
103
105
|
}
|
|
104
106
|
}
|
|
107
|
+
function assertProgrammingName(name) {
|
|
108
|
+
if (name === undefined)
|
|
109
|
+
return;
|
|
110
|
+
if (typeof name !== "string" || !/^[A-Za-z][A-Za-z0-9_ -]{0,127}$/.test(name)) {
|
|
111
|
+
throw new ToolRefusalError({ status: "refused", blocker: "programming-name-invalid", message: "businessRuleName 只能以字母开头,并只含字母、数字、下划线、空格或连字符(最多 128 字符)。" });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
105
114
|
async function listRules(session, schemaCode) {
|
|
106
115
|
const response = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode } });
|
|
107
116
|
return rowsOf(response.data);
|
|
@@ -223,6 +232,17 @@ function sourceRollback() {
|
|
|
223
232
|
},
|
|
224
233
|
};
|
|
225
234
|
}
|
|
235
|
+
function publishRollback() {
|
|
236
|
+
return {
|
|
237
|
+
target: (args) => `programming.publish:${String(args.schemaCode ?? "")}/${String(args.businessRuleCode ?? "")}`,
|
|
238
|
+
// A published version has no authoritative read face and cannot be
|
|
239
|
+
// restored by replaying the draft. Deliberately fail before-capture so the
|
|
240
|
+
// shared journal records rollbackable:false rather than implying a draft
|
|
241
|
+
// restore would undo the runtime version.
|
|
242
|
+
read: async () => { throw new Error("published version has no rollbackable read face"); },
|
|
243
|
+
inverse: () => ({ kind: "irreversible", reason: "无图形化回退端点;删除规则才会连源码一起删" }),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
226
246
|
export function registerProgrammingLane(tools) {
|
|
227
247
|
tools.push(defineTool({
|
|
228
248
|
name: "programming.schemas",
|
|
@@ -310,12 +330,14 @@ export function registerProgrammingLane(tools) {
|
|
|
310
330
|
},
|
|
311
331
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
312
332
|
const read = await readSource(session, args);
|
|
333
|
+
const model = await readModelRecord(session, args.schemaCode);
|
|
313
334
|
const content = typeof read.model?.content === "string" ? read.model.content : null;
|
|
314
335
|
return {
|
|
315
336
|
schemaCode: args.schemaCode,
|
|
316
337
|
businessRuleCode: args.businessRuleCode,
|
|
317
338
|
code: String(read.model?.code ?? args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode)),
|
|
318
339
|
present: read.present,
|
|
340
|
+
modelExists: model !== null,
|
|
319
341
|
provisioned: read.provisioned,
|
|
320
342
|
className: read.model?.className ?? null,
|
|
321
343
|
codeType: read.model?.codeType ?? null,
|
|
@@ -326,13 +348,15 @@ export function registerProgrammingLane(tools) {
|
|
|
326
348
|
contentBytes: content === null ? null : Buffer.byteLength(content, "utf8"),
|
|
327
349
|
// r28j F7: --search "bizObject" 被写危害文档淹没;API 骨架入口钉在这里。
|
|
328
350
|
apiHint: 'BizObject 读写骨架:omc help omc-backend-online-coding --section "5. 典型 handler 骨架"',
|
|
329
|
-
...(
|
|
330
|
-
? {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
351
|
+
...(model === null
|
|
352
|
+
? { hint: `模型 ${args.schemaCode} 不存在;get_source 返回的只是平台模板骨架,不代表源码或模型存在。先用 model.list 核对 schemaCode。` }
|
|
353
|
+
: read.provisioned
|
|
354
|
+
? {
|
|
355
|
+
// Draft face always reports published:false after publish too —
|
|
356
|
+
// do not let callers treat this boolean as the publish verdict (F11).
|
|
357
|
+
publishedNote: "本面 published 来自 version=0 草稿行;发布是否生效请看 programming.publish 的 readback.runtimeEffective / developmentMode+enabled,不要看本字段。",
|
|
358
|
+
}
|
|
359
|
+
: { hint: "未落库草稿:content 是平台按规则类型生成的模板骨架(version:null)。改完后 programming.save 落库,programming.publish 生效。" }),
|
|
336
360
|
};
|
|
337
361
|
}, {
|
|
338
362
|
target: `programming.get:${args.schemaCode}/${args.businessRuleCode}`,
|
|
@@ -442,10 +466,20 @@ export function registerProgrammingLane(tools) {
|
|
|
442
466
|
},
|
|
443
467
|
preflight: (args) => {
|
|
444
468
|
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
469
|
+
assertProgrammingName(args.businessRuleName);
|
|
445
470
|
assertTemplateShape(args.content, code);
|
|
446
471
|
},
|
|
447
|
-
dryRunPayload: async (
|
|
472
|
+
dryRunPayload: async (session, args) => {
|
|
448
473
|
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
474
|
+
const rule = await ruleOf(session, args.schemaCode, args.businessRuleCode);
|
|
475
|
+
if (!rule) {
|
|
476
|
+
throw new ToolRefusalError({
|
|
477
|
+
status: "refused",
|
|
478
|
+
blocker: "programming-rule-absent",
|
|
479
|
+
message: `业务规则 ${args.schemaCode}/${args.businessRuleCode} 不存在;save 不会凭空创建规则行。先用 rule.save(或 rule.list)确认/创建规则,再保存源码。`,
|
|
480
|
+
nextCommand: `omc call rule.save --env local --args '{"schemaCode":"${args.schemaCode}","ruleCode":"${args.businessRuleCode}"}'`,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
449
483
|
return {
|
|
450
484
|
code,
|
|
451
485
|
contentSha256: sha256(args.content),
|
|
@@ -538,6 +572,8 @@ export function registerProgrammingLane(tools) {
|
|
|
538
572
|
const publishStatusRaw = readbackRule?.publishStatus;
|
|
539
573
|
return {
|
|
540
574
|
performed: { errcode: 0, code, contentSha256: sha256(content), contentBytes: Buffer.byteLength(content, "utf8"), compiled: true },
|
|
575
|
+
rollbackReceipt: { supported: false, reason: "无图形化回退端点;rollback plan 亦标 irreversible" },
|
|
576
|
+
runtimeVerification: { note: "自定义规则无调用响应体读面;验证须写副作用后用 data.load 读取" },
|
|
541
577
|
readback: {
|
|
542
578
|
present: readbackSource.present,
|
|
543
579
|
contentMatches: readbackContent === content,
|
|
@@ -554,19 +590,36 @@ export function registerProgrammingLane(tools) {
|
|
|
554
590
|
// so it cannot contradict runtimeEffective:"published".
|
|
555
591
|
...(readbackSource.model?.published === true ? { publishedFlag: true } : { publishedFlagNote: "get_source.published 读的是 version=0 草稿行,发布后仍常为 false;不是发布成败判据(published 内容无 wire 读端点)" }),
|
|
556
592
|
note: "运行时执行的是 published 最新版;published 内容无 wire 读取端点,回滚靠本地 git + 重新 publish。判发布是否生效:authoritative=developmentMode+enabled(1/CODING + true)。",
|
|
593
|
+
rollback: { supported: false, reason: "无图形化回退端点;删除规则才会连源码一起删" },
|
|
594
|
+
runtimeVerification: { note: "自定义规则无调用响应体读面;须写副作用(如字段赋值)后 data.load 验证" },
|
|
557
595
|
},
|
|
558
596
|
};
|
|
559
597
|
},
|
|
560
598
|
preflight: (args) => {
|
|
561
599
|
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
600
|
+
assertProgrammingName(args.businessRuleName);
|
|
562
601
|
if (args.content !== undefined)
|
|
563
602
|
assertTemplateShape(args.content, code);
|
|
564
603
|
},
|
|
565
|
-
dryRunPayload: async (
|
|
566
|
-
code
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
604
|
+
dryRunPayload: async (session, args) => {
|
|
605
|
+
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
606
|
+
const rule = await ruleOf(session, args.schemaCode, args.businessRuleCode);
|
|
607
|
+
if (!rule)
|
|
608
|
+
throw new ToolRefusalError({ status: "refused", blocker: "programming-rule-absent", message: `业务规则 ${args.schemaCode}/${args.businessRuleCode} 不存在:publish 必须携带 businessRule 整模型。` });
|
|
609
|
+
let content = args.content;
|
|
610
|
+
if (content === undefined) {
|
|
611
|
+
const read = await readSource(session, { schemaCode: args.schemaCode, businessRuleCode: args.businessRuleCode });
|
|
612
|
+
content = typeof read.model?.content === "string" ? read.model.content : undefined;
|
|
613
|
+
}
|
|
614
|
+
if (content === undefined || content.trim() === "")
|
|
615
|
+
throw new ToolRefusalError({ status: "refused", blocker: "programming-content-required", message: "没有可发布的源码:传 content,或先 programming.save 落库草稿。" });
|
|
616
|
+
assertTemplateShape(content, code);
|
|
617
|
+
const compiled = await compileSource(session, code, content);
|
|
618
|
+
if (!compiled.success)
|
|
619
|
+
throw new ToolRefusalError({ status: "refused", blocker: "programming-compile-failed", message: "编译失败,拒绝 dry-run(真写也会拒绝发布)。", errors: compiled.presented.errors ?? compiled.errors, userErrors: compiled.presented.userErrors, platformErrors: compiled.presented.platformErrors });
|
|
620
|
+
return { code, contentSha256: sha256(content), contentBytes: Buffer.byteLength(content, "utf8"), note: "dry-run 与真写执行相同的规则存在、模板形状、编译 preflight;不会写入或切 CODING。" };
|
|
621
|
+
},
|
|
622
|
+
rollback: publishRollback(),
|
|
570
623
|
});
|
|
571
624
|
}
|
|
572
625
|
export { PROGRAMMING_WIRE };
|
|
@@ -14,6 +14,7 @@ import { getReplayToken, listInstanceActivities, searchWorkItems, wireString } f
|
|
|
14
14
|
import { PLATFORM_ADMIN_START_BYPASS, resolvePermissionActor } from "../identity.js";
|
|
15
15
|
import { ENGINE_OWNED_KEYS, RUNTIME_DATA_LANE_CONTRACT, RUNTIME_DATA_WIRE, runtimeFormDataArg, } from "./runtime-data-contract.js";
|
|
16
16
|
import { SYSTEM_FIELD_CODES } from "../field-families.js";
|
|
17
|
+
import { applicationResolutionOf, resolveApplicationScope } from "../support.js";
|
|
17
18
|
/** r29b F8: keep ids/itemsPreview through governedRead's compact diet. */
|
|
18
19
|
const DATA_LIST_PREVIEW_LIMIT = 20;
|
|
19
20
|
function dataListItemPreview(items) {
|
|
@@ -371,6 +372,106 @@ function readbackWarningFor(emptyFields, toolName) {
|
|
|
371
372
|
return undefined;
|
|
372
373
|
return `${toolName} 已 DELIVERED,但即时读回里这些本次提交的字段为空/缺省:${emptyFields.join("、")}。平台 form/load 在写事务后可能先回系统字段、业务字段滞后(真机 1.1.25,r14 见过只回 modifiedTime 的一笔)。**先 omc call data.load 核对,不要重写**;若 data.load 仍为空,才按投影/字段 code 排查。`;
|
|
373
374
|
}
|
|
375
|
+
function parseOptions(value) {
|
|
376
|
+
if (typeof value === "string") {
|
|
377
|
+
try {
|
|
378
|
+
const parsed = JSON.parse(value);
|
|
379
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return {};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
386
|
+
}
|
|
387
|
+
function choiceCodesFromFieldOptions(field, options) {
|
|
388
|
+
const asCodes = (value) => {
|
|
389
|
+
if (!Array.isArray(value))
|
|
390
|
+
return [];
|
|
391
|
+
return value
|
|
392
|
+
.map((entry) => {
|
|
393
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
394
|
+
const row = entry;
|
|
395
|
+
return String(row.code ?? row.value ?? row.key ?? "");
|
|
396
|
+
}
|
|
397
|
+
return String(entry ?? "");
|
|
398
|
+
})
|
|
399
|
+
.filter((code) => code.length > 0);
|
|
400
|
+
};
|
|
401
|
+
const direct = asCodes(field.choices);
|
|
402
|
+
if (direct.length > 0)
|
|
403
|
+
return direct;
|
|
404
|
+
for (const candidate of [options.choices, options.items]) {
|
|
405
|
+
const codes = asCodes(candidate);
|
|
406
|
+
if (codes.length > 0)
|
|
407
|
+
return codes;
|
|
408
|
+
}
|
|
409
|
+
const set = options.optionsSet;
|
|
410
|
+
if (!set || typeof set !== "object" || Array.isArray(set))
|
|
411
|
+
return [];
|
|
412
|
+
const optionSet = set;
|
|
413
|
+
for (const candidate of [optionSet.custom, optionSet.choices, optionSet.items, optionSet.useDictionariesData]) {
|
|
414
|
+
const codes = asCodes(candidate);
|
|
415
|
+
if (codes.length > 0)
|
|
416
|
+
return codes;
|
|
417
|
+
}
|
|
418
|
+
const dictionary = optionSet.dictionary;
|
|
419
|
+
if (dictionary && typeof dictionary === "object" && !Array.isArray(dictionary)) {
|
|
420
|
+
const codes = asCodes(dictionary.useDictionariesData);
|
|
421
|
+
if (codes.length > 0)
|
|
422
|
+
return codes;
|
|
423
|
+
}
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Runtime/form/save deliberately remains permissive: the platform stores the
|
|
428
|
+
* row even when the portal would reject it. These are advisory findings only.
|
|
429
|
+
* In particular, never manufacture a warning for a field which is absent from
|
|
430
|
+
* the published field face (the old `code/required` ghost warning did exactly
|
|
431
|
+
* that).
|
|
432
|
+
*/
|
|
433
|
+
async function runtimeDataWarnings(session, schemaCode, sheetCode, formData, row = 1) {
|
|
434
|
+
const warnings = [];
|
|
435
|
+
let fields = [];
|
|
436
|
+
try {
|
|
437
|
+
fields = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return warnings;
|
|
441
|
+
}
|
|
442
|
+
const byCode = new Map(fields.map((field) => [String(field.code ?? field.propertyCode ?? ""), field]));
|
|
443
|
+
let required = new Set();
|
|
444
|
+
try {
|
|
445
|
+
const sheet = ((await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode, sheetCode } })).data ?? {});
|
|
446
|
+
const face = typeof sheet.publishedAttributesJson === "string" ? sheet.publishedAttributesJson : typeof sheet.draftAttributesJson === "string" ? sheet.draftAttributesJson : undefined;
|
|
447
|
+
if (face) {
|
|
448
|
+
const controls = JSON.parse(face);
|
|
449
|
+
required = new Set(Object.entries(controls).filter(([, control]) => control?.options?.required === true || typeof control?.options?.requiredFormula === "string").map(([code]) => code));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// Advisory diagnostics must never affect the write.
|
|
454
|
+
}
|
|
455
|
+
for (const [field, value] of Object.entries(formData)) {
|
|
456
|
+
const meta = byCode.get(field);
|
|
457
|
+
if (!meta)
|
|
458
|
+
continue;
|
|
459
|
+
if ((required.has(field) || parseOptions(meta.options).required === true) && (value === null || value === undefined || (typeof value === "string" && value.trim() === ""))) {
|
|
460
|
+
warnings.push({ code: "required-empty", row, fieldCode: field, message: `字段 ${field} 声明必填但本行为空;平台照存,OMC 仅提示` });
|
|
461
|
+
}
|
|
462
|
+
const options = parseOptions(meta.options);
|
|
463
|
+
const optionSet = options.optionsSet && typeof options.optionsSet === "object" ? options.optionsSet : {};
|
|
464
|
+
const choiceCodes = choiceCodesFromFieldOptions(meta, options);
|
|
465
|
+
if (choiceCodes.length > 0 && value !== null && value !== undefined && value !== "") {
|
|
466
|
+
const allowed = new Set(choiceCodes);
|
|
467
|
+
const values = Array.isArray(value) ? value : [value];
|
|
468
|
+
const invalid = values.filter((entry) => !allowed.has(String(entry)));
|
|
469
|
+
if (invalid.length > 0)
|
|
470
|
+
warnings.push({ code: "choice-out-of-range", row, fieldCode: field, value: invalid.map(String), allowed: choiceCodes, message: `字段 ${field} 含未声明选项(实际值:${invalid.map(String).join("、")};合法集:${choiceCodes.join("、")});平台照存,OMC 仅提示` });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return warnings;
|
|
474
|
+
}
|
|
374
475
|
function projectApprovalReadback(record, submittedKeys) {
|
|
375
476
|
if (record === null || typeof record !== "object" || Array.isArray(record))
|
|
376
477
|
return {};
|
|
@@ -979,6 +1080,8 @@ export function registerRuntimeDataLane(tools) {
|
|
|
979
1080
|
throw error;
|
|
980
1081
|
}
|
|
981
1082
|
return governedRead(ctx, self, args, async (session) => {
|
|
1083
|
+
const applicationScope = await resolveApplicationScope(session);
|
|
1084
|
+
const application = { applicationCode: applicationScope.code, applicationResolution: applicationResolutionOf(applicationScope) };
|
|
982
1085
|
// A2:DSL 过滤命中的单选/字典字段(total:0 时给值写法提示)
|
|
983
1086
|
let choiceFilterFields = [];
|
|
984
1087
|
// Published field metadata is the sole source for filter propertyType
|
|
@@ -1103,7 +1206,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1103
1206
|
const firstPage = extractBizObjectPage(first.data);
|
|
1104
1207
|
const total = Number(firstPage.total ?? 0);
|
|
1105
1208
|
if (total === 0 || firstPage.content.length === 0)
|
|
1106
|
-
return { schemaCode: args.schemaCode, total: 0, page: "all", size: 0, pagesFetched: 0, items: [], itemsCount: 0, itemsReturned: 0, resultTruncated: false, itemsExternalized: false, ...(choiceFilterFields.length > 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}) };
|
|
1209
|
+
return { schemaCode: args.schemaCode, ...application, total: 0, page: "all", size: 0, pagesFetched: 0, items: [], itemsCount: 0, itemsReturned: 0, resultTruncated: false, itemsExternalized: false, ...(choiceFilterFields.length > 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}) };
|
|
1107
1210
|
const rows = [...firstPage.content];
|
|
1108
1211
|
const target = Math.min(total, CAP);
|
|
1109
1212
|
let pageIndex = 1;
|
|
@@ -1148,6 +1251,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1148
1251
|
const preview = dataListItemPreview(items);
|
|
1149
1252
|
return {
|
|
1150
1253
|
schemaCode: args.schemaCode,
|
|
1254
|
+
...application,
|
|
1151
1255
|
total,
|
|
1152
1256
|
page: "all",
|
|
1153
1257
|
size: collected.length,
|
|
@@ -1155,7 +1259,11 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1155
1259
|
// 与普通读同一个口径(r28b 真机):草稿策略必须**从结果集实际统计**,
|
|
1156
1260
|
// 不能按"有没有查询条件"静态声明——静态声明会与 items 自相矛盾。
|
|
1157
1261
|
filter: (() => {
|
|
1158
|
-
const draftRows =
|
|
1262
|
+
const draftRows = collected.filter((row) => {
|
|
1263
|
+
const record = row;
|
|
1264
|
+
const data = record.data && typeof record.data === "object" ? record.data : record;
|
|
1265
|
+
return String(data.sequenceStatus ?? record.sequenceStatus ?? "") === "DRAFT";
|
|
1266
|
+
}).length;
|
|
1159
1267
|
return draftRows > 0
|
|
1160
1268
|
? { draftPolicy: "included", draftRows, hint: `本次结果含 ${draftRows} 条 DRAFT 行——total 含草稿,不要当"已提交数据量";判定状态读 data.load 的 sequenceStatus` }
|
|
1161
1269
|
: { draftPolicy: "unspecified", hint: "本次结果无 DRAFT 行;判定状态以 data.load 的 sequenceStatus 为准" };
|
|
@@ -1187,7 +1295,26 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1187
1295
|
}
|
|
1188
1296
|
catch (error) {
|
|
1189
1297
|
if (error instanceof WireError && error.kind === "business" && error.errcode === RUNTIME_DATA_WIRE.emptyModelErrcode) {
|
|
1190
|
-
return { schemaCode: args.schemaCode, total: 0, totalPages: 0, page: args.page ?? 0, size: args.size ?? 10, items: [], ...(choiceFilterFields.length > 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}) };
|
|
1298
|
+
return { schemaCode: args.schemaCode, ...application, total: 0, totalPages: 0, page: args.page ?? 0, size: args.size ?? 10, items: [], ...(choiceFilterFields.length > 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}) };
|
|
1299
|
+
}
|
|
1300
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === 303001) {
|
|
1301
|
+
let available = [];
|
|
1302
|
+
try {
|
|
1303
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.listQueryList, query: { schemaCode: args.schemaCode, clientType: "PC" } })).data);
|
|
1304
|
+
available = rows.map((row) => ({ code: row.code ?? null, name: row.name ?? null }));
|
|
1305
|
+
}
|
|
1306
|
+
catch {
|
|
1307
|
+
available = [];
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
status: "refused",
|
|
1311
|
+
blocker: "query-view-not-found",
|
|
1312
|
+
errcode: 303001,
|
|
1313
|
+
schemaCode: args.schemaCode,
|
|
1314
|
+
queryCode: args.queryCode ?? args.schemaCode,
|
|
1315
|
+
available,
|
|
1316
|
+
message: available.length > 0 ? `列表视图不存在:${String(args.queryCode ?? args.schemaCode)};可用视图见 available` : `列表视图不存在:${String(args.queryCode ?? args.schemaCode)};该模型无列表视图`,
|
|
1317
|
+
};
|
|
1191
1318
|
}
|
|
1192
1319
|
// r30: a MISSING model answers this face with a generic 50000
|
|
1193
1320
|
// 服务异常 — indistinguishable from a real outage. One field-face
|
|
@@ -1266,8 +1393,13 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1266
1393
|
else if (totalPages > 0 && requestedPage >= totalPages) {
|
|
1267
1394
|
pageOutOfRange = true;
|
|
1268
1395
|
}
|
|
1396
|
+
const draftRows = page.content.filter((row) => {
|
|
1397
|
+
const data = row.data && typeof row.data === "object" ? row.data : row;
|
|
1398
|
+
return String(data.sequenceStatus ?? row.sequenceStatus ?? "") === "DRAFT";
|
|
1399
|
+
}).length;
|
|
1269
1400
|
const readback = {
|
|
1270
1401
|
schemaCode: args.schemaCode,
|
|
1402
|
+
...application,
|
|
1271
1403
|
...(projectionEmpty ? { projectionWarning: { reason: "business-columns-null", hint: "模型可能未配置列表视图:data.list 只投影列表列,业务列会全为 null;核对写入结果请用 data.load(或先建列表视图)" } } : {}),
|
|
1272
1404
|
...(args.queryCode !== undefined ? { queryCode: args.queryCode } : {}),
|
|
1273
1405
|
total,
|
|
@@ -1282,7 +1414,6 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1282
1414
|
// 统一一个枚举键 `draftPolicy`(included | excluded | unspecified),
|
|
1283
1415
|
// 不再用两个不同键名表达同一件事(那要调用方写两套分支)。
|
|
1284
1416
|
...(() => {
|
|
1285
|
-
const draftRows = items.filter((item) => String(item.fields.sequenceStatus ?? "") === "DRAFT").length;
|
|
1286
1417
|
const policy = draftRows > 0
|
|
1287
1418
|
? {
|
|
1288
1419
|
draftPolicy: "included",
|
|
@@ -1456,6 +1587,8 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1456
1587
|
...readbackProjectionArgs,
|
|
1457
1588
|
},
|
|
1458
1589
|
execute: async (session, args) => {
|
|
1590
|
+
const applicationScope = await resolveApplicationScope(session);
|
|
1591
|
+
const application = { applicationCode: applicationScope.code, applicationResolution: applicationResolutionOf(applicationScope) };
|
|
1459
1592
|
// F-A (self-audit): the combo used to be silently ignored (the workflow
|
|
1460
1593
|
// draft branch returns without honoring submit). Refuse before any wire.
|
|
1461
1594
|
if (args.submit === true && typeof args.workflowCode === "string" && args.workflowCode.trim())
|
|
@@ -1570,8 +1703,8 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1570
1703
|
readbackData = (await wireCall(session, { path: ENDPOINTS.runtimeFormLoad, query: { schemaCode: args.schemaCode, sheetCode, objectId: returnedId } })).data;
|
|
1571
1704
|
sequenceStatus = extractSequenceStatus((readbackData ?? {}));
|
|
1572
1705
|
return {
|
|
1573
|
-
performed: { errcode: performed.errcode, bizObjectId: returnedId, workflowInstanceId, workItemId, workItemSource, sheetCode, resolvedFrom, formType: "save-draft-workflow" },
|
|
1574
|
-
readback: { authoritative: projectReadbackRecord(normalizeLoadedRecord((readbackData ?? {})), args), sequenceStatus, expectedSequenceStatus: "DRAFT" },
|
|
1706
|
+
performed: { errcode: performed.errcode, ...application, bizObjectId: returnedId, workflowInstanceId, workItemId, workItemSource, sheetCode, resolvedFrom, formType: "save-draft-workflow" },
|
|
1707
|
+
readback: { ...application, authoritative: projectReadbackRecord(normalizeLoadedRecord((readbackData ?? {})), args), sequenceStatus, expectedSequenceStatus: "DRAFT" },
|
|
1575
1708
|
};
|
|
1576
1709
|
}
|
|
1577
1710
|
let performed;
|
|
@@ -1599,9 +1732,12 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1599
1732
|
// "rule didn't fire" misdiagnosis.
|
|
1600
1733
|
const savedRecord = normalizeLoadedRecord((readbackData ?? {}));
|
|
1601
1734
|
const savedEmptyFields = readbackEmptyFields(formData, savedRecord);
|
|
1735
|
+
const dataWarnings = await runtimeDataWarnings(session, args.schemaCode, sheetCode, formData, 1);
|
|
1602
1736
|
const plainReadback = {
|
|
1737
|
+
...application,
|
|
1603
1738
|
authoritative: projectReadbackRecord(savedRecord, args),
|
|
1604
1739
|
...(savedEmptyFields.length > 0 ? { readbackWarning: readbackWarningFor(savedEmptyFields, "data.save") } : {}),
|
|
1740
|
+
...(dataWarnings.length > 0 ? { warnings: dataWarnings } : {}),
|
|
1605
1741
|
draftRuleHint: "草稿态(DRAFT)保存不触发业务规则——DRAFT 行跳过 Create/Update 分发(DomainServiceImpl.java:240-248);要一调完成态本调用加 submit:true;已存草稿直提形状:data.submit {schemaCode, bizObjectId: 本读回的 bizObjectId, formData}(submit 自持久化)",
|
|
1606
1742
|
};
|
|
1607
1743
|
// r18 retro (G3): one-call COMPLETED creation — the agent goal is usually
|
|
@@ -1650,7 +1786,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1650
1786
|
};
|
|
1651
1787
|
}
|
|
1652
1788
|
return {
|
|
1653
|
-
performed: { errcode: performed.errcode, bizObjectId, sheetCode, resolvedFrom },
|
|
1789
|
+
performed: { errcode: performed.errcode, ...application, bizObjectId, sheetCode, resolvedFrom },
|
|
1654
1790
|
readback: plainReadback,
|
|
1655
1791
|
};
|
|
1656
1792
|
},
|
|
@@ -1731,9 +1867,9 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1731
1867
|
* gone, not that the model is clean — a submit can leave a separate draft row
|
|
1732
1868
|
* behind. After a delete, read the model's residue so "cleaned up" is a
|
|
1733
1869
|
* model-level fact (`total:0`). Best-effort: a query failure never fails the
|
|
1734
|
-
* delete, but it IS surfaced
|
|
1870
|
+
* delete, but it IS surfaced under `preExistingRows.error`.
|
|
1735
1871
|
*/
|
|
1736
|
-
async function
|
|
1872
|
+
async function preExistingRows(session, schemaCode, sheetCode) {
|
|
1737
1873
|
try {
|
|
1738
1874
|
const result = await wireCall(session, {
|
|
1739
1875
|
path: ENDPOINTS.runtimeQueryList,
|
|
@@ -1741,12 +1877,12 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1741
1877
|
body: { schemaCode, queryCode: schemaCode, filters: [], mobile: false, page: 0, size: 1, queryVersion: 1, showTotal: true },
|
|
1742
1878
|
});
|
|
1743
1879
|
const page = extractBizObjectPage(result.data);
|
|
1744
|
-
return {
|
|
1880
|
+
return { preExistingRows: { total: page.total, clean: page.total === 0, sheetCode, note: "含历史既有数据,非本次创建" } };
|
|
1745
1881
|
}
|
|
1746
1882
|
catch (error) {
|
|
1747
1883
|
if (error instanceof WireError && error.kind === "business" && error.errcode === RUNTIME_DATA_WIRE.emptyModelErrcode)
|
|
1748
|
-
return {
|
|
1749
|
-
return {
|
|
1884
|
+
return { preExistingRows: { total: 0, clean: true, sheetCode, note: "含历史既有数据,非本次创建" } };
|
|
1885
|
+
return { preExistingRows: { total: null, clean: null, sheetCode, note: "含历史既有数据,非本次创建", error: error instanceof Error ? error.message : String(error) } };
|
|
1750
1886
|
}
|
|
1751
1887
|
}
|
|
1752
1888
|
writeTool({
|
|
@@ -1843,14 +1979,15 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1843
1979
|
absent: true,
|
|
1844
1980
|
deleted: false,
|
|
1845
1981
|
noop: true,
|
|
1846
|
-
recoveryHint: "若另一进程刚刚删除,这是正常并发结果(already-absent),不是 id 拿错。否则:真实 id 必须取自 data.list 的 items[].id;工作流中心/门户页面回显的 id
|
|
1847
|
-
|
|
1982
|
+
recoveryHint: "若另一进程刚刚删除,这是正常并发结果(already-absent),不是 id 拿错。否则:真实 id 必须取自 data.list 的 items[].id;工作流中心/门户页面回显的 id 会脱敏。",
|
|
1983
|
+
thisRow: { absent: true, deleted: false },
|
|
1984
|
+
...(await preExistingRows(session, args.schemaCode, sheetCode)),
|
|
1848
1985
|
},
|
|
1849
1986
|
};
|
|
1850
1987
|
}
|
|
1851
1988
|
return {
|
|
1852
1989
|
performed: { errcode: one.errcode, deleted: one.deleted === true, noop: false, sheetCode, resolvedFrom },
|
|
1853
|
-
readback: { absent: one.absent, deleted: one.deleted === true, ...(await
|
|
1990
|
+
readback: { absent: one.absent, deleted: one.deleted === true, thisRow: { absent: one.absent, deleted: one.deleted === true }, ...(await preExistingRows(session, args.schemaCode, sheetCode)) },
|
|
1854
1991
|
};
|
|
1855
1992
|
}
|
|
1856
1993
|
const deleted = [];
|
|
@@ -1858,7 +1995,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1858
1995
|
deleted.push(await deleteOne(id));
|
|
1859
1996
|
// `deleted`/`count` ride `readback` — the write envelope surfaces only
|
|
1860
1997
|
// performed/readback inline (extra top-level keys are artifact-only).
|
|
1861
|
-
return { performed: { sheetCode, resolvedFrom }, readback: { absent: true, count: deleted.length, deleted, ...(await
|
|
1998
|
+
return { performed: { sheetCode, resolvedFrom }, readback: { absent: true, count: deleted.length, deleted, ...(await preExistingRows(session, args.schemaCode, sheetCode)) } };
|
|
1862
1999
|
},
|
|
1863
2000
|
});
|
|
1864
2001
|
writeTool({
|
package/dist/mcp-graph.js
CHANGED
|
@@ -58,24 +58,27 @@ const EXPLORE_TOOL = {
|
|
|
58
58
|
name: "omc_graph_explore",
|
|
59
59
|
description: [
|
|
60
60
|
"一次查全一个目标的引用与影响面(本地索引,毫秒级)。",
|
|
61
|
-
"输入 query
|
|
61
|
+
"输入 query 可为业务名(实体 name+code 子串)或规则 code+name 词首;也可直接给字段/模型/服务 code。kind 可选 field|model|service,不给则三种都查。",
|
|
62
62
|
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via}]}, impact:{kind:totals}, search:{matches:{model,field,service},rules}}。",
|
|
63
|
+
"references/impact 支持 offset+limit 分页;truncated 时按返回的 nextOffset 继续调用。",
|
|
63
64
|
"改/删/重命名任何字段、模型、服务之前必查;找\"关于 X 的规则\"拿 code 也走这里。",
|
|
64
65
|
"索引不含规则条件叶子与流程绑定;freshness=partial 时命中可能不完整(答案里有 gapWarning)。",
|
|
65
66
|
].join(" "),
|
|
66
67
|
inputSchema: {
|
|
67
68
|
type: "object",
|
|
68
69
|
properties: {
|
|
69
|
-
query: { type: "string", description: "
|
|
70
|
+
query: { type: "string", description: "业务名(实体 name+code 子串)、字段/模型/服务 code,或规则检索词(规则 code+name 词首匹配)" },
|
|
70
71
|
kind: { type: "string", enum: [...KINDS], description: "限定一种引用种类;省略则 field/model/service 全查" },
|
|
71
72
|
env: { type: "string", description: "期望的环境名;与索引所属环境不一致时会拒绝(防跨环境误答)" },
|
|
73
|
+
offset: { type: "integer", minimum: 0, description: "引用/实体结果起始偏移;truncated 时使用 nextOffset" },
|
|
74
|
+
limit: { type: "integer", minimum: 1, maximum: MATCH_CAP, description: "每类结果最多返回多少条(默认 50)" },
|
|
72
75
|
},
|
|
73
76
|
required: ["query"],
|
|
74
77
|
},
|
|
75
78
|
};
|
|
76
79
|
const STATUS_TOOL = {
|
|
77
80
|
name: "omc_graph_status",
|
|
78
|
-
description: "索引完整性与新鲜度:environmentName、freshness(complete|partial)、模型/规则计数、pulledAtIso、gaps
|
|
81
|
+
description: "索引完整性与新鲜度:environmentName、freshness(complete|partial)、模型/规则计数、pulledAtIso、ageMinutes、staleAfterMinutes、dbPath、coverage.applicationCodes、gaps。超过阈值会给 warning;回答前先看一眼,索引缺失会给出 pull 指引。",
|
|
79
82
|
inputSchema: { type: "object", properties: {} },
|
|
80
83
|
};
|
|
81
84
|
export function graphToolDefinitions() {
|
|
@@ -87,8 +90,9 @@ function toolResult(payload, isError = false) {
|
|
|
87
90
|
function toolError(message) {
|
|
88
91
|
return toolResult({ status: "error", message }, true);
|
|
89
92
|
}
|
|
90
|
-
function cap(rows) {
|
|
91
|
-
|
|
93
|
+
function cap(rows, offset = 0, limit = MATCH_CAP) {
|
|
94
|
+
const page = rows.slice(offset, offset + limit);
|
|
95
|
+
return { rows: page, truncated: offset + page.length < rows.length, offset, limit, ...(offset + page.length < rows.length ? { nextOffset: offset + page.length } : {}), total: rows.length };
|
|
92
96
|
}
|
|
93
97
|
/** Shared handler core — exported so tests drive it without a stdio pair. */
|
|
94
98
|
export async function executeGraphTool(name, rawArgs) {
|
|
@@ -111,17 +115,22 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
111
115
|
return toolError(`索引属于环境「${index.environmentName}」;要查「${args.env.trim()}」请先 omc graph pull --env ${args.env.trim()} 重建索引`);
|
|
112
116
|
}
|
|
113
117
|
const kinds = kind === undefined ? [...KINDS] : [kind];
|
|
118
|
+
const offset = typeof args.offset === "number" && Number.isSafeInteger(args.offset) && args.offset >= 0 ? args.offset : 0;
|
|
119
|
+
const limit = typeof args.limit === "number" && Number.isSafeInteger(args.limit) && args.limit > 0 && args.limit <= MATCH_CAP ? args.limit : MATCH_CAP;
|
|
114
120
|
const references = {};
|
|
115
121
|
const impact = {};
|
|
116
122
|
for (const one of kinds) {
|
|
117
123
|
const ref = queryGraphReferences(one, query);
|
|
118
|
-
const capped = cap(ref.matches);
|
|
124
|
+
const capped = cap(ref.matches, offset, limit);
|
|
119
125
|
// r35a-F4c(真机 2026-11-06):**MCP 面是第三个消费者**——CLI 的 references
|
|
120
126
|
// 与 impact 都带 coverage 了,这里若不显式透传,MCP 用户看到的仍是
|
|
121
127
|
// 「matches:[] + 一句旧 note」,照样会把「表单没扫」误读成「没人引用」。
|
|
122
128
|
references[one] = {
|
|
123
129
|
count: ref.matches.length,
|
|
124
|
-
|
|
130
|
+
rows: capped.rows,
|
|
131
|
+
offset: capped.offset,
|
|
132
|
+
limit: capped.limit,
|
|
133
|
+
...(capped.truncated ? { truncated: true, nextOffset: capped.nextOffset, note: `共 ${capped.total} 条,已给 ${capped.rows.length} 条,offset=${capped.offset} 取下一段(调用时传 offset=${capped.nextOffset}、limit=${capped.limit})` } : {}),
|
|
125
134
|
...(ref.coverage ? { coverage: ref.coverage } : {}),
|
|
126
135
|
...(ref.gapWarning ? { gapWarning: ref.gapWarning } : {}),
|
|
127
136
|
};
|
|
@@ -129,26 +138,36 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
129
138
|
const imp = impactGraph(one, query);
|
|
130
139
|
impact[one] = {
|
|
131
140
|
totals: imp.totals,
|
|
132
|
-
rules: cap(imp.rules),
|
|
133
|
-
datarules: cap(imp.datarules),
|
|
134
|
-
listviews: cap(imp.listviews),
|
|
141
|
+
rules: cap(imp.rules, offset, limit),
|
|
142
|
+
datarules: cap(imp.datarules, offset, limit),
|
|
143
|
+
listviews: cap(imp.listviews, offset, limit),
|
|
135
144
|
...(imp.coverage ? { coverage: imp.coverage } : {}),
|
|
136
145
|
};
|
|
137
146
|
}
|
|
138
147
|
}
|
|
139
|
-
const search = searchGraphRules(query);
|
|
148
|
+
const search = searchGraphRules(query, limit, offset);
|
|
149
|
+
const resolvedEntity = search.matches.model.items[0] ? { kind: "model", schemaCode: search.matches.model.items[0].schemaCode, code: search.matches.model.items[0].schemaCode } :
|
|
150
|
+
search.matches.field.items[0] ? { kind: "field", schemaCode: search.matches.field.items[0].schemaCode, code: search.matches.field.items[0].code } :
|
|
151
|
+
search.matches.service.items[0] ? { kind: "service", schemaCode: "", code: search.matches.service.items[0].code } :
|
|
152
|
+
search.rules[0] ? { kind: "rule", schemaCode: search.rules[0].schemaCode, code: search.rules[0].ruleCode } : undefined;
|
|
153
|
+
const searchNote = "实体检索:业务名/实体 name+code 使用子串匹配。\n规则检索:规则 code+name 使用词首匹配(中文子串不命中)。";
|
|
154
|
+
const entityTruncated = Object.values(search.matches).some((match) => match.truncated === true);
|
|
155
|
+
const paginationNote = entityTruncated ? `\n结果已截断:共 N 条时请使用对应 match 的 nextOffset,并在下一次调用传 offset=nextOffset、limit=${limit} 取下一段。` : "";
|
|
140
156
|
return toolResult({
|
|
141
157
|
status: "ok",
|
|
142
158
|
query,
|
|
159
|
+
found: resolvedEntity !== undefined,
|
|
160
|
+
...(resolvedEntity ? { resolved: resolvedEntity } : {}),
|
|
143
161
|
...(kind !== undefined ? { kind } : {}),
|
|
144
162
|
index,
|
|
145
163
|
references,
|
|
146
164
|
impact,
|
|
147
165
|
search: { count: search.rules.length, rules: search.rules, matches: search.matches, coverage: search.coverage },
|
|
148
|
-
|
|
166
|
+
searchNote: `${searchNote}${paginationNote}`,
|
|
167
|
+
...(search.note ? { searchNote: `${searchNote}\n${search.note}` } : {}),
|
|
149
168
|
// 旧句「索引不含规则条件叶子与流程绑定」漏了表单与流程权限(r35a-F4 实证
|
|
150
169
|
// 探针据此差点误删被表单引用的字段)。改为指向结构化 coverage,不再靠散文。
|
|
151
|
-
note:
|
|
170
|
+
note: `零命中只代表 references[].coverage.scanned 面没有命中;本索引覆盖应用:${index.coverage.applicationCodes.join("、") || "(未知)"},不代表其他应用没有结果;notScanned 面(尤其 form 与 workflow.permissions)未扫描,不可据此判定可安全删除或无影响`,
|
|
152
171
|
});
|
|
153
172
|
}
|
|
154
173
|
return toolError(`未知工具:${name}`);
|
|
@@ -17,6 +17,7 @@ import { wireCall, WireError, wireRows } from "./wire.js";
|
|
|
17
17
|
* endpoint — see .scratch/backend-online-coding/recon.md §8).
|
|
18
18
|
*
|
|
19
19
|
* Commands:
|
|
20
|
+
* ⚠ test 会 publish 并切编码模式(不可逆)
|
|
20
21
|
* pull remote → tree + manifest (environment-stamped)
|
|
21
22
|
* status local edits vs manifest, plus optional remote drift (--deep)
|
|
22
23
|
* apply changed files → programming.save (→ --publish)
|
|
@@ -801,18 +802,30 @@ export async function cmdProgrammingTest(options) {
|
|
|
801
802
|
const targetSchema = String(dataSpec.schema ?? schemaCode);
|
|
802
803
|
const filter = (dataSpec.filter ?? {});
|
|
803
804
|
const expect = (dataSpec.expect ?? {});
|
|
805
|
+
// data.list is only a locator here. Its projection is intentionally not
|
|
806
|
+
// used as evidence: omitted columns are returned as null and used to turn
|
|
807
|
+
// a correct rule into a false failure (F11).
|
|
804
808
|
const listed = await callTool("data.list", {
|
|
805
809
|
environment: session.environmentName,
|
|
806
810
|
schemaCode: targetSchema,
|
|
807
811
|
...filterOf(filter),
|
|
808
|
-
displayFields:
|
|
812
|
+
displayFields: ["bizObjectId", "id"],
|
|
809
813
|
}, context);
|
|
810
814
|
const rows = (listed.rows ?? listed.items ?? []);
|
|
811
|
-
const
|
|
815
|
+
const loadedRows = [];
|
|
816
|
+
for (const row of rows) {
|
|
817
|
+
const id = String(row.bizObjectId ?? row.id ?? "");
|
|
818
|
+
if (!id)
|
|
819
|
+
continue;
|
|
820
|
+
const loaded = await callTool("data.load", { environment: session.environmentName, schemaCode: targetSchema, bizObjectId: id, full: true }, context);
|
|
821
|
+
if (loaded.exists === true && loaded.data && typeof loaded.data === "object")
|
|
822
|
+
loadedRows.push(loaded.data);
|
|
823
|
+
}
|
|
824
|
+
const matched = loadedRows.filter((row) => matchExpectation(row, expect).ok);
|
|
812
825
|
const ok = matched.length > 0;
|
|
813
826
|
if (!ok)
|
|
814
827
|
assertsFailed += 1;
|
|
815
|
-
assertReports.push({ schema: targetSchema, filter, expect, rows:
|
|
828
|
+
assertReports.push({ schema: targetSchema, filter, expect, rows: loadedRows.length, ok, ...(ok ? {} : { sample: loadedRows.slice(0, 3) }) });
|
|
816
829
|
}
|
|
817
830
|
report.asserts = assertReports;
|
|
818
831
|
// 5. Cleanup — only rows the fixture names (seed filter), never a blanket key.
|
package/dist/read-shapes.js
CHANGED
|
@@ -71,7 +71,7 @@ const READ_SHAPES = {
|
|
|
71
71
|
keys: ["bizObjectId", "exists", "data:{...}"],
|
|
72
72
|
},
|
|
73
73
|
"app.list": {
|
|
74
|
-
keys: ["applications:array<{code,name,groupId
|
|
74
|
+
keys: ["applications:array<{code,name,groupId?,modelCount}>", "applicationsCount", "omitted"],
|
|
75
75
|
notes: ["applicationsCount + omitted 与 discover 同构;omitted:false 表示内联即全集"],
|
|
76
76
|
},
|
|
77
77
|
"model.list": {
|
|
@@ -85,8 +85,8 @@ const READ_SHAPES = {
|
|
|
85
85
|
},
|
|
86
86
|
"model.inspect": {
|
|
87
87
|
// Slim/compact face (window diet): field inventory is `fieldCodes`, not fields[].
|
|
88
|
-
keys: ["schemaCode", "fieldCodes", "form", "listview", "ruleCodes", "datarules", "workflow", "hint"],
|
|
89
|
-
notes: ["slim 面键是 fieldCodes(不是 fields[].code
|
|
88
|
+
keys: ["schemaCode", "applicationCode", "fieldCodes", "form", "listview", "ruleCodes", "datarules", "workflow", "hint"],
|
|
89
|
+
notes: ["slim 面键是 fieldCodes(不是 fields[].code);all:true 时 fields/artifact 行含 options"],
|
|
90
90
|
},
|
|
91
91
|
"field.list": {
|
|
92
92
|
keys: ["fields:array<{code,name,type,...}>", "modelPresent?"],
|