704 lines
19 KiB
JavaScript
704 lines
19 KiB
JavaScript
const VALID_PIN_TYPES = new Set([
|
|
"power_in",
|
|
"power_out",
|
|
"input",
|
|
"output",
|
|
"bidirectional",
|
|
"passive",
|
|
"analog",
|
|
"ground"
|
|
]);
|
|
|
|
const VALID_NET_CLASSES = new Set([
|
|
"power",
|
|
"ground",
|
|
"signal",
|
|
"analog",
|
|
"differential",
|
|
"clock",
|
|
"bus"
|
|
]);
|
|
const BUILTIN_PART_TYPES = new Set(["resistor", "capacitor", "inductor", "diode", "led", "connector", "generic"]);
|
|
|
|
const GENERIC_DEFAULT_WIDTH = 160;
|
|
const GENERIC_MIN_HEIGHT = 120;
|
|
const GENERIC_PIN_STEP = 18;
|
|
const TEMPLATE_PIN_STEP = 24;
|
|
|
|
const TEMPLATE_DEFS = {
|
|
resistor: {
|
|
category: "passive_resistor",
|
|
body: { width: 120, height: 70 },
|
|
pins: [
|
|
{ name: "1", side: "left", offset: 35, type: "passive" },
|
|
{ name: "2", side: "right", offset: 35, type: "passive" }
|
|
]
|
|
},
|
|
capacitor: {
|
|
category: "passive_capacitor",
|
|
body: { width: 120, height: 70 },
|
|
pins: [
|
|
{ name: "1", side: "left", offset: 35, type: "passive" },
|
|
{ name: "2", side: "right", offset: 35, type: "passive" }
|
|
]
|
|
},
|
|
inductor: {
|
|
category: "passive_inductor",
|
|
body: { width: 120, height: 70 },
|
|
pins: [
|
|
{ name: "1", side: "left", offset: 35, type: "passive" },
|
|
{ name: "2", side: "right", offset: 35, type: "passive" }
|
|
]
|
|
},
|
|
diode: {
|
|
category: "passive_diode",
|
|
body: { width: 120, height: 70 },
|
|
pins: [
|
|
{ name: "A", side: "left", offset: 35, type: "passive" },
|
|
{ name: "K", side: "right", offset: 35, type: "passive" }
|
|
]
|
|
},
|
|
led: {
|
|
category: "passive_led",
|
|
body: { width: 120, height: 70 },
|
|
pins: [
|
|
{ name: "A", side: "left", offset: 35, type: "passive" },
|
|
{ name: "K", side: "right", offset: 35, type: "passive" }
|
|
]
|
|
},
|
|
connector: {
|
|
category: "connector_generic",
|
|
body: { width: 140, height: 90 },
|
|
pins: [
|
|
{ name: "1", side: "left", offset: 24, type: "passive" },
|
|
{ name: "2", side: "left", offset: 48, type: "passive" },
|
|
{ name: "3", side: "right", offset: 24, type: "passive" },
|
|
{ name: "4", side: "right", offset: 48, type: "passive" }
|
|
]
|
|
}
|
|
};
|
|
|
|
function clone(value) {
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function hasTemplateName(sym) {
|
|
const name = String(sym?.template_name ?? "").toLowerCase();
|
|
return Object.prototype.hasOwnProperty.call(TEMPLATE_DEFS, name);
|
|
}
|
|
|
|
function normalizePartName(value) {
|
|
return String(value ?? "")
|
|
.trim()
|
|
.toLowerCase();
|
|
}
|
|
|
|
function symbolIdForPart(partName) {
|
|
return `__part_${partName}`;
|
|
}
|
|
|
|
function isGenericSymbol(sym) {
|
|
if (!sym || typeof sym !== "object") {
|
|
return false;
|
|
}
|
|
if (sym.auto_generated === true) {
|
|
return true;
|
|
}
|
|
const category = String(sym.category ?? "").toLowerCase();
|
|
return category.includes("generic");
|
|
}
|
|
|
|
function pinTypeFromNet(netClass) {
|
|
if (netClass === "ground") {
|
|
return "ground";
|
|
}
|
|
if (netClass === "power") {
|
|
return "power_in";
|
|
}
|
|
if (netClass === "clock") {
|
|
return "input";
|
|
}
|
|
if (netClass === "analog") {
|
|
return "analog";
|
|
}
|
|
return "passive";
|
|
}
|
|
|
|
function inferSymbolTemplate(inst) {
|
|
const ref = String(inst.ref ?? "").toUpperCase();
|
|
const symbol = String(inst.symbol ?? "").toLowerCase();
|
|
const value = String(inst.properties?.value ?? "").toLowerCase();
|
|
const haystack = `${symbol} ${value}`;
|
|
|
|
if (ref.startsWith("R") || /\bres(istor)?\b/.test(haystack)) {
|
|
return "resistor";
|
|
}
|
|
if (ref.startsWith("C") || /\bcap(acitor)?\b/.test(haystack)) {
|
|
return "capacitor";
|
|
}
|
|
if (ref.startsWith("L") || /\bind(uctor)?\b/.test(haystack)) {
|
|
return "inductor";
|
|
}
|
|
if (ref.startsWith("D") || /\bdiod(e)?\b/.test(haystack)) {
|
|
return "diode";
|
|
}
|
|
if (/\bled\b/.test(haystack)) {
|
|
return "led";
|
|
}
|
|
if (ref.startsWith("J") || ref.startsWith("P") || /\b(conn(ector)?|header)\b/.test(haystack)) {
|
|
return "connector";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function buildTemplateSymbol(symbolId, templateName) {
|
|
const template = TEMPLATE_DEFS[templateName];
|
|
if (!template) {
|
|
return null;
|
|
}
|
|
|
|
const pins = template.pins.map((p, idx) => ({
|
|
name: p.name,
|
|
number: String(idx + 1),
|
|
side: p.side,
|
|
offset: p.offset,
|
|
type: p.type
|
|
}));
|
|
|
|
return {
|
|
symbol_id: symbolId,
|
|
category: template.category,
|
|
auto_generated: true,
|
|
template_name: templateName,
|
|
body: {
|
|
width: template.body.width,
|
|
height: template.body.height
|
|
},
|
|
pins,
|
|
graphics: {
|
|
primitives: [
|
|
{
|
|
type: "rect",
|
|
x: 0,
|
|
y: 0,
|
|
w: template.body.width,
|
|
h: template.body.height
|
|
}
|
|
]
|
|
}
|
|
};
|
|
}
|
|
|
|
function buildUsageByRef(model) {
|
|
const usage = new Map();
|
|
for (const net of model.nets) {
|
|
for (const node of net.nodes ?? []) {
|
|
const item = usage.get(node.ref) ?? new Map();
|
|
const entry = item.get(node.pin) ?? { pin: node.pin, classes: new Set() };
|
|
entry.classes.add(net.class);
|
|
item.set(node.pin, entry);
|
|
usage.set(node.ref, item);
|
|
}
|
|
}
|
|
return usage;
|
|
}
|
|
|
|
function buildUsageBySymbol(model) {
|
|
const byRef = new Map((model.instances ?? []).map((inst) => [inst.ref, inst]));
|
|
const usage = new Map();
|
|
|
|
for (const net of model.nets ?? []) {
|
|
for (const node of net.nodes ?? []) {
|
|
const inst = byRef.get(node.ref);
|
|
if (!inst) {
|
|
continue;
|
|
}
|
|
const symbolId = inst.symbol;
|
|
const symbolUsage = usage.get(symbolId) ?? new Map();
|
|
const entry = symbolUsage.get(node.pin) ?? { pin: node.pin, classes: new Set() };
|
|
entry.classes.add(net.class);
|
|
symbolUsage.set(node.pin, entry);
|
|
usage.set(symbolId, symbolUsage);
|
|
}
|
|
}
|
|
|
|
return usage;
|
|
}
|
|
|
|
function pinCountToHeight(pinCount) {
|
|
const rows = Math.max(4, pinCount + 1);
|
|
return Math.max(GENERIC_MIN_HEIGHT, rows * GENERIC_PIN_STEP);
|
|
}
|
|
|
|
function buildPinsFromUsage(ref, pinUsage) {
|
|
const names = [...pinUsage.values()].map((x) => x.pin).sort((a, b) => a.localeCompare(b));
|
|
if (!names.length) {
|
|
return [
|
|
{
|
|
name: "P1",
|
|
number: "1",
|
|
side: "left",
|
|
offset: GENERIC_PIN_STEP,
|
|
type: "passive"
|
|
}
|
|
];
|
|
}
|
|
|
|
const left = names.filter((_, idx) => idx % 2 === 0);
|
|
const right = names.filter((_, idx) => idx % 2 === 1);
|
|
let leftOffset = GENERIC_PIN_STEP;
|
|
let rightOffset = GENERIC_PIN_STEP;
|
|
|
|
const ordered = [...left, ...right];
|
|
const pins = [];
|
|
for (let i = 0; i < ordered.length; i += 1) {
|
|
const name = ordered[i];
|
|
const classes = [...(pinUsage.get(name)?.classes ?? new Set())].sort();
|
|
const preferredClass = classes[0] ?? "signal";
|
|
const side = left.includes(name) ? "left" : "right";
|
|
const offset = side === "left" ? leftOffset : rightOffset;
|
|
if (side === "left") {
|
|
leftOffset += GENERIC_PIN_STEP;
|
|
} else {
|
|
rightOffset += GENERIC_PIN_STEP;
|
|
}
|
|
|
|
pins.push({
|
|
name,
|
|
number: String(i + 1),
|
|
side,
|
|
offset,
|
|
type: pinTypeFromNet(preferredClass)
|
|
});
|
|
}
|
|
|
|
return pins;
|
|
}
|
|
|
|
function ensureGenericSymbols(model, issues, enableGenericSymbols) {
|
|
if (!enableGenericSymbols) {
|
|
return model;
|
|
}
|
|
|
|
const next = clone(model);
|
|
next.symbols = next.symbols ?? {};
|
|
const usageByRef = buildUsageByRef(next);
|
|
|
|
for (const inst of next.instances ?? []) {
|
|
const part = normalizePartName(inst.part);
|
|
if (!part) {
|
|
continue;
|
|
}
|
|
|
|
if (!BUILTIN_PART_TYPES.has(part)) {
|
|
issues.push({
|
|
code: "invalid_part_type",
|
|
message: `Instance '${inst.ref}' uses unsupported part '${inst.part}'.`,
|
|
severity: "error",
|
|
path: `instances.${inst.ref}.part`
|
|
});
|
|
continue;
|
|
}
|
|
|
|
inst.part = part;
|
|
if (!inst.symbol) {
|
|
inst.symbol = symbolIdForPart(part);
|
|
}
|
|
|
|
if (next.symbols[inst.symbol]) {
|
|
continue;
|
|
}
|
|
|
|
if (part === "generic") {
|
|
next.symbols[inst.symbol] = {
|
|
symbol_id: inst.symbol,
|
|
category: "generic",
|
|
auto_generated: true
|
|
};
|
|
continue;
|
|
}
|
|
|
|
const templateSymbol = buildTemplateSymbol(inst.symbol, part);
|
|
if (templateSymbol) {
|
|
next.symbols[inst.symbol] = templateSymbol;
|
|
}
|
|
}
|
|
|
|
for (const inst of next.instances ?? []) {
|
|
if (next.symbols[inst.symbol]) {
|
|
continue;
|
|
}
|
|
|
|
const templateName = inferSymbolTemplate(inst);
|
|
const templated = templateName ? buildTemplateSymbol(inst.symbol, templateName) : null;
|
|
|
|
if (templated) {
|
|
next.symbols[inst.symbol] = templated;
|
|
issues.push({
|
|
code: "auto_template_symbol_created",
|
|
message: `Created '${templateName}' symbol '${inst.symbol}' for instance '${inst.ref}'.`,
|
|
severity: "warning",
|
|
path: `instances.${inst.ref}.symbol`
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const pinUsage = usageByRef.get(inst.ref) ?? new Map();
|
|
const pins = buildPinsFromUsage(inst.ref, pinUsage);
|
|
|
|
next.symbols[inst.symbol] = {
|
|
symbol_id: inst.symbol,
|
|
category: "generic",
|
|
auto_generated: true,
|
|
body: {
|
|
width: GENERIC_DEFAULT_WIDTH,
|
|
height: pinCountToHeight(pins.length)
|
|
},
|
|
pins,
|
|
graphics: {
|
|
primitives: [
|
|
{
|
|
type: "rect",
|
|
x: 0,
|
|
y: 0,
|
|
w: GENERIC_DEFAULT_WIDTH,
|
|
h: pinCountToHeight(pins.length)
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
issues.push({
|
|
code: "auto_generic_symbol_created",
|
|
message: `Created generic symbol '${inst.symbol}' for instance '${inst.ref}'.`,
|
|
severity: "warning",
|
|
path: `instances.${inst.ref}.symbol`
|
|
});
|
|
}
|
|
|
|
const usageBySymbol = buildUsageBySymbol(next);
|
|
for (const [id, sym] of Object.entries(next.symbols)) {
|
|
if (typeof sym !== "object" || !sym) {
|
|
continue;
|
|
}
|
|
|
|
if (!sym.symbol_id) {
|
|
sym.symbol_id = id;
|
|
issues.push({
|
|
code: "auto_symbol_id_filled",
|
|
message: `Filled missing symbol_id for '${id}'.`,
|
|
severity: "warning",
|
|
path: `symbols.${id}.symbol_id`
|
|
});
|
|
}
|
|
|
|
const templateName = String(sym.template_name ?? "").toLowerCase();
|
|
if (hasTemplateName(sym)) {
|
|
const templated = buildTemplateSymbol(id, templateName);
|
|
let templateHydrated = false;
|
|
if (!sym.category) {
|
|
sym.category = templated.category;
|
|
issues.push({
|
|
code: "auto_symbol_category_filled",
|
|
message: `Filled missing category for '${id}' from template '${templateName}'.`,
|
|
severity: "warning",
|
|
path: `symbols.${id}.category`
|
|
});
|
|
}
|
|
if (!sym.body || sym.body.width == null || sym.body.height == null) {
|
|
sym.body = { ...(sym.body ?? {}), ...templated.body };
|
|
templateHydrated = true;
|
|
}
|
|
if (!Array.isArray(sym.pins) || sym.pins.length === 0) {
|
|
sym.pins = templated.pins;
|
|
templateHydrated = true;
|
|
}
|
|
if (!sym.graphics) {
|
|
sym.graphics = templated.graphics;
|
|
}
|
|
if (templateHydrated) {
|
|
issues.push({
|
|
code: "auto_template_symbol_hydrated",
|
|
message: `Hydrated template fields for '${id}' (${templateName}).`,
|
|
severity: "warning",
|
|
path: `symbols.${id}`
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!sym.category) {
|
|
sym.category = "generic";
|
|
issues.push({
|
|
code: "auto_symbol_category_filled",
|
|
message: `Filled missing category for '${id}' as generic.`,
|
|
severity: "warning",
|
|
path: `symbols.${id}.category`
|
|
});
|
|
}
|
|
|
|
const genericCategory = String(sym.category ?? "").toLowerCase().includes("generic");
|
|
if (!genericCategory) {
|
|
continue;
|
|
}
|
|
|
|
let genericHydrated = false;
|
|
if (!Array.isArray(sym.pins) || sym.pins.length === 0) {
|
|
const pinUsage = usageBySymbol.get(id) ?? new Map();
|
|
sym.pins = buildPinsFromUsage(id, pinUsage);
|
|
genericHydrated = true;
|
|
}
|
|
|
|
if (!sym.body || sym.body.width == null || sym.body.height == null) {
|
|
sym.body = {
|
|
width: sym.body?.width ?? GENERIC_DEFAULT_WIDTH,
|
|
height: Math.max(sym.body?.height ?? GENERIC_MIN_HEIGHT, pinCountToHeight(sym.pins.length))
|
|
};
|
|
genericHydrated = true;
|
|
}
|
|
|
|
if (genericHydrated) {
|
|
issues.push({
|
|
code: "auto_generic_symbol_hydrated",
|
|
message: `Hydrated generic fields for '${id}' from net usage.`,
|
|
severity: "warning",
|
|
path: `symbols.${id}`
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const net of next.nets ?? []) {
|
|
for (const node of net.nodes ?? []) {
|
|
const inst = (next.instances ?? []).find((x) => x.ref === node.ref);
|
|
if (!inst) {
|
|
continue;
|
|
}
|
|
const sym = next.symbols[inst.symbol];
|
|
if (!sym || !isGenericSymbol(sym)) {
|
|
continue;
|
|
}
|
|
|
|
const hasPin = Array.isArray(sym.pins) && sym.pins.some((p) => p.name === node.pin);
|
|
if (hasPin) {
|
|
continue;
|
|
}
|
|
|
|
const side = (sym.pins?.length ?? 0) % 2 === 0 ? "left" : "right";
|
|
const sameSideCount = (sym.pins ?? []).filter((p) => p.side === side).length;
|
|
const pinStep = sym.template_name ? TEMPLATE_PIN_STEP : GENERIC_PIN_STEP;
|
|
const offset = pinStep + sameSideCount * pinStep;
|
|
const nextNumber = String((sym.pins?.length ?? 0) + 1);
|
|
|
|
sym.pins = sym.pins ?? [];
|
|
sym.pins.push({
|
|
name: node.pin,
|
|
number: nextNumber,
|
|
side,
|
|
offset,
|
|
type: pinTypeFromNet(net.class)
|
|
});
|
|
|
|
sym.body = sym.body ?? { width: GENERIC_DEFAULT_WIDTH, height: GENERIC_MIN_HEIGHT };
|
|
sym.body.width = sym.body.width ?? GENERIC_DEFAULT_WIDTH;
|
|
sym.body.height = Math.max(sym.body.height ?? GENERIC_MIN_HEIGHT, pinCountToHeight(sym.pins.length));
|
|
|
|
issues.push({
|
|
code: "auto_generic_pin_created",
|
|
message: `Added pin '${node.pin}' to generic symbol '${inst.symbol}' from net '${net.name}'.`,
|
|
severity: "warning",
|
|
path: `symbols.${inst.symbol}.pins.${node.pin}`
|
|
});
|
|
}
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
export function validateModel(model, options = {}) {
|
|
const issues = [];
|
|
const enableGenericSymbols = options.generic_symbols !== false;
|
|
|
|
if (!model || typeof model !== "object") {
|
|
return {
|
|
model: null,
|
|
issues: [
|
|
{
|
|
code: "invalid_root",
|
|
message: "Root payload must be an object.",
|
|
severity: "error",
|
|
path: "$"
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
if (!model.symbols || typeof model.symbols !== "object") {
|
|
issues.push({
|
|
code: "symbols_missing",
|
|
message: "symbols must be an object map.",
|
|
severity: "error",
|
|
path: "symbols"
|
|
});
|
|
}
|
|
if (!Array.isArray(model.instances)) {
|
|
issues.push({
|
|
code: "instances_missing",
|
|
message: "instances must be an array.",
|
|
severity: "error",
|
|
path: "instances"
|
|
});
|
|
}
|
|
if (!Array.isArray(model.nets)) {
|
|
issues.push({
|
|
code: "nets_missing",
|
|
message: "nets must be an array.",
|
|
severity: "error",
|
|
path: "nets"
|
|
});
|
|
}
|
|
|
|
if (issues.some((x) => x.severity === "error")) {
|
|
return { model: null, issues };
|
|
}
|
|
|
|
const workingModel = ensureGenericSymbols(model, issues, enableGenericSymbols);
|
|
const symbolIds = new Set(Object.keys(workingModel.symbols));
|
|
|
|
for (const [id, sym] of Object.entries(workingModel.symbols)) {
|
|
if (sym.symbol_id !== id) {
|
|
issues.push({
|
|
code: "symbol_id_mismatch",
|
|
message: `symbol_id '${sym.symbol_id}' must match map key '${id}'.`,
|
|
severity: "error",
|
|
path: `symbols.${id}.symbol_id`
|
|
});
|
|
}
|
|
|
|
if (!Array.isArray(sym.pins) || sym.pins.length === 0) {
|
|
issues.push({
|
|
code: "symbol_no_pins",
|
|
message: `Symbol '${id}' has no pins.`,
|
|
severity: "error",
|
|
path: `symbols.${id}.pins`
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const pinNames = new Set();
|
|
for (const pin of sym.pins) {
|
|
if (pinNames.has(pin.name)) {
|
|
issues.push({
|
|
code: "duplicate_pin_name",
|
|
message: `Symbol '${id}' has duplicate pin '${pin.name}'.`,
|
|
severity: "error",
|
|
path: `symbols.${id}.pins`
|
|
});
|
|
}
|
|
pinNames.add(pin.name);
|
|
|
|
if (!VALID_PIN_TYPES.has(pin.type)) {
|
|
issues.push({
|
|
code: "invalid_pin_type",
|
|
message: `Invalid pin type '${pin.type}' on ${id}.${pin.name}.`,
|
|
severity: "error",
|
|
path: `symbols.${id}.pins.${pin.name}.type`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const refs = new Set();
|
|
const instanceSymbol = new Map();
|
|
for (const inst of workingModel.instances) {
|
|
if (refs.has(inst.ref)) {
|
|
issues.push({
|
|
code: "duplicate_ref",
|
|
message: `Duplicate instance ref '${inst.ref}'.`,
|
|
severity: "error",
|
|
path: "instances"
|
|
});
|
|
}
|
|
refs.add(inst.ref);
|
|
|
|
if (!inst.symbol) {
|
|
issues.push({
|
|
code: "instance_symbol_or_part_missing",
|
|
message: `Instance '${inst.ref}' must define either 'symbol' or 'part'.`,
|
|
severity: "error",
|
|
path: `instances.${inst.ref}`
|
|
});
|
|
} else if (!symbolIds.has(inst.symbol)) {
|
|
issues.push({
|
|
code: "unknown_symbol",
|
|
message: `Instance '${inst.ref}' references unknown symbol '${inst.symbol}'.`,
|
|
severity: "error",
|
|
path: `instances.${inst.ref}.symbol`
|
|
});
|
|
}
|
|
instanceSymbol.set(inst.ref, inst.symbol);
|
|
}
|
|
|
|
const netNames = new Set();
|
|
for (const net of workingModel.nets) {
|
|
if (netNames.has(net.name)) {
|
|
issues.push({
|
|
code: "duplicate_net_name",
|
|
message: `Duplicate net '${net.name}'.`,
|
|
severity: "error",
|
|
path: `nets.${net.name}`
|
|
});
|
|
}
|
|
netNames.add(net.name);
|
|
|
|
if (!VALID_NET_CLASSES.has(net.class)) {
|
|
issues.push({
|
|
code: "invalid_net_class",
|
|
message: `Invalid net class '${net.class}' on '${net.name}'.`,
|
|
severity: "error",
|
|
path: `nets.${net.name}.class`
|
|
});
|
|
}
|
|
|
|
if (!Array.isArray(net.nodes) || net.nodes.length < 2) {
|
|
issues.push({
|
|
code: "net_too_small",
|
|
message: `Net '${net.name}' must contain at least two nodes.`,
|
|
severity: "error",
|
|
path: `nets.${net.name}.nodes`
|
|
});
|
|
continue;
|
|
}
|
|
|
|
for (const node of net.nodes) {
|
|
const symId = instanceSymbol.get(node.ref);
|
|
if (!symId) {
|
|
issues.push({
|
|
code: "unknown_ref_in_net",
|
|
message: `Net '${net.name}' references unknown component '${node.ref}'.`,
|
|
severity: "error",
|
|
path: `nets.${net.name}.nodes`
|
|
});
|
|
continue;
|
|
}
|
|
const sym = workingModel.symbols[symId];
|
|
if (!sym || !Array.isArray(sym.pins)) {
|
|
continue;
|
|
}
|
|
const foundPin = sym.pins.some((p) => p.name === node.pin);
|
|
if (!foundPin) {
|
|
issues.push({
|
|
code: "unknown_pin_in_net",
|
|
message: `Net '${net.name}' references unknown pin '${node.ref}.${node.pin}'.`,
|
|
severity: "error",
|
|
path: `nets.${net.name}.nodes`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
model: issues.some((x) => x.severity === "error") ? null : workingModel,
|
|
issues
|
|
};
|
|
}
|