- /console/rules tabs: rule groups, custom conditions, defaults, acquisition policies, suppressions - editor: rule string parsed live via /rules/parse (position + suggestions) and synced both ways with layer cards; hard excludes; expectedRevision conflicts keep local edits - preview against recent releases or pasted titles with pri / level / reasons / trace - single-rule groups shown as preference tags; disable/delete writes suppression records - /console/preferences redirects to /console/rules (origin passed through) Co-Authored-By: Claude Opus 5.5 <[email protected]>
116 lines
4.0 KiB
TypeScript
116 lines
4.0 KiB
TypeScript
import type { RuleExpr, RuleLayer } from "@umt/contracts";
|
|
|
|
// 规则组层的编辑模型:一层 = 顶层 且/或 + 若干条目;条目是条件(可取反)或一个括号组(组内同一种 且/或)。
|
|
// 覆盖 MP 规则串的常见写法;更深的嵌套只能在规则串里改(cardsOf 返回 null)。
|
|
|
|
export type Op = "and" | "or";
|
|
export type AtomItem = { kind: "atom"; id: string; neg: boolean };
|
|
export type GroupItem = { kind: "group"; op: Op; neg: boolean; items: AtomItem[] };
|
|
export type LayerItem = AtomItem | GroupItem;
|
|
export type LayerCard = { name?: string; op: Op; items: LayerItem[] };
|
|
|
|
/** 规范化:原子大写,层内 ! > & > |,只加必要括号(与后端 /rules/serialize 同口径) */
|
|
export function exprToString(e: RuleExpr, parent: "top" | "and" | "or" | "not" = "top"): string {
|
|
switch (e.op) {
|
|
case "atom":
|
|
return e.id.toUpperCase();
|
|
case "not":
|
|
return `!${exprToString(e.arg, "not")}`;
|
|
case "and": {
|
|
const s = e.args.map((a) => exprToString(a, "and")).join(" & ");
|
|
return parent === "not" ? `(${s})` : s;
|
|
}
|
|
case "or": {
|
|
const s = e.args.map((a) => exprToString(a, "or")).join(" | ");
|
|
return parent === "and" || parent === "not" ? `(${s})` : s;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function layersToString(layers: RuleLayer[]): string {
|
|
return layers.map((l) => exprToString(l.expr)).join(" > ");
|
|
}
|
|
|
|
function atomOf(e: RuleExpr): AtomItem | null {
|
|
if (e.op === "atom") return { kind: "atom", id: e.id.toUpperCase(), neg: false };
|
|
if (e.op === "not" && e.arg.op === "atom") return { kind: "atom", id: e.arg.id.toUpperCase(), neg: true };
|
|
return null;
|
|
}
|
|
|
|
function groupOf(e: RuleExpr): GroupItem | null {
|
|
let neg = false;
|
|
let inner = e;
|
|
if (inner.op === "not") {
|
|
neg = true;
|
|
inner = inner.arg;
|
|
}
|
|
if (inner.op !== "and" && inner.op !== "or") return null;
|
|
const items: AtomItem[] = [];
|
|
for (const a of inner.args) {
|
|
const at = atomOf(a);
|
|
if (!at) return null;
|
|
items.push(at);
|
|
}
|
|
return { kind: "group", op: inner.op, neg, items };
|
|
}
|
|
|
|
/** 表达式 → 卡片;表达不了(嵌套超过两层)返回 null */
|
|
export function cardOf(layer: RuleLayer): LayerCard | null {
|
|
const e = layer.expr;
|
|
const single = atomOf(e);
|
|
if (single) return { name: layer.name, op: "and", items: [single] };
|
|
if (e.op === "not") {
|
|
const g = groupOf(e);
|
|
return g ? { name: layer.name, op: "and", items: [g] } : null;
|
|
}
|
|
if (e.op !== "and" && e.op !== "or") return null;
|
|
const items: LayerItem[] = [];
|
|
for (const a of e.args) {
|
|
const it = atomOf(a) ?? groupOf(a);
|
|
if (!it) return null;
|
|
items.push(it);
|
|
}
|
|
return { name: layer.name, op: e.op, items };
|
|
}
|
|
|
|
export function cardsOf(layers: RuleLayer[]): LayerCard[] | null {
|
|
const out: LayerCard[] = [];
|
|
for (const l of layers) {
|
|
const c = cardOf(l);
|
|
if (!c) return null;
|
|
out.push(c);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function atomExpr(a: AtomItem): RuleExpr {
|
|
const e: RuleExpr = { op: "atom", id: a.id };
|
|
return a.neg ? { op: "not", arg: e } : e;
|
|
}
|
|
|
|
function itemExpr(it: LayerItem): RuleExpr | null {
|
|
if (it.kind === "atom") return atomExpr(it);
|
|
if (it.items.length === 0) return null;
|
|
const inner: RuleExpr =
|
|
it.items.length === 1 ? atomExpr(it.items[0] as AtomItem) : { op: it.op, args: it.items.map(atomExpr) };
|
|
return it.neg ? { op: "not", arg: inner } : inner;
|
|
}
|
|
|
|
/** 卡片 → 层;空层返回 null 的 expr(调用方据此提示) */
|
|
export function cardToLayer(c: LayerCard): RuleLayer | null {
|
|
const args = c.items.map(itemExpr).filter((x): x is RuleExpr => !!x);
|
|
if (args.length === 0) return null;
|
|
const expr: RuleExpr = args.length === 1 ? (args[0] as RuleExpr) : { op: c.op, args };
|
|
return c.name ? { name: c.name, expr } : { expr };
|
|
}
|
|
|
|
/** 表达式里引用的全部条件 id */
|
|
export function atomsIn(e: RuleExpr, out = new Set<string>()): Set<string> {
|
|
if (e.op === "atom") out.add(e.id.toUpperCase());
|
|
else if (e.op === "not") atomsIn(e.arg, out);
|
|
else for (const a of e.args) atomsIn(a, out);
|
|
return out;
|
|
}
|
|
|
|
export const OP_LABEL: Record<Op, string> = { and: "且", or: "或" };
|