Astra DS component documentation (figma-agent/astra-ds-doc-build.md)

Источник: ds-skills/src/skills/astra-figma-doc-build/figma-agent/astra-ds-doc-build.md — скилл агента Figma одним файлом, часть скилла astra-figma-doc-build. Как его опубликовать — figma-agent/README.md.

Шапка скилла:

name: astra-ds-doc-build
description: "Собирает документацию выделенного компонента Astra DS по шаблону ds-component из библиотеки «Astra DS: Docs» — секция из трёх страниц: «История изменений», «Спецификация», «❖ Компонент». Используй этот скилл всегда, когда в команде Rossel просят задокументировать компонент Astra DS, собрать или пересобрать его документацию, оформить компонент по ds-component, сделать секцию документации или добавить запись в историю изменений компонента. Работает только с атомами библиотеки «Astra DS: Docs»."

You build a documentation SECTION next to the selected Astra DS component. Every visual piece comes from the Astra DS: Docs library (the ds-component template and its atoms). You never draw anything from scratch and never use components from any other file or library.

Language

  • Talk to the user in Russian. Every question, status line and final report is in Russian.
  • Every text you write into Figma is in Russian — the templates in «Texts» are already Russian; copy them verbatim.
  • These instructions are in English only for precision. Never answer the user in English.

Scope

  • Works only in the Rossel team, only with the Astra DS: Docs library. The atom keys below belong to that library. If an atom cannot be found, stop (Step 0) — never substitute anything.
  • Recommend running in a copy of the working file: the build moves the component set into the new section. The set's key, properties and all its instances stay the same.

Output

One SECTION named after the component, three pages left to right:

  1. История изменений — ds-log entries, oldest on top, newest at the bottom.
  2. Спецификация — lead paragraph, «Структура» block with Dev Mode annotations, then one block per VARIANT property and per own BOOLEAN property, in declaration order.
  3. ❖ Компонент — the component set itself, axis labels around it, version in the footer.

«Рекомендации» and «Редактура» are not built: their content cannot be derived from the component. Tell the user to add them by hand from the ds-component template.

How to run code

Run each step as Figma Plugin API JavaScript. The code is the body of an async function: top-level await and return are allowed; only the returned value is visible.

  • Paste the Helpers block (below) at the top of every step. Nothing survives between steps.
  • Replace the UPPER_CASE placeholders with the ids returned by earlier steps, as strings.
  • One step per call. After Steps 2, 3, 4 and 5 take a screenshot of the new page and check it: no overflow, no overlap, no clipped text.
  • A step failed — read the error, fix the cause, rerun only that step. Never rerun blindly.
  • Do not use ?., ?? or structuredClone.

Hard rules

  1. Atoms only by key from the table in Helpers. Names in the library are duplicated — never search by name.
  2. Never touch the source component: properties, layers, name, description stay as they are. The only allowed action is moving the set into «❖ Компонент» (Step 5).
  3. Never edit the page header ds-head.
  4. Never set visuals by value (fills, font sizes, radii) on created nodes; everything comes from atoms.
  5. No numbers in documentation text. Sizes, spacing and colours go into annotations.
  6. Facts come from the component, wording only from «Texts». Never write: when to use which variant, what styles mean, text rules, do/don't advice, a layer's purpose not obvious from its name.
  7. Write only inside your own SECTION. Never delete the old documentation section — offer it in the report.

Helpers

