@stalfh233/omc-cli 0.4.98 → 0.4.100

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.
@@ -146,7 +146,13 @@ function bestRecipe(registry, capabilityId, lane, goal, minScore = 3) {
146
146
  if (tool.lane !== lane)
147
147
  continue;
148
148
  for (const recipe of tool.executionMetadata?.recipes ?? []) {
149
- const score = overlap(goal, `${recipe.title} ${recipe.id} ${(recipe.aliases ?? []).join(" ")}`);
149
+ // r19 ticket 04: an authored ALIAS phrase present in the goal is a much
150
+ // stronger signal than bigram overlap — "…汇总另一个模型的…" must route to
151
+ // formula-aggregation-cross-model, not the title-overlapping dropdown
152
+ // recipe. +20 per exact alias hit keeps authored intent decisive while
153
+ // overlap still ranks within equally aliased candidates.
154
+ const aliasBonus = (recipe.aliases ?? []).reduce((sum, alias) => (goal.includes(alias) ? sum + 20 : sum), 0);
155
+ const score = overlap(goal, `${recipe.title} ${recipe.id} ${(recipe.aliases ?? []).join(" ")}`) + aliasBonus;
150
156
  if (score >= minScore && (best === undefined || score > best.score))
151
157
  best = { tool: tool.name, recipe: recipe.id, title: recipe.title, score };
152
158
  }
@@ -265,50 +271,39 @@ function sectionPointersFor(ids) {
265
271
  return pointers;
266
272
  }
267
273
  /**
268
- * Ready-to-run methodology reads.
274
+ * Ready-to-run methodology reads — r19 ticket 04: ONE pointer, not a shelf.
269
275
  *
270
- * Round 15 (bench60-62) collapsed ~18 per-section reads into one per topic to
271
- * save turns. r10 F7 found the other edge: following that hint fetched
272
- * EVERYTHING at once (~15 KB) when most tasks need only the first section.
273
- * So the primary command per topic fetches the FIRST required section, and the
274
- * remaining sections are offered as separate incremental commands (each keeps
275
- * its byte count in `readSections`) — cheap by default, still one command away.
276
+ * Round 15 collapsed per-section reads; r10 F7 made the first section cheap.
277
+ * r18 then showed the residual cost is ATTENTION, not bytes: seven pointers
278
+ * made the agent crawl methodology before acting, and the generic draft
279
+ * template ("3. Draft template") was read and produced nothing on
280
+ * recipe-matched routes. So: a recipe-matched route carries NO pointer (the
281
+ * recipe command is the answer); otherwise exactly one — the first authored
282
+ * pointer that is not the omc-config-draft-review human-gate template (that
283
+ * gate is conversational, not a section to prefetch), falling back to the
284
+ * first pointer when every authored pointer is draft-review.
276
285
  */
277
- function splitReadCommands(pointers) {
278
- const byTopic = new Map();
279
- for (const pointer of pointers)
280
- byTopic.set(pointer.topic, [...(byTopic.get(pointer.topic) ?? []), pointer.section]);
281
- const readCommands = [];
282
- const readMore = [];
283
- for (const [topic, sections] of byTopic.entries()) {
284
- readCommands.push(`omc help ${topic} --section "${sections[0]}"`);
285
- for (const section of sections.slice(1))
286
- readMore.push(`omc help ${topic} --section "${section}"`);
287
- }
288
- return { readCommands, readMore };
286
+ function pickSectionPointer(pointers, recipeMatched) {
287
+ if (recipeMatched || pointers.length === 0)
288
+ return [];
289
+ const specific = pointers.find((pointer) => pointer.topic !== "omc-config-draft-review");
290
+ return [specific ?? pointers[0]];
289
291
  }
292
+ const FIRST_SECTION_INLINE_BUDGET_BYTES = 2048;
290
293
  /**
291
- * The route's methodology-read surface: pointers (+ per-section bytes) +
292
- * ready-to-run batched commands + hint.
293
- *
294
- * F9 (r10-friction): the most common task flow was `next` followed
295
- * IMMEDIATELY by fetching the first readSection — one extra process +
296
- * round-trip every task. The first section now rides inline when it fits the
297
- * inline budget (~2 KB); larger sections stay pointer-only so `next` never
298
- * balloons.
294
+ * r19 ticket 04: a multi-capability ladder already carries tool maps and
295
+ * recipes, so its inline section budget is tighter — the 4096B `next` ceiling
296
+ * must survive a ladder + a 2.5KB section (r18: "工作流审批流程" would have
297
+ * been 4729B). Single-capability routes keep the fuller budget (F9).
299
298
  */
300
- const FIRST_SECTION_INLINE_BUDGET_BYTES = 2560;
301
- function routeReads(readSections) {
302
- // Per-section UTF-8 bytes ride each pointer (one field, not a parallel array)
303
- // so a caller can budget which sections to fetch — dogfood R1 found the
304
- // batched readCommands could exceed a window with no way to see sizes.
305
- const budgeted = readSections.map((pointer) => ({ ...pointer, bytes: sectionBytes(pointer) }));
306
- // Inline the FIRST pointer that fits the budget (order = relevance); an
307
- // oversized head section falls through to the next fitting one rather than
308
- // dropping the inline content entirely.
299
+ const LADDER_INLINE_BUDGET_BYTES = 1200;
300
+ function routeReads(readSections, recipeMatched = false, inlineBudget = FIRST_SECTION_INLINE_BUDGET_BYTES) {
301
+ const budgeted = pickSectionPointer(readSections, recipeMatched).map((pointer) => ({ ...pointer, bytes: sectionBytes(pointer) }));
302
+ // Inline the ONE pointer's content when it fits the budget: the agent then
303
+ // needs zero extra calls for methodology (F9, r10-friction).
309
304
  let firstSection;
310
- for (const pointer of budgeted.slice(0, 3)) {
311
- if (pointer.bytes > 0 && pointer.bytes <= FIRST_SECTION_INLINE_BUDGET_BYTES) {
305
+ for (const pointer of budgeted) {
306
+ if (pointer.bytes > 0 && pointer.bytes <= inlineBudget) {
312
307
  const resolved = readSkillSection(pointer.topic, pointer.section);
313
308
  if (resolved) {
314
309
  firstSection = { topic: pointer.topic, section: pointer.section, bytes: pointer.bytes, content: resolved.section };
@@ -316,15 +311,19 @@ function routeReads(readSections) {
316
311
  }
317
312
  }
318
313
  }
319
- const commands = splitReadCommands(readSections);
320
- return { readSections: budgeted, readCommands: commands.readCommands, ...(commands.readMore.length > 0 ? { readMore: commands.readMore } : {}), readHint: READ_HINT, ...(firstSection !== undefined ? { firstSection } : {}) };
314
+ return {
315
+ readSections: budgeted,
316
+ readCommands: budgeted.map((pointer) => `omc help ${pointer.topic} --section "${pointer.section}"`),
317
+ readHint: READ_HINT,
318
+ ...(firstSection !== undefined ? { firstSection } : {}),
319
+ };
321
320
  }
322
321
  /** UTF-8 byte length of one methodology section (best-effort; 0 when unreadable). */
323
322
  function sectionBytes(pointer) {
324
323
  const resolved = readSkillSection(pointer.topic, pointer.section);
325
324
  return resolved ? Buffer.byteLength(resolved.section, "utf8") : 0;
326
325
  }
327
- const READ_HINT = '先跑 readCommands(每个 topic 只取第一节,形状 `omc help <topic> --section "<section>"`,字节数见 readSections);不够再按需补 readMore,不要一次全读。`omc help <topic>` 默认只回节索引(每节带字节数,约 1KB),不要再用 `--all` 读整篇(5–14KB,读一次就留在后续每一轮)。';
326
+ const READ_HINT = '方法论只挂最相关的一节(readSections[0] 带字节数,`omc help <topic> --section "<节>"` 可回放,firstSection 已内联时就不用再取);其他具体问题(错误码/跨能力语义/某个词)用 `omc help --search "<词>"` 一步全文检索;不要 `--all` 读整篇。';
328
327
  function orderedCapabilities(ids, index) {
329
328
  const selected = new Set(ids);
330
329
  const nodes = [...index.nodeById.values()];
@@ -434,6 +433,21 @@ const COMPOSER_ROUTES = Object.freeze([
434
433
  ]);
435
434
  /** Test seam: the composer registry, so a template typo can be caught offline. */
436
435
  export const COMPOSER_ROUTES_FOR_TEST = COMPOSER_ROUTES;
436
+ /**
437
+ * r19 ticket 04 — the ONE question that unlocks each composer. A composer
438
+ * skeleton always carries placeholders, so a composer match is a `needs-input`
439
+ * first answer by definition; the question names exactly what to fill in.
440
+ */
441
+ const COMPOSER_QUESTIONS = Object.freeze({
442
+ "model-purge": "要清理哪个模型?给我 schemaCode(不确定就先跑下面的只读判别动作拿清单)",
443
+ "model-scaffold": "给我模型 code、显示名和字段清单(每字段 code/名称/类型;子表放 subFields),我一条命令建完",
444
+ "approval-flow": "目标模型 schemaCode 和每级审批节点(节点名@角色code,多级用重复 --steps)是什么?",
445
+ compute: "目标模型 schemaCode、结果字段 code、公式是什么?(子表条件聚合再加 --where \"子列 = 值\")",
446
+ "rule-when": "目标模型 schemaCode、触发时机(Create/Update…)、条件(--when)和动作(--then)各是什么?",
447
+ "form-default": "目标模型 schemaCode、字段 code 和默认值是什么?",
448
+ });
449
+ /** The read-only discriminator that supplies a missing schema code. */
450
+ const LIST_MODELS_ACTION = "omc call model.list --args '{}'";
437
451
  function matchComposer(goal, mode) {
438
452
  // A composer is a WRITE path: reads and scouting asks must keep their ladder.
439
453
  if (mode !== "build" && mode !== "modify")
@@ -471,11 +485,19 @@ function wholePackageRoute(registry, goal, index) {
471
485
  primary: "intent",
472
486
  atomicSupplement: { note: "intent 已覆盖下列能力的创建;这些原子工具只用于 intent 未覆盖面(删除/生命周期/运行数据/精确 patch):", ladder },
473
487
  ...routeReads(sectionPointersFor(ordered)),
474
- firstCommands: [DISCOVERY_AUTHORITY.readiness],
488
+ // r19 ticket 04: readiness (discover_environment) left firstCommands —
489
+ // the probe executed it 0 times in 184 calls; it rides its own field now.
490
+ readiness: DISCOVERY_AUTHORITY.readiness,
491
+ firstCommands: ["omc intent schema"],
475
492
  write: true,
476
493
  },
477
494
  write: true,
478
495
  intent: intentGuidance(),
496
+ firstAnswer: {
497
+ state: "ready",
498
+ action: "omc intent schema",
499
+ why: "整包需求走声明式意图:先看 intent YAML 的键位与可运行示例,再按 starter 写文件(validate → plan → apply)",
500
+ },
479
501
  };
480
502
  }
481
503
  /** A configuration read-back ask that names no capability ("给我一份现状说明"). */
@@ -490,8 +512,13 @@ function readOverviewRoute(registry, goal, index) {
490
512
  confident: true,
491
513
  mode: "read",
492
514
  match: { kind: "read-overview", ref: ordered.join("→"), capabilities: ordered },
493
- route: { ladder, ...routeReads(sectionPointersFor(["environment"])), firstCommands: [DISCOVERY_AUTHORITY.readiness], write: false, readback: true },
515
+ route: { ladder, ...routeReads(sectionPointersFor(["environment"])), readiness: DISCOVERY_AUTHORITY.readiness, firstCommands: [LIST_MODELS_ACTION], write: false, readback: true },
494
516
  write: false,
517
+ firstAnswer: {
518
+ state: "ready",
519
+ action: LIST_MODELS_ACTION,
520
+ why: "盘点从模型清单开始:一次调用拿到全部 schemaCode,后续读面(inspect/field.list)据此展开",
521
+ },
495
522
  };
496
523
  }
497
524
  export function goalRoute(registry, goal) {
@@ -522,7 +549,8 @@ export function goalRoute(registry, goal) {
522
549
  verify: "omc capabilities / omc capability <id>(运行时清单才是权威边界;能力表只是阅读辅助)",
523
550
  topics: ["omc-requirement-analysis", "omc-capability-scouting"].filter((topic) => availableTopics.has(topic)),
524
551
  },
525
- route: { write: false, readback: false, firstCommands: ["omc capabilities", "omc capability <id>", "omc help omc-capability-scouting"] },
552
+ route: { write: false, readback: false, readiness: DISCOVERY_AUTHORITY.readiness, firstCommands: ["omc capabilities", "omc capability <id>", "omc help omc-capability-scouting"] },
553
+ firstAnswer: { state: "blocked", why: boundary.note },
526
554
  };
527
555
  }
528
556
  // Composer before the capability solver: a recurring write SHAPE has a single
@@ -542,14 +570,22 @@ export function goalRoute(registry, goal) {
542
570
  composer: composer.id,
543
571
  command: composer.command,
544
572
  summary: composer.summary,
545
- // The composer IS the first step; readiness still comes first because
546
- // the command needs a bound environment to run.
547
- firstCommands: [DISCOVERY_AUTHORITY.readiness, composer.command, composer.usage],
573
+ readiness: DISCOVERY_AUTHORITY.readiness,
574
+ // The composer IS the first step (its skeleton waits on the ONE
575
+ // question in firstAnswer; discover_environment no longer occupies
576
+ // the head slot — r18 executed it 0 times in 184 calls).
577
+ firstCommands: [composer.command, composer.usage],
548
578
  write: true,
549
579
  readback: true,
550
- ...(topics.length > 0 ? { readCommands: topics.map((topic) => `omc help ${topic} --section "<节>"`), readHint: "只在 composer 的 --help 不够用时再读这些 topic 的对应节" } : {}),
551
580
  },
552
581
  ...(topics.length > 0 ? { topics } : {}),
582
+ firstAnswer: {
583
+ state: "needs-input",
584
+ question: COMPOSER_QUESTIONS[composer.id] ?? "补齐命令里的尖括号占位符后运行(形状见 pendingCommand)",
585
+ pendingCommand: composer.command,
586
+ ...(composer.id === "model-purge" || composer.id === "compute" ? { action: LIST_MODELS_ACTION } : {}),
587
+ why: composer.summary,
588
+ },
553
589
  };
554
590
  }
555
591
  const scores = [...capabilityScores(registry, goal).entries()].sort((left, right) => right[1] - left[1]);
@@ -562,6 +598,13 @@ export function goalRoute(registry, goal) {
562
598
  topics: [...availableTopics],
563
599
  write: false,
564
600
  mode,
601
+ // Not confident → say so and hand ONE next move instead of a topic shelf.
602
+ firstAnswer: {
603
+ state: "needs-input",
604
+ question: "把目标说成一个具体动作短语(对象 + 动作,如「给 X 模型加一个汇总字段」),我就能给出一步可跑的命令",
605
+ action: "omc help omc-requirement-analysis",
606
+ why: "没命中任何能力线索;不猜路径(precision over recall)",
607
+ },
565
608
  };
566
609
  const best = scores[0];
567
610
  if (best === undefined) {
@@ -601,6 +644,12 @@ export function goalRoute(registry, goal) {
601
644
  const readSections = sectionPointersFor(ordered);
602
645
  const write = readish ? false : ordered.some((id) => routeById.get(id)?.tools.some((name) => registryTools.get(name)?.kind === "write"));
603
646
  const whole = isWholePackageWrite(mode, goal, ordered.length);
647
+ // r19 ticket 04: the ladder's first RECIPE command is the first answer
648
+ // when one exists (its describe carries the copyable firstCall shape);
649
+ // otherwise the only missing input is the target object code.
650
+ const recipeCommands = ladder.flatMap((step) => step.recipe?.command ? [step.recipe.command] : []);
651
+ const firstAction = recipeCommands[0];
652
+ const firstRecipeId = ladder.find((step) => step.recipe) && (ladder.find((step) => step.recipe).recipe.id);
604
653
  // When intent is the primary path, the atomic ladder is only a POINTER to the
605
654
  // capabilities (dogfood R1: repeating every tool map was ~60% unused bytes).
606
655
  const atomicSupplement = { note: "intent 已覆盖这些能力的创建;原子工具只用于 intent 未覆盖面:", ladder: ladder.map((step) => ({ capability: step.capability, lane: step.lane, firstCommand: step.firstCommand ?? `omc commands --json --compact --lane ${step.lane}` })) };
@@ -612,13 +661,14 @@ export function goalRoute(registry, goal) {
612
661
  match: { kind: readish ? "read-ladder" : "ladder", ref: ordered.join("→"), capabilities: ordered },
613
662
  route: {
614
663
  ...(whole ? { primary: "intent", atomicSupplement } : { ladder }),
615
- ...routeReads(readSections),
664
+ ...routeReads(readSections, recipeCommands.length > 0, LADDER_INLINE_BUDGET_BYTES),
665
+ readiness: DISCOVERY_AUTHORITY.readiness,
616
666
  // r18: when a step already carries a goal-matched recipe, the generic
617
667
  // first section ("3. Draft template") is noise — the probe read it and
618
668
  // got nothing. Say that the recipe command is the answer and the
619
669
  // sections are only a fallback.
620
- ...(ladder.some((step) => step.recipe)
621
- ? { readHint: "已命中配方:先跑 firstCommands 里的 `omc describe … --example …` 取形状;下面的 readSections 只是补充,不必先读(确实需要时用 `omc help <topic> --section \"<节>\"`,按 readMore 逐节补)。" }
670
+ ...(recipeCommands.length > 0
671
+ ? { readHint: "已命中配方:先跑 firstCommands 里的 `omc describe … --example …` 取形状(首答自带 firstCall 可照抄);方法论节已按需收缩,确实需要时用 `omc help <topic> --section \"<节>\"`。" }
622
672
  : {}),
623
673
  // Each ladder step already carried a runnable recipe command, but it sat
624
674
  // buried at `ladder[i].recipe.command` behind a readiness line — r14's
@@ -626,8 +676,7 @@ export function goalRoute(registry, goal) {
626
676
  // graph instead. Surface them at the top level where `firstCommands` is
627
677
  // the thing an agent actually reads.
628
678
  firstCommands: [
629
- DISCOVERY_AUTHORITY.readiness,
630
- ...ladder.flatMap((step) => step.recipe?.command ? [step.recipe.command] : []),
679
+ ...recipeCommands,
631
680
  // r13 #3: `capability workflow --executable` is 23 KB and still did not
632
681
  // answer "how do I submit an approval" — the answer is a data.submit
633
682
  // recipe, which lives in a DIFFERENT capability. When the goal is about
@@ -640,7 +689,17 @@ export function goalRoute(registry, goal) {
640
689
  ...(readish ? { readback: true } : {}),
641
690
  },
642
691
  write,
643
- ...(whole ? { intent: intentGuidance() } : {}),
692
+ ...(whole
693
+ ? { intent: intentGuidance(), firstAnswer: { state: "ready", action: "omc intent schema", why: "整包需求走声明式意图:先看 intent 的键位与示例,再按 starter 写文件" } }
694
+ : readish
695
+ // A read ask about the model inventory itself is answered BY the
696
+ // inventory call; any other read needs the target object code first.
697
+ ? ordered.every((id) => id === "environment" || id === "model")
698
+ ? { firstAnswer: { state: "ready", action: LIST_MODELS_ACTION, why: "问的就是模型清单本身:model.list 一次调用直接回答" } }
699
+ : { firstAnswer: { state: "needs-input", question: "要查看哪个对象?给我 schemaCode(或业务名,我列清单核对)", action: LIST_MODELS_ACTION, why: "读路径命中多个能力:readTools 已内联,只差目标对象 code" } }
700
+ : firstAction !== undefined
701
+ ? { firstAnswer: { state: "ready", action: firstAction, why: `命中配方 ${firstRecipeId}:describe 首答自带 firstCall 形状,代入你的 schemaCode/字段 code 即可执行` } }
702
+ : { firstAnswer: { state: "needs-input", question: "第一步要操作哪个对象?给我 schemaCode(或模型业务名,我列清单核对)", action: LIST_MODELS_ACTION, why: "未命中配方:需要目标对象 code 才能给出一步可跑的写命令" } }),
644
703
  };
645
704
  }
646
705
  // Single-capability mode: when several capabilities tie, a matching recipe
@@ -651,7 +710,7 @@ export function goalRoute(registry, goal) {
651
710
  return fallback;
652
711
  const write = readish ? false : route.tools.some((name) => registryTools.get(name)?.kind === "write");
653
712
  const recipe = readish ? undefined : bestRecipe(registry, capabilityId, route.lane, goal);
654
- const firstCommands = [DISCOVERY_AUTHORITY.readiness];
713
+ const firstCommands = [];
655
714
  if (recipe)
656
715
  firstCommands.push(`omc describe ${recipe.tool} --example ${recipe.recipe}`);
657
716
  if (readish)
@@ -666,6 +725,7 @@ export function goalRoute(registry, goal) {
666
725
  if (!readish && /审批|通过|驳回|提交|签收/.test(goal) && (capabilityId === "workflow" || capabilityId === "runtimeData"))
667
726
  firstCommands.push("omc describe data.submit --example workflow-backed-data-submit");
668
727
  const whole = isWholePackageWrite(mode, goal, 1);
728
+ const recipeCommand = recipe ? `omc describe ${recipe.tool} --example ${recipe.recipe}` : undefined;
669
729
  return {
670
730
  status: "ok",
671
731
  goal,
@@ -683,13 +743,23 @@ export function goalRoute(registry, goal) {
683
743
  lane: route.lane,
684
744
  ...(whole ? { primary: "intent" } : {}),
685
745
  ...(readish ? { readTools: readToolsInLane(registry, route.lane) } : { tools: route.tools }),
686
- ...routeReads(sectionPointersFor([capabilityId])),
746
+ ...routeReads(sectionPointersFor([capabilityId]), recipe !== undefined),
747
+ readiness: DISCOVERY_AUTHORITY.readiness,
687
748
  firstCommands,
688
749
  write,
689
750
  ...(readish ? { readback: true } : {}),
690
751
  },
691
752
  write,
692
753
  ...(whole ? { intent: intentGuidance() } : {}),
754
+ ...(whole
755
+ ? { firstAnswer: { state: "ready", action: "omc intent schema", why: "整包需求走声明式意图:先看 intent 的键位与示例,再按 starter 写文件" } }
756
+ : readish
757
+ ? capabilityId === "model"
758
+ ? { firstAnswer: { state: "ready", action: LIST_MODELS_ACTION, why: "问的就是模型清单本身:model.list 一次调用直接回答" } }
759
+ : { firstAnswer: { state: "needs-input", question: "要查看哪个对象?给我 schemaCode(或业务名,我列清单核对)", action: LIST_MODELS_ACTION, why: `读路径命中能力 ${capabilityId}:readTools 已内联,只差目标对象 code` } }
760
+ : recipeCommand !== undefined
761
+ ? { firstAnswer: { state: "ready", action: recipeCommand, why: `命中配方 ${recipe.recipe}:describe 首答自带 firstCall 形状,代入你的 schemaCode/字段 code 即可执行` } }
762
+ : { firstAnswer: { state: "needs-input", question: "要操作哪个对象?给我 schemaCode(或模型业务名,我列清单核对)", action: LIST_MODELS_ACTION, why: "未命中配方:需要目标对象 code 才能给出一步可跑的写命令" } }),
693
763
  };
694
764
  }
695
765
  //# sourceMappingURL=goal-routes.js.map
@@ -387,11 +387,27 @@ export function registerReportLane(tools) {
387
387
  const page = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code } })).data;
388
388
  const objectId = String(page.id ?? page.reportObjectId ?? "");
389
389
  if (objectId) {
390
- await biCall(session, {
391
- path: ENDPOINTS.biReportPageSave,
392
- body: { report: { objectId, title: args.name, attributes: "{}", global: JSON.stringify(DEFAULT_REPORT_GLOBAL) } },
393
- auth: authOf(args.corpId, code, args.appCode),
394
- });
390
+ try {
391
+ await biCall(session, {
392
+ path: ENDPOINTS.biReportPageSave,
393
+ body: { report: { objectId, title: args.name, attributes: "{}", global: JSON.stringify(DEFAULT_REPORT_GLOBAL) } },
394
+ auth: authOf(args.corpId, code, args.appCode),
395
+ });
396
+ }
397
+ catch (error) {
398
+ // r19 p14(p9 实证):报表服务未启动时 BI 初始化统一 502,但平台层的
399
+ // 报表页行**已经建了**。裸抛 uncertain 会掩盖这个残留——半成功必须
400
+ // 显式:回执带 halfSuccess 警示 + 清理命令,让 agent 下一调用就能收拾。
401
+ return {
402
+ performed: { errcode: 0, code, appCode: args.appCode, objectId, biInit: "failed" },
403
+ readback: { present: page.code === code, report: reportPageView(page), biInitialized: false },
404
+ halfSuccess: {
405
+ message: `平台层报表页已建(code=${code},objectId=${objectId}),但 BI 初始化失败(${error instanceof Error ? error.message : String(error)})——元数据已残留`,
406
+ cleanup: `omc call report.delete --args '{"code":"${code}"}'(或保留,待报表服务启动后重试 BI 配置)`,
407
+ boundary: "报表服务未启动时 BI 层(/dashboard/v1/*)统一 502——这是环境边界,不要原样重试",
408
+ },
409
+ };
410
+ }
395
411
  }
396
412
  void created;
397
413
  return {
@@ -40,7 +40,7 @@ export function dataSubmitDescription() {
40
40
  `就地语义:${wire.submitInPlace};无流程 readback 必须确认 objectId 与传入 bizObjectId 相同;无 bizObjectId 的 workflowCode 新建单 → PROCESSING + workflowInstanceId,无流程 → 该行 COMPLETED。`,
41
41
  `既有草稿发起流程的唯一支持形状是两段式:先 data.save 带 workflowCode 暂存(${wire.workflowDraftSave}),再以返回的 workItemId 提交。`,
42
42
  `仅无 workflowCode/workItemId/workflowInstanceId 且无 bizObjectId 时保留 save→submit 便捷链(新建记录);只要存在 workItemId 或 workflowInstanceId 就 fail-closed 拒绝(blocker submit-biz-object-id-required),零 runtime/form/submit wire。`,
43
- `formType 自动判定:无任何 workflowCode/workItemId/workflowInstanceId 时**先按模型自动判定**——GET workflow/isworkflow?schemaCode=true 则用 workflow/getWorkflow 解析已发布流程码并**发起流程**(formType="${wire.formTypeWorkflow}",自动先 GET getReplayToken 注入 replayToken,否则 ${wire.replayTokenErrcode} 频繁重复提交);模型无流程 → "${wire.formTypeNoWorkflow}"(无流程提交,走 live 已证链 ${wire.submitChain};单发新建对象会 ${wire.typeCastErrcode},bizObject 勿带字符串 status)或 workItemId+workflowInstanceId(审批待办,agree 通过/驳回,**必须带 bizObjectId 指向在途单据**,否则引擎 NPE ${wire.approvalNpeErrcode})→ "${wire.formTypeWorkflow}"。即「无流程直接提交、有流程发起流程」,不再把有流程模型的新建数据落成游离 sheet 行。`,
43
+ `formType 自动判定:无任何 workflowCode/workItemId/workflowInstanceId 时**先按模型自动判定**——GET workflow/isworkflow?schemaCode=true 则用 workflow/getWorkflow 解析已发布流程码并**发起流程**(formType="${wire.formTypeWorkflow}",自动先 GET getReplayToken 注入 replayToken,否则 ${wire.replayTokenErrcode} 频繁重复提交);模型无流程 → "${wire.formTypeNoWorkflow}"(无流程提交,走 live 已证链 ${wire.submitChain};单发新建对象会 ${wire.typeCastErrcode},bizObject 勿带字符串 status)或 workItemId+workflowInstanceId(审批待办,agree **顶层**通过/驳回(approval 对象里放 agree 会被静默忽略并按通过处理),**必须带 bizObjectId 指向在途单据**,否则引擎 NPE ${wire.approvalNpeErrcode})→ "${wire.formTypeWorkflow}"。即「无流程直接提交、有流程发起流程」,不再把有流程模型的新建数据落成游离 sheet 行。`,
44
44
  "根因提示(环境相关,ticket 78 观测):**某些环境**里模型所在应用未发布时运行时提交稳定 50000,工具会反推并拒 app-not-published。该根因非普遍规律(见 data.save 的同条提示与 docs/issues/omc-cli-findings-2026-09-19.md B17)——遇 50000 先只读核对再判断。",
45
45
  "Pass `sheetCode` explicitly when the model has multiple sheets; otherwise the default-form sheetCode is resolved from bizsheet/get.",
46
46
  `计算字段:${wire.formulaFieldNote}`,
@@ -1290,6 +1290,27 @@ export function registerRuntimeDataLane(tools) {
1290
1290
  `而错误里不会说是哪一个。要么给出真实 id,要么整个省略该键(省略 ≠ 传空串)。`,
1291
1291
  });
1292
1292
  const hasWorkItem = typeof args.workItemId === "string" && args.workItemId.trim().length > 0;
1293
+ // r18b: pure shape check — run before any read/wire so a wrong-result
1294
+ // payload never costs a round trip.
1295
+ if (args.approval && typeof args.approval === "object") {
1296
+ const misplaced = args.approval;
1297
+ if (misplaced.agree !== undefined)
1298
+ throw new ToolRefusalError({
1299
+ status: "refused",
1300
+ blocker: "submit-approval-agree-misplaced",
1301
+ schemaCode: args.schemaCode,
1302
+ message: `approval.agree 不生效:平台只读**顶层** agree(true=通过 / false=驳回)。approval 对象里放 agree 会被**静默忽略并按通过处理**` +
1303
+ `(r18b 真机:approval:{agree:false} 仍写入 result=APPROVE 并把流程走完)。改成顶层参数 {"agree":false,...}。`,
1304
+ });
1305
+ if (misplaced.opinion !== undefined)
1306
+ throw new ToolRefusalError({
1307
+ status: "refused",
1308
+ blocker: "submit-approval-opinion-misplaced",
1309
+ schemaCode: args.schemaCode,
1310
+ message: `approval.opinion 不是平台键,会被静默丢弃(r18b 真机:意见不进 h_biz_comment,content 为 NULL)。` +
1311
+ `审批意见(BizCommentModel)用 **approval.content**;附件 approval.resources;下一步审批人 approval.relUsers。`,
1312
+ });
1313
+ }
1293
1314
  const hasInstance = typeof args.workflowInstanceId === "string" && args.workflowInstanceId.trim().length > 0;
1294
1315
  const targetObjectId = typeof args.bizObjectId === "string" && args.bizObjectId.trim() ? args.bizObjectId.trim() : null;
1295
1316
  if ((hasWorkItem || hasInstance) && !targetObjectId) {
@@ -1688,6 +1709,15 @@ export function registerRuntimeDataLane(tools) {
1688
1709
  // SEPARATE concern from server-side business rules (which DO run on
1689
1710
  // submit) — name that so `portalRulesExecuted:false` is not misread as
1690
1711
  // "no rule ran" (dogfood R2).
1712
+ // r18b: an approval submit that omits `agree` is treated as APPROVE by
1713
+ // the platform (`agree: args.agree !== false`). That default is right
1714
+ // for the portal button, but for an API caller "I forgot the flag" and
1715
+ // "I approve" must not look identical in the receipt.
1716
+ ...(hasWorkItem && args.agree === undefined
1717
+ ? {
1718
+ approvalNote: "本次是审批提交但未传 agree——平台按**通过**处理(顶层 agree 缺省 true)。要驳回必须显式 agree:false;approval.agree 不生效。",
1719
+ }
1720
+ : {}),
1691
1721
  portalRulesExecuted: false,
1692
1722
  portalRulesNote: "portalRulesExecuted 只指门户客户端的必填/控件校验未在 REST 提交时执行;服务端业务规则(含流程 end 规则)照常运行。",
1693
1723
  requiredFields: validation.requiredFields,
@@ -259,7 +259,7 @@ const formulaAggregationCrossModel = Object.freeze({
259
259
  conditionalRequirements: [
260
260
  "**DYNAMIC 条件的两个 code 分属两侧,写反了就静默 0**:`propertyCode` = **来源模型**的字段(这里是 r17_c.nm),`currentPropertyCode` = **目标模型**的字段(这里是 r17_d.srcNo)。语义 = 「来源行的 nm 等于本目标行的 srcNo」。",
261
261
  "`propertyType`/`currentPropertyType` 是 BizPropertyType 的**整数码**(0=SHORT_TEXT 2=NUMERICAL 9=WORK_SHEET…),`operatorType` 是 OperatorType 的**整数码**(1=EQ 6=ITEQ 9=IS_NULL 10=IS_NOT_NULL…)。写名字会被 ObjectMapperUtils 抛 json processing exception 并掩码成 305003;工具会自动转码。",
262
- "`calculateType` 是**字符串**(SUM/COUNT/COUNTIF/SUMIF/AVG/MAX/MIN/LARGE/SMALL/UNION),不是枚举 code。",
262
+ "`calculateType` 是**字符串**(SUM/COUNT/COUNTIF/SUMIF/AVG/MAX/MIN/**LARGE/SMALL/UNION**),不是枚举 code。r18b 真机三种特殊算法:LARGE/SMALL **必须带位置参数** `LARGE({<来源>.<列>},2)`(缺 k → 平台 NumberFormatException → 目标写入 50000);**UNION 是文本拼接**,顶层 `resultType` 必须是 0/LONG_TEXT=1(写 2 会 50000,读回是去重串,如单号本身);SUM/AVG/MAX/MIN/COUNT 用 resultType:2。",
263
263
  "发布后平台会**自动生成一条 t100 CALCULATE_RULE 镜像行**(h_biz_data_rule,propertyCode=字段 code),它是回算触发的前置条件,别手动删。",
264
264
  "**回算时机(r17b 2026-10-26 真机完整行为图谱)**:① 目标行**自己 submit** 时镜像规则一定算(最可靠,不依赖事件);② 来源行 submit/update/delete 触发 `RollUpEventListener`→`FormulaPropertySupport` 事件回算——FIXED+DYNAMIC 混用、多组 OR、COUNTIF/MAX、子表行级条件(propertyCode 带 `.`)都验证过,DELETE 也会回算;③ ⚠️ 事件回算**只在目标模型发布后的几分钟内可靠**,之后静默停止(源端数据变了目标字段不更新、无报错)——重新 `field.publish --force` 目标模型即可恢复。定时巡检/对账场景务必重发或依赖目标行写入路径。",
265
265
  ],
@@ -48,7 +48,9 @@ const workflowSubmit = Object.freeze({
48
48
  tool: "data.submit",
49
49
  args: { environment, schemaCode: model, workflowCode: "<workflow.code>", bizObjectId: "<data.save.performed.bizObjectId>", workflowInstanceId: "<data.save.performed.workflowInstanceId>", workItemId: "<data.save.performed.workItemId>", formData: { name: "recipe draft" } },
50
50
  preconditions: ["同一 recipe 的 data.save 已成功并读回 DRAFT;workflowInstanceId/workItemId/bizObjectId 均非空"],
51
- conditionalRequirements: ["workItemId 或 workflowInstanceId 出现时必须传 bizObjectId;身份只能复制 data.save 的 authoritative readback"],
51
+ conditionalRequirements: [
52
+ "workItemId 或 workflowInstanceId 出现时必须传 bizObjectId;身份只能复制 data.save 的 authoritative readback",
53
+ ],
52
54
  priorToolReferences: [
53
55
  { tool: "data.save", args: { environment, schemaCode: model, workflowCode: "<workflow.code>", formData: { name: "recipe draft" } }, purpose: "提供三个环境生成的提交身份" },
54
56
  { tool: "workitem.list", args: { environment }, purpose: "当 data.save 未直接返回 workItemId 时按 workflowInstanceId 解析待办" },