const K = {
  pageLog: "cc0b93384365461d9de59ebef15fbbe2f7c7ee4c", pageSpec: "54dba64bb1ed6734946f020991c07b30459cc21f",
  pageComp: "cb2cff7d2706d72096e1e206734e05e275771489", log: "00d9e32bc28904a27517489b84f9f281a39b2011",
  para: "ca2f04a5efaa20c9d9ae32a396330b7a13e696e9", mc: "f9ee550e7ecc537fc63075015db4c76d0355a619",
  state: "ddafef2738b4d3183aa5e0e418da8fa23ede1cf2", label: "9d9fc72e383e614ac585a65acdeb7a621e094875",
};
const IS_SET = { log: 1, para: 1, mc: 1, state: 1, label: 1 };
async function atom(name) {
  const key = K[name];
  try { return IS_SET[name] ? await figma.importComponentSetByKeyAsync(key) : await figma.importComponentByKeyAsync(key); }
  catch (e) {
    for (const p of figma.root.children) {
      await p.loadAsync();
      const hit = p.findAllWithCriteria({ types: [IS_SET[name] ? "COMPONENT_SET" : "COMPONENT"] }).find((n) => n.key === key);
      if (hit) return hit;
    }
    throw new Error("Нет атома " + name + " из «Astra DS: Docs». Включите библиотеку: Assets → Libraries.");
  }
}
async function loadFonts(nodes) {
  const map = new Map();
  for (const root of nodes) for (const t of root.findAllWithCriteria({ types: ["TEXT"] }))
    for (const s of t.getStyledTextSegments(["fontName"])) map.set(s.fontName.family + "|" + s.fontName.style, s.fontName);
  await Promise.all(Array.from(map.values()).map((f) => figma.loadFontAsync(f)));
}
function propKey(inst, name, type) {
  const props = inst.componentProperties;
  const hits = Object.keys(props).filter((k) => k.split("#")[0] === name && (!type || props[k].type === type));
  if (hits.length !== 1) throw new Error("property " + name + " (" + (type || "any") + "): " + hits.length + " matches on " + inst.name);
  return hits[0];
}
function setProps(inst, map) {
  const out = {};
  for (const name of Object.keys(map)) {
    const v = map[name];
    let key;
    if (typeof v === "boolean") key = propKey(inst, name, "BOOLEAN");
    else { try { key = propKey(inst, name, "TEXT"); } catch (_e) { key = propKey(inst, name, "VARIANT"); } }
    out[key] = v;
  }
  inst.setProperties(out);
}
function variantOf(set, props) {
  const hit = set.children.find((c) => Object.keys(props).every((k) => c.variantProperties[k] === props[k]));
  if (!hit) throw new Error(set.name + ": no variant " + JSON.stringify(props));
  return hit;
}
const findInst = (root, name) => root.findOne((n) => n.type === "INSTANCE" && n.name === name);
const findSlot = (root, name) => root.findOne((n) => n.type === "SLOT" && n.name === name);
function clearSlotWithMarker(slot) {
  const m = figma.createFrame(); m.name = "__MARKER__"; m.resize(1, 1); slot.appendChild(m);
  for (;;) { const d = slot.children.find((c) => c.name !== "__MARKER__"); if (!d) break; d.remove(); }
  slot.children.find((c) => c.name === "__MARKER__").remove();
}
const NBSP = String.fromCharCode(160);
function nb(s) {
  let t = String(s);
  for (let i = 0; i < 2; i++) t = t.replace(/(^|[\s(«])([а-яёА-ЯЁ]{1,2}|для|без|при|под|над|про|или|как) /g, "$1$2" + NBSP);
  return t.replace(/ (—|–)/g, NBSP + "$1").replace(/(\d) (?=[^\s\d—–])/g, "$1" + NBSP);
}
function plural(n, f) { const a = Math.abs(n) % 100, b = a % 10; return a > 10 && a < 20 ? f[2] : b > 1 && b < 5 ? f[1] : b === 1 ? f[0] : f[2]; }
function addPage(section, pageComponent) {
  const frame = pageComponent.createInstance().detachInstance();
  const others = section.children.slice();
  section.appendChild(frame);
  frame.name = pageComponent.name;
  frame.x = others.reduce((m, c) => Math.max(m, c.x + c.width + 100), 100);
  frame.y = 100;
  return { frame, content: frame.findChild((n) => n.name === "Content") };
}

Step 0 — check the input and the library (read only)

Ask the user to select the component set if nothing suitable is selected. Run:

const sel = figma.currentPage.selection;
if (sel.length !== 1) return { stop: "Выделите один компонент: набор вариантов или отдельный компонент." };
let node = sel[0];
if (node.type === "COMPONENT" && node.parent && node.parent.type === "COMPONENT_SET") node = node.parent;
if (node.type !== "COMPONENT_SET" && node.type !== "COMPONENT")
  return { stop: "Выделен слой «" + node.name + "» (" + node.type + "). Нужен сам компонент — у экземпляра откройте Go to main component." };
const missing = [];
for (const n of Object.keys(K)) { try { await atom(n); } catch (e) { missing.push(n); } }
if (missing.length) return { stop: "Нет атомов «Astra DS: Docs»: " + missing.join(", ") + ". Включите библиотеку: Assets → Libraries." };
const defs = node.componentPropertyDefinitions;
const props = Object.keys(defs).map((k) => ({ name: k.split("#")[0], type: defs[k].type, values: defs[k].variantOptions }));
let s = node.parent; while (s && s.type !== "SECTION" && s.type !== "PAGE") s = s.parent;
const oldLog = s && s.type === "SECTION" ? s.children.find((c) => c.name === "История изменений") : null;
return { setId: node.id, name: node.name, type: node.type, description: node.description,
  variants: node.type === "COMPONENT_SET" ? node.children.length : 1, props,
  rebuild: oldLog ? { sectionId: s.id, sectionName: s.name, logId: oldLog.id } : null };

Report the facts to the user in Russian (name, variants, properties in order). Stop on stop. If rebuild is not null, documentation already exists: ask what changed and the change type — «Новая версия», «Доработка компонента» or «Исправление бага». No change described → no new entry.

Step 1 — section

const set = await figma.getNodeByIdAsync("SET_ID");
let page = set; while (page.type !== "PAGE") page = page.parent;
const right = page.children.reduce((m, n) => Math.max(m, n.x + n.width), 0);
const section = figma.createSection();
page.appendChild(section);
section.name = set.name; section.x = right + 400; section.y = 0;
section.resizeWithoutConstraints(4000, 4000);
const V = { base: "27e361c2f3293fcb62995b9c94b1eb22b64490d3", film: "fe38f9ec9248426ff35d8322cb882f61b23ea523",
  stroke: "e3ac4487860ac9fdce1eaee3f42c678ca573cf19", radius: "6c0eb5739ed61e6eebcc4dea47f415a74e8119a1" };
const v = {};
for (const k of Object.keys(V)) v[k] = await figma.variables.importVariableByKeyAsync(V[k]);
const solid = (op) => ({ type: "SOLID", color: { r: 0, g: 0, b: 0 }, opacity: op });
section.fills = [figma.variables.setBoundVariableForPaint(solid(1), "color", v.base),
  figma.variables.setBoundVariableForPaint(solid(0.01), "color", v.film)];
section.strokes = [figma.variables.setBoundVariableForPaint(solid(0.4), "color", v.stroke)];
section.strokeWeight = 1; section.strokeAlign = "INSIDE";
for (const c of ["topLeftRadius", "topRightRadius", "bottomLeftRadius", "bottomRightRadius"]) section.setBoundVariable(c, v.radius);
return { sectionId: section.id };

Step 2 — «История изменений»

Set CHANGE: first build or no change described → null; otherwise { type: "Доработка компонента", text: "<the user's words>" }. OLD_LOG_ID — rebuild.logId from Step 0, or "". DATE — today, two digits each.

const section = await figma.getNodeByIdAsync("SECTION_ID");
const CHANGE = null;
const OLD_LOG_ID = "";
const DATE = { d: "DD", m: "MM", y: "YY" };
const pageLog = await atom("pageLog"), logSet = await atom("log");
await loadFonts([pageLog, logSet.defaultVariant]);
const { frame, content } = addPage(section, pageLog);
const demo = content.children.slice();
const sizing = demo.length ? demo[0].layoutSizingHorizontal : "FILL";
const readVersion = (log) => {
  const v = findInst(log, "Версия");
  const g = (n) => Number(v.componentProperties[propKey(v, n, "TEXT")].value);
  return { major: g("major"), minor: g("minor"), patch: g("patch") };
};
let version = null, carried = 0;
if (OLD_LOG_ID) {
  const old = await figma.getNodeByIdAsync(OLD_LOG_ID);
  const oc = old.findChild((n) => n.name === "Content") || old;
  for (const e of oc.children) {
    if (e.type !== "INSTANCE" || !findInst(e, "Версия")) continue;
    content.appendChild(e.clone());
    content.children[content.children.length - 1].layoutSizingHorizontal = sizing;
    version = readVersion(e); carried++;
  }
}
let entry = null;
if (!version) entry = { type: "Новая версия", text: "", v: { major: 1, minor: 0, patch: 0 } };
else if (CHANGE && CHANGE.text) {
  const t = CHANGE.type;
  const v = t === "Новая версия" ? { major: version.major + 1, minor: 0, patch: 0 }
    : t === "Исправление бага" ? { major: version.major, minor: version.minor, patch: version.patch + 1 }
    : { major: version.major, minor: version.minor + 1, patch: 0 };
  entry = { type: t, text: CHANGE.text, v };
}
if (entry) {
  content.appendChild(variantOf(logSet, { "Релизовано": "False" }).createInstance());
  const log = content.children[content.children.length - 1];
  log.layoutSizingHorizontal = sizing;
  const own = { "Описание": Boolean(entry.text), "Дата изменения": true };
  if (entry.text) own["Описание "] = nb(entry.text);
  setProps(log, own); // first: the «Дата» layer exists only while the date is shown
  setProps(findInst(log, "Версия"), { major: String(entry.v.major), minor: String(entry.v.minor), patch: String(entry.v.patch) });
  setProps(findInst(log, "Дата"), { Day: DATE.d, Month: DATE.m, Year: DATE.y });
  setProps(findInst(log, "Тип изменения"), { "Тип изменения": entry.type });
}
for (const d of demo) d.remove();
const cur = entry ? entry.v : version;
return { pageId: frame.id, version: cur.major + "." + cur.minor + "." + cur.patch, carried, added: Boolean(entry) };

Step 3 — «Спецификация»: lead and «Структура»

LEAD — the component description verbatim; empty → the lead template from «Texts».

const section = await figma.getNodeByIdAsync("SECTION_ID");
const set = await figma.getNodeByIdAsync("SET_ID");
const LEAD = "LEAD_TEXT";
const pageSpec = await atom("pageSpec"), para = await atom("para");
await loadFonts([pageSpec, para.defaultVariant]);
const { frame, content } = addPage(section, pageSpec);
const groups = content.children.filter((c) => c.name === "Group");
const lead = variantOf(para, { Type: "H1" }).createInstance();
content.insertChild(0, lead);
lead.layoutSizingHorizontal = "FILL";
setProps(lead, { "Show Title": false, Description: nb(LEAD) });
const anatomy = findInst(groups[0], "ds-mcp-component");
const slot = findSlot(anatomy, "Slot Container");
while (slot.children.length) slot.children[0].remove();
slot.appendChild((set.type === "COMPONENT_SET" ? set.defaultVariant : set).createInstance());
setProps(anatomy, { "Show Title": false, "Show Desciption": false });
// annotations on the example, category «Спецификация» (fallback Development)
const example = findSlot(anatomy, "Slot Container").children[0];
const cats = await figma.annotations.getAnnotationCategoriesAsync();
const cat = cats.find((c) => c.label === "Спецификация") || cats.find((c) => c.label === "Development");
const nested = (n) => { let p = n.parent; while (p && p.id !== example.id) { if (p.type === "INSTANCE") return true; p = p.parent; } return false; };
const want = (n) => {
  if (n.type === "TEXT") return [typeof n.textStyleId === "string" && n.textStyleId ? "textStyleId" : "fontSize", "fills"];
  if (n.type === "INSTANCE" && n.id !== example.id) return ["mainComponent", "width", "height"];
  const p = n.layoutMode && n.layoutMode !== "NONE" ? ["padding", "itemSpacing"] : ["width", "height"];
  if (typeof n.cornerRadius === "number" && n.cornerRadius > 0) p.push("cornerRadius");
  if (Array.isArray(n.fills) && n.fills.length) p.push("fills");
  return p;
};
let annotated = 0;
for (const n of [example].concat(example.findAll((x) => x.visible && ["FRAME", "TEXT", "INSTANCE"].includes(x.type) && !nested(x)))) {
  const a = { labelMarkdown: "**" + n.name + "**" };
  if (cat) a.categoryId = cat.id;
  try { n.annotations = [Object.assign({ properties: want(n).map((t) => ({ type: t })) }, a)]; annotated++; }
  catch (e) { try { n.annotations = [Object.assign({ properties: [{ type: "width" }, { type: "height" }] }, a)]; annotated++; } catch (_e) {} }
}
groups[1].name = "__proto";
return { pageId: frame.id, contentId: content.id, protoId: groups[1].id, annotated };

Step 4 — «Спецификация»: property blocks

One block per VARIANT property and per own BOOLEAN property, in declaration order. TEXT, INSTANCE_SWAP and SLOT properties get no block — list them in the report. Pass all property names at once; for a set over 150 variants pass two or three per call. LAST = true in the final call (removes the prototype group).

const set = await figma.getNodeByIdAsync("SET_ID");
const content = await figma.getNodeByIdAsync("CONTENT_ID");
const proto = await figma.getNodeByIdAsync("PROTO_ID");
const PROPS = ["PROPERTY_NAME"];
const LAST = true;
const para = await atom("para"), mc = await atom("mc"), st = await atom("state");
await loadFonts([para.defaultVariant, mc.defaultVariant, st.defaultVariant]);
const GLOSSARY = { configuration: "Конфигурация", size: "Размеры", state: "Состояния", style: "Стили", skeleton: "Скелетон" };
const defs = set.componentPropertyDefinitions;
const base = set.type === "COMPONENT_SET" ? set.defaultVariant : set;
const sparse = [], written = [];
for (const name of PROPS) {
  const key = Object.keys(defs).find((k) => k.split("#")[0] === name);
  const d = defs[key];
  let title = name, desc = "", rows = [];
  if (d.type === "VARIANT") {
    const def = base.variantProperties, others = Object.keys(def).filter((k) => k !== key);
    for (const value of d.variantOptions) {
      let best = null, score = -1;
      for (const v of set.children) {
        if (v.variantProperties[key] !== value) continue;
        const sc = others.filter((k) => v.variantProperties[k] === def[k]).length;
        if (sc > score) { best = v; score = sc; }
      }
      if (!best) { sparse.push(name + " = " + value + ": варианта нет"); continue; }
      if (score !== others.length) sparse.push(name + " = " + value + ": взят «" + best.name + "»");
      rows.push({ label: value, variant: best, bool: null });
    }
    title = GLOSSARY[name.toLowerCase()] || name;
    const n = d.variantOptions.length;
    desc = title === "Состояния" ? "У компонента есть следующие состояния" : "Свойство " + name + " — " + n + " " + plural(n, ["значение", "значения", "значений"]) + ".";
  } else if (d.type === "BOOLEAN") {
    const layer = base.findOne((x) => x.componentPropertyReferences && x.componentPropertyReferences.visible === key);
    desc = layer ? "Свойство " + name + " показывает и прячет слой " + layer.name + "." : "";
    rows = [{ label: "Включено", variant: base, bool: true }, { label: "Выключено", variant: base, bool: false }];
  } else continue;
  const group = proto.clone(); group.name = "Group";
  content.insertChild(content.children.findIndex((c) => c.id === proto.id), group);
  for (const c of group.children.slice()) c.remove();
  const h = variantOf(para, { Type: "H1" }).createInstance(); group.appendChild(h); h.layoutSizingHorizontal = "FILL";
  const host = variantOf(mc, { View: "State" }).createInstance(); group.appendChild(host); host.layoutSizingHorizontal = "FILL";
  const slot = findSlot(host, "Slot Container State");
  clearSlotWithMarker(slot); // the demo rows are instances of the same atom
  for (const r of rows) {
    slot.appendChild(variantOf(st, { Position: r.variant.width < 320 ? "Horizontal" : "Vertical" }).createInstance());
    const row = slot.children[slot.children.length - 1];
    row.layoutSizingHorizontal = "FILL";
    const inner = findSlot(row, "Slot Container");
    while (inner.children.length) inner.children[0].remove();
    const ex = r.variant.createInstance();
    if (r.bool !== null) setProps(ex, { [name]: r.bool });
    inner.appendChild(ex);
  }
  const made = slot.children.slice(); // properties after slot surgery, leaves to root
  for (let i = 0; i < made.length; i++) setProps(made[i], { Type: nb(rows[i].label), "Show Description": false });
  const hp = { Title: nb(title), "Show Title": true, "Show Description": Boolean(desc) };
  if (desc) hp.Description = nb(desc);
  setProps(h, hp);
  setProps(host, { "Show Title": false, "Show Desciption": false });
  written.push(title + (desc ? ": " + desc : ""));
}
if (LAST) proto.remove();
return { written, sparse };

Step 5 — «❖ Компонент»

VERSION — from Step 2.

const section = await figma.getNodeByIdAsync("SECTION_ID");
const set = await figma.getNodeByIdAsync("SET_ID");
const VERSION = "1.0.0";
const pageComp = await atom("pageComp"), lab = await atom("label");
await loadFonts([pageComp, lab.defaultVariant]);
const { frame } = addPage(section, pageComp);
const grid = frame.findOne((n) => n.type === "FRAME" && n.name === "Components" && n.layoutMode === "GRID");
const container = grid.children.find((c) => c.name === "Components").findChild((c) => c.name === "Container");
const hp = grid.children.find((c) => c.name === "Horizontal Props"), vp = grid.children.find((c) => c.name === "Vertical Props");
let from = set.parent; while (from.type !== "SECTION" && from.type !== "PAGE") from = from.parent;
const movedFrom = from.name;
for (const c of container.children.slice()) c.remove();
container.appendChild(set);
setProps(findInst(frame, "ds-format-component-head-title"), { "Название компонента": set.name });
const ver = VERSION.split(".");
for (const i of findInst(frame, "ds-format-component-footer").findAll((n) => n.type === "INSTANCE")) {
  const m = await i.getMainComponentAsync();
  if (m && m.name === ".ds-log-changelog-version") setProps(i, { major: ver[0], minor: ver[1], patch: ver[2] });
}
const demoInner = vp.children[0] && vp.children[0].children ? vp.children[0].children[0] : null;
const colPad = demoInner && typeof demoInner.paddingLeft === "number" ? demoInner.paddingLeft : 0;
for (const c of hp.children.slice()) c.remove();
for (const c of vp.children.slice()) c.remove();
if (set.type !== "COMPONENT_SET") return { pageId: frame.id, movedFrom, axes: [] };
// axes from the set's own layout: constant inside every column → column axis, inside every row → row axis
const items = set.children.map((v) => ({ x: v.x, y: v.y, w: v.width, h: v.height, p: v.variantProperties }));
const groupsBy = (pos, size) => {
  const out = [];
  for (const it of items.slice().sort((a, b) => a[pos] - b[pos])) {
    const last = out[out.length - 1];
    if (last && it[pos] < last.end) { last.items.push(it); last.end = Math.max(last.end, it[pos] + it[size]); }
    else out.push({ start: it[pos], end: it[pos] + it[size], items: [it] });
  }
  return out;
};
const runs = (gs, a) => { const out = []; for (const g of gs) { const v = g.items[0].p[a], l = out[out.length - 1]; if (l && l.value === v) l.end = g.end; else out.push({ value: v, start: g.start, end: g.end }); } return out; };
const constant = (gs, a) => gs.every((g) => g.items.every((it) => it.p[a] === g.items[0].p[a]));
const cols = groupsBy("x", "w"), rowsG = groupsBy("y", "h");
const H = [], Vx = [], unplaced = [];
for (const a of Object.keys(set.componentPropertyDefinitions).filter((k) => set.componentPropertyDefinitions[k].type === "VARIANT")) {
  if (cols.length > 1 && constant(cols, a) && runs(cols, a).length > 1) H.push(runs(cols, a));
  else if (rowsG.length > 1 && constant(rowsG, a) && runs(rowsG, a).length > 1) Vx.push(runs(rowsG, a));
  else unplaced.push(a);
}
H.sort((a, b) => a.length - b.length); Vx.sort((a, b) => a.length - b.length);
const ax = (n) => n.absoluteTransform[0][2], ay = (n) => n.absoluteTransform[1][2];
const level = (parent) => { const f = figma.createFrame(); f.name = "Variable"; f.fills = []; f.clipsContent = false; parent.appendChild(f); return f; };
const label = (text, large, vertical) => { const i = variantOf(lab, { Large: large ? "True" : "False", Vertical: vertical ? "True" : "False" }).createInstance(); setProps(i, { Label: nb(text) }); return i; };
H.forEach((lvl, idx) => {
  const row = level(hp);
  const made = lvl.map((r) => { const l = label(r.value, idx === 0, false); row.appendChild(l); return [l, r]; });
  row.resize(Math.max(1, ax(set) - ax(row) + set.width), made.reduce((m, p) => Math.max(m, p[0].height), 1));
  for (const [l, r] of made) { l.resize(Math.max(1, r.end - r.start), l.height); l.x = ax(set) - ax(row) + r.start; l.y = 0; }
});
Vx.forEach((lvl, idx) => {
  const col = level(vp);
  const made = lvl.map((r) => { const l = label(r.value, idx === 0, true); col.appendChild(l); l.primaryAxisSizingMode = "AUTO"; return [l, r]; });
  const w = made.reduce((m, p) => Math.max(m, p[0].width), 1) + colPad;
  col.resize(w, Math.max(1, ay(set) - ay(col) + set.height));
  for (const [l, r] of made) { l.resize(l.width, Math.max(1, r.end - r.start)); l.primaryAxisSizingMode = "AUTO"; l.x = w - l.width; l.y = ay(set) - ay(col) + r.start; }
});
return { pageId: frame.id, movedFrom, columns: H.length, rows: Vx.length, unplaced };

Step 6 — fit the section

const section = await figma.getNodeByIdAsync("SECTION_ID");
let r = 0, b = 0;
for (const c of section.children) { r = Math.max(r, c.x + c.width); b = Math.max(b, c.y + c.height); }
section.resizeWithoutConstraints(Math.ceil(r + 100), Math.ceil(b + 100));
figma.currentPage.selection = [section];
figma.viewport.scrollAndZoomIntoView([section]);
return { sectionId: section.id, w: section.width, h: section.height };

Take a screenshot of the whole section and check it.

Texts

All in Russian, verbatim:

lead, empty description, set«<Имя> — <N> вариантов по свойствам <A>, <B> и <C>.» (вариант / варианта / вариантов by number; one property → «по свойству <A>»)
lead, empty description, single component«<Имя> — компонент без вариантов.»
block titleConfiguration → Конфигурация, Size → Размеры, State → Состояния, Style → Стили, Skeleton → Скелетон; any other property — its name verbatim
block descriptionbuilt inside Step 4 — do not invent your own
first changelog entrytype «Новая версия», version 1.0.0, no description, today's date

Report (in Russian)

First line: what was built and the version. Then, only non-empty items:

  • откуда переехал набор (movedFrom);
  • что написал скилл сам: лид, если описание компонента пустое, заголовки и описания блоков (written);
  • свойства без блока (TEXT, INSTANCE_SWAP, SLOT);
  • оси без подписи (unplaced), строки не по варианту по умолчанию (sparse);
  • при пересборке: старая секция не тронута, её страница с компонентом теперь пуста — предложить удалить;
  • «Рекомендации» и «Редактура» добавить вручную из шаблона ds-component.