/* data.jsx — sample data + resolver for the SiCARRIER UI design-system console */

/* ---------------------------------------------------------------- *
 *  Brand sub-themes (one per product line). Each carries an accent
 *  scale, so switching theme re-resolves accent-derived tokens.
 * ---------------------------------------------------------------- */
/* `overrides` is a SPARSE per-theme pin map: { tokenId: source } holding ONLY the tokens
   a brand pins. Empty for every theme today (no-op). A pinned source is a normal source
   (value | ref | modeRef | themeAccent) plus a non-interfering `_basis` field carrying a
   cheap hash of the base token's source at pin time, for stale-pin detection. */
const THEMES = [
  { id: 'atlas',    name: 'Atlas Analytics', line: 'Data dashboards',      accent: { 400:'#4E84FB', 500:'#0A59F7', 600:'#0A47C9' }, overrides: {} },
  { id: 'forge',    name: 'Forge HMI',       line: 'Semiconductor HMI',    accent: { 400:'#E08A3C', 500:'#C8691E', 600:'#A4530F' }, overrides: {} },
  { id: 'cortex',   name: 'Cortex AI',       line: 'AI web apps',          accent: { 400:'#7C7CE0', 500:'#5B5BD6', 600:'#4646B8' }, overrides: {} },
  { id: 'sentinel', name: 'Sentinel',        line: 'Monitoring & alerts',  accent: { 400:'#2BB39E', 500:'#0E8A7A', 600:'#0A6B5E' }, overrides: {} },
  { id: 'site',     name: 'SiCARRIER Web',   line: 'Public site (red)',    accent: { 400:'#E2434C', 500:'#C7000B', 600:'#9E0009' }, overrides: {} },
];

/* layers & categories for the tree */
const LAYERS = [
  { id: 'primitive', name: 'Primitive', hint: 'Raw values' },
  { id: 'semantic',  name: 'Semantic',  hint: 'Purpose-based' },
  { id: 'component', name: 'Component', hint: 'Part-specific' },
];
const CATEGORIES = ['Color', 'Spacing', 'Typography', 'Radius', 'Shadow'];

/* ---------------------------------------------------------------- *
 *  Tokens. source.type ∈ value | themeAccent | ref | modeRef
 * ---------------------------------------------------------------- */
const TOKENS = [
  /* ===== PRIMITIVE · Color ===== */
  t('color.base.white','primitive','Color','color',{type:'value',value:'#FFFFFF'},'Pure white base.'),
  t('color.gray.50', 'primitive','Color','color',{type:'value',value:'#F5F5F7'}),
  t('color.gray.100','primitive','Color','color',{type:'value',value:'#E8E8ED'}),
  t('color.gray.200','primitive','Color','color',{type:'value',value:'#D2D2D7'}),
  t('color.gray.400','primitive','Color','color',{type:'value',value:'#8E8E93'}),
  t('color.gray.500','primitive','Color','color',{type:'value',value:'#6E6E73'}),
  t('color.gray.700','primitive','Color','color',{type:'value',value:'#3A3A3C'}),
  t('color.gray.900','primitive','Color','color',{type:'value',value:'#1C1C1E'}),
  t('color.gray.950','primitive','Color','color',{type:'value',value:'#0C0C0D'}),
  t('color.red.500',  'primitive','Color','color',{type:'value',value:'#D63B3B'},'Critical / danger base.'),
  t('color.green.500','primitive','Color','color',{type:'value',value:'#1E874B'},'Nominal / success base.'),
  t('color.amber.500','primitive','Color','color',{type:'value',value:'#C8691E'},'Caution base.'),
  t('brand.accent.400','primitive','Color','color',{type:'themeAccent',shade:400},'Theme accent — light.'),
  t('brand.accent.500','primitive','Color','color',{type:'themeAccent',shade:500},'Theme accent — base. Resolves per active brand theme.'),
  t('brand.accent.600','primitive','Color','color',{type:'themeAccent',shade:600},'Theme accent — pressed.'),

  /* ===== PRIMITIVE · Spacing ===== */
  t('space.100','primitive','Spacing','spacing',{type:'value',value:4}),
  t('space.200','primitive','Spacing','spacing',{type:'value',value:8}),
  t('space.300','primitive','Spacing','spacing',{type:'value',value:12}),
  t('space.400','primitive','Spacing','spacing',{type:'value',value:16}),
  t('space.600','primitive','Spacing','spacing',{type:'value',value:24}),
  t('space.800','primitive','Spacing','spacing',{type:'value',value:32}),

  /* ===== PRIMITIVE · Radius ===== */
  t('radius.sm','primitive','Radius','radius',{type:'value',value:4}),
  t('radius.md','primitive','Radius','radius',{type:'value',value:8}),
  t('radius.lg','primitive','Radius','radius',{type:'value',value:12}),

  /* ===== PRIMITIVE · Typography ===== */
  t('font.size.100','primitive','Typography','type',{type:'value',value:12,lh:16}),
  t('font.size.200','primitive','Typography','type',{type:'value',value:13,lh:18}),
  t('font.size.300','primitive','Typography','type',{type:'value',value:14,lh:20}),
  t('font.size.400','primitive','Typography','type',{type:'value',value:16,lh:24}),
  t('font.size.600','primitive','Typography','type',{type:'value',value:20,lh:28}),
  t('font.size.800','primitive','Typography','type',{type:'value',value:28,lh:34}),

  /* ===== PRIMITIVE · Shadow ===== */
  t('shadow.sm','primitive','Shadow','shadow',{type:'value',value:'0 1px 2px rgba(0,0,0,0.06)'}),
  t('shadow.md','primitive','Shadow','shadow',{type:'value',value:'0 4px 14px rgba(0,0,0,0.10)'}),

  /* ===== SEMANTIC · Color (mode-aware) ===== */
  t('color.bg.canvas',     'semantic','Color','color',{type:'modeRef',light:'color.gray.50', dark:'color.gray.950'},'App canvas background.'),
  t('color.bg.surface',    'semantic','Color','color',{type:'modeRef',light:'color.base.white',dark:'color.gray.900'},'Raised surface / card.'),
  t('color.bg.sunken',     'semantic','Color','color',{type:'modeRef',light:'color.gray.100',dark:'color.gray.950'},'Inset wells, tracks.'),
  t('color.border.default','semantic','Color','color',{type:'modeRef',light:'color.gray.200',dark:'color.gray.700'},'Hairline divider.'),
  t('color.text.primary',  'semantic','Color','color',{type:'modeRef',light:'color.gray.900',dark:'color.gray.50'},'Primary text.'),
  t('color.text.secondary','semantic','Color','color',{type:'modeRef',light:'color.gray.500',dark:'color.gray.400'},'Secondary text.', { status:'renamed', from:'color.text.muted' }),
  t('color.text.onAccent', 'semantic','Color','color',{type:'ref',ref:'color.base.white'},'Text on accent fills.'),
  t('color.accent.default','semantic','Color','color',{type:'ref',ref:'brand.accent.500'},'Primary brand accent.'),
  t('color.accent.hover',  'semantic','Color','color',{type:'ref',ref:'brand.accent.600'},'Accent pressed/hover.', { status:'modified', prevRef:'brand.accent.500' }),
  t('color.status.danger', 'semantic','Color','color',{type:'ref',ref:'color.red.500'},'Critical state.'),
  t('color.status.success','semantic','Color','color',{type:'ref',ref:'color.green.500'},'Nominal state.'),
  t('color.status.warning','semantic','Color','color',{type:'ref',ref:'color.amber.500'},'Caution state.'),

  /* ===== SEMANTIC · Spacing / Radius / Type / Shadow ===== */
  t('spacing.inset.sm','semantic','Spacing','spacing',{type:'ref',ref:'space.200'},'Compact inset.'),
  t('spacing.inset.md','semantic','Spacing','spacing',{type:'ref',ref:'space.400'},'Default inset.'),
  t('spacing.inset.lg','semantic','Spacing','spacing',{type:'ref',ref:'space.600'},'Roomy inset.', { status:'modified', prevRef:'space.500', prevVal:20 }),
  t('radius.chip',   'semantic','Radius','radius',{type:'ref',ref:'radius.sm'},'Tags, chips, small pills.', { status:'new' }),
  t('radius.control','semantic','Radius','radius',{type:'ref',ref:'radius.md'},'Buttons, inputs.'),
  t('radius.surface','semantic','Radius','radius',{type:'ref',ref:'radius.lg'},'Cards, panels.'),
  t('text.body',   'semantic','Typography','type',{type:'ref',ref:'font.size.300'},'Body copy.'),
  t('text.caption','semantic','Typography','type',{type:'ref',ref:'font.size.100'},'Meta / captions.'),
  t('text.heading','semantic','Typography','type',{type:'ref',ref:'font.size.600'},'Section headings.'),
  t('shadow.overlay','semantic','Shadow','shadow',{type:'ref',ref:'shadow.md'},'Drawers, popovers.'),

  /* ===== COMPONENT — ONLY where a component makes its own decision.
     Structural needs (radius / border / generic surfaces) reference semantic
     tokens directly; no redundant 1:1 component aliases. ===== */
  t('button.bg',         'component','Color','color',{type:'ref',ref:'color.accent.default'},'Primary button fill — a brand-overridable CTA hook.', { status:'modified', prevRef:'color.accent.default' }),
  t('button.text',       'component','Color','color',{type:'ref',ref:'color.text.onAccent'},'Primary button label.'),
  t('link.text',         'component','Color','color',{type:'ref',ref:'color.accent.default'},'Link text color.'),
  t('switch.on.bg',      'component','Color','color',{type:'ref',ref:'color.accent.default'},'Switch — on track (component-specific state).'),
  t('switch.off.bg',     'component','Color','color',{type:'ref',ref:'color.border.default'},'Switch — off track (component-specific state).'),
  t('badge.bg',          'component','Color','color',{type:'ref',ref:'color.status.danger'},'Badge count fill — deliberately danger, not accent.'),
  t('badge.text',        'component','Color','color',{type:'ref',ref:'color.text.onAccent'},'Badge label.'),
  t('tabs.active.text',  'component','Color','color',{type:'ref',ref:'color.accent.default'},'Active tab label.'),
  t('tabs.active.border','component','Color','color',{type:'ref',ref:'color.accent.default'},'Active tab underline.'),
  t('progress.fill',     'component','Color','color',{type:'ref',ref:'color.accent.default'},'Progress fill.', { status:'new' }),
  t('modal.shadow',      'component','Shadow','shadow',{type:'ref',ref:'shadow.overlay'},'Modal elevation.'),
  t('drawer.shadow',     'component','Shadow','shadow',{type:'ref',ref:'shadow.overlay'},'Drawer elevation.'),
];

function t(id, layer, category, kind, source, description, extra) {
  const e = extra || {};
  return {
    id, layer, category, kind, source,
    description: description || '',
    tags: e.tags || defaultTags(id, category),
    status: e.status || 'published',     // published | new | modified | deprecated | renamed
    prevRef: e.prevRef, prevVal: e.prevVal, from: e.from,
  };
}
function defaultTags(id, category) {
  const head = id.split('.')[0];
  return Array.from(new Set([head, category.toLowerCase()]));
}

/* ---------------------------------------------------------------- *
 *  Resolver — walks reference chains, mode + theme aware, cycle-safe
 * ---------------------------------------------------------------- */
const TOKEN_MAP = Object.fromEntries(TOKENS.map(tk => [tk.id, tk]));

function nextRef(source, mode) {
  if (!source) return null;
  if (source.type === 'ref') return source.ref;
  if (source.type === 'modeRef') return source[mode] || source.light;
  return null;
}

/* cheap, order-stable hash of a token source — used as the `_basis` snapshot stored on a pin
   and to detect when the base token's source has drifted since the pin was made (stale pin). */
function sourceHash(source) {
  if (!source) return '0';
  const s = source;
  if (s.type === 'value') return 'v:' + s.value + (s.lh != null ? '/' + s.lh : '');
  if (s.type === 'themeAccent') return 'a:' + s.shade;
  if (s.type === 'ref') return 'r:' + s.ref;
  if (s.type === 'modeRef') return 'm:' + s.light + '|' + s.dark;
  return 'x:' + JSON.stringify(s);
}

/* the effective source for a token under a theme: the theme's pin if present, else the base.
   Empty override maps ⇒ always returns the base source (perfect no-op). */
function applyOverride(tokenId, ctx) {
  const tk = TOKEN_MAP[tokenId];
  if (!tk) return null;
  const ov = ctx && ctx.theme && ctx.theme.overrides && ctx.theme.overrides[tokenId];
  return ov || tk.source;
}

/* returns { value, kind, chain:[{id,label}], cycle:bool, pinned:bool } */
function resolve(tokenId, ctx, _seen) {
  const ctxr = ctx || { theme: THEMES[0], mode: 'light' };
  const seen = _seen || [];
  const tk = TOKEN_MAP[tokenId];
  if (!tk) return { value: null, kind: null, chain: [], cycle: false, missing: true };
  if (seen.includes(tokenId)) return { value: null, kind: tk.kind, chain: seen.concat(tokenId).map(id => ({ id })), cycle: true };
  const nseen = seen.concat(tokenId);
  /* OVERRIDE HOP: resolve using the theme's pinned source if one exists, else the base source.
     Identical downstream logic runs either way, so an empty override map is a byte-identical no-op. */
  const ov = ctxr.theme && ctxr.theme.overrides && ctxr.theme.overrides[tokenId];
  const s = ov || tk.source;
  if (s.type === 'value') return { value: s.value, lh: s.lh, kind: tk.kind, chain: nseen.map(id => ({ id })), cycle: false, pinned: !!ov };
  if (s.type === 'themeAccent') return { value: ctxr.theme.accent[s.shade], kind: tk.kind, chain: nseen.map(id => ({ id })), cycle: false, pinned: !!ov };
  const target = nextRef(s, ctxr.mode);
  const down = resolve(target, ctxr, nseen);
  return { value: down.value, lh: down.lh, kind: tk.kind, chain: down.chain, cycle: down.cycle, missing: down.missing, pinned: !!ov || down.pinned };
}

/* Would pointing `fromId` at `targetId` create a cycle? */
function wouldCycle(fromId, targetId, mode) {
  if (fromId === targetId) return true;
  let cur = targetId, hops = 0;
  const visited = new Set();
  while (cur && hops < 64) {
    if (cur === fromId) return true;
    if (visited.has(cur)) return false;
    visited.add(cur);
    const tk = TOKEN_MAP[cur];
    if (!tk) return false;
    cur = nextRef(tk.source, mode || 'light');
    hops++;
  }
  return false;
}

/* reverse dependencies: which tokens reference `targetId` (any mode) */
function dependentsOf(targetId) {
  return TOKENS.filter(tk => {
    const s = tk.source;
    if (s.type === 'ref') return s.ref === targetId;
    if (s.type === 'modeRef') return s.light === targetId || s.dark === targetId;
    return false;
  }).map(tk => tk.id);
}

/* ---------------------------------------------------------------- *
 *  Assets — icons (inline line SVG) + image placeholders
 * ---------------------------------------------------------------- */
const ASSETS = [
  a('icon.status.alert','icon','Status',['alert','critical','warning'],'warningTri','SVG','24×24',1284,9,'modified',{ desc:'Critical alerts and threshold-breach banners across HMI dashboards.', usedBy:['Alert banner','Sensor tile','Dashboard header'] }),
  a('icon.status.online','icon','Status',['online','nominal','health'],'activity','SVG','24×24',1106,7,'published',{ desc:'Online / healthy status on equipment and connection cards.', usedBy:['Sensor tile','Device list','Status badge'] }),
  a('icon.data.gauge','icon','Data viz',['gauge','meter','dial'],'gauge','SVG','24×24',968,14,'published',{ desc:'Radial metric readouts — pressure, utilization, temperature.', usedBy:['Gauge','Metric tile'] }),
  a('icon.data.chart-line','icon','Data viz',['trend','line','series'],'chartLine','SVG','24×24',742,21,'published',{ desc:'Time-series trend charts on analytics dashboards.', usedBy:['Chart','Trend panel'] }),
  a('icon.data.chart-bar','icon','Data viz',['bar','histogram','distribution'],'chartBar','SVG','24×24',688,12,'published',{ desc:'Categorical distribution charts and reports.', usedBy:['Chart','Report builder'] }),
  a('icon.sensor.temperature','icon','Sensor',['temp','thermal','probe'],'thermometer','SVG','24×24',1042,6,'published',{ desc:'Temperature sensor readouts on equipment detail.', usedBy:['Sensor tile','Equipment detail'] }),
  a('icon.sensor.vibration','icon','Sensor',['vibration','waveform','signal'],'vibration','SVG','24×24',624,0,'new',{ desc:'Vibration / waveform sensor readouts.', usedBy:['Sensor tile'] }),
  a('icon.equipment.chip','icon','Equipment',['cpu','die','wafer','silicon'],'chip','SVG','24×24',1320,8,'published',{ desc:'Semiconductor / wafer equipment nodes in topology views.', usedBy:['Tree','Equipment map'] }),
  a('icon.equipment.database','icon','Equipment',['db','store','records'],'database','SVG','24×24',1188,5,'published',{ desc:'Data-store / records nodes in pipeline views.', usedBy:['Tree','Pipeline view'] }),
  a('icon.alert.bell','icon','Status',['notify','bell','subscribe'],'bell','SVG','24×24',902,11,'published',{ desc:'Notification subscriptions and alert toggles.', usedBy:['Nav rail','Notification center'] }),
  a('icon.data.signal','icon','Data viz',['signal','strength','bars'],'signal','SVG','24×24',556,4,'published',{ desc:'Signal-strength / connectivity indicators.', usedBy:['Device list','Status bar'] }),
  a('icon.action.export','icon','Action',['export','download','save'],'download','SVG','24×24',604,17,'published',{ desc:'Export / download data and reports.', usedBy:['Toolbar','Report builder'] }),
  a('icon.action.export-legacy','icon','Action',['export','old'],'external','SVG','24×24',588,1,'deprecated',{ desc:'Legacy export glyph — superseded by icon.action.export.', usedBy:[] }),

  a('image.illustration.empty-dashboard','image','Illustration',['empty','dashboard','zero-state'],null,'PNG','640×400',38400,9,'modified',{ desc:'Zero-state illustration for empty dashboards.', usedBy:['Dashboard','Empty state'] }),
  a('image.illustration.error-500','image','Illustration',['error','fault','5xx'],null,'PNG','640×400',41200,4,'published',{ desc:'5xx / fault error-page illustration.', usedBy:['Error page'] }),
  a('image.banner.maintenance','image','Banner',['maintenance','downtime','notice'],null,'PNG','1280×320',72100,2,'published',{ desc:'Scheduled-downtime / maintenance banner.', usedBy:['System banner'] }),
  a('image.avatar.default','image','Avatar',['avatar','placeholder','user'],null,'PNG','128×128',9200,33,'published',{ desc:'Default user avatar placeholder.', usedBy:['Account menu','User list'] }),
];
function a(id, type, category, keywords, icon, format, dims, bytes, used, status, extra) {
  const e = extra || {};
  return { id, type, category, keywords, icon, format, dims, bytes, used, status,
    desc: e.desc || '', usedBy: e.usedBy || [],
    tags: [id.split('.')[1], category.toLowerCase()] };
}
/* NOTE: an icon's standardized text label now lives in the Content module (content-data.jsx,
   surface 'icon-label', linked by iconId) — the single source of truth. Read it via copyForIcon(id). */

/* ---------------------------------------------------------------- *
 *  Initial change set (pending, unpublished)
 * ---------------------------------------------------------------- */
const CHANGES = [
  { id:'c1', kind:'token', action:'modified',  target:'color.accent.hover', impact:'patch',
    diffType:'colorRef', from:'brand.accent.500', to:'brand.accent.600', fromVal:'#0B6BCB', toVal:'#0857A6' },
  { id:'c2', kind:'token', action:'modified',  target:'spacing.inset.lg', impact:'minor',
    diffType:'num', from:'20px', to:'24px' },
  { id:'c3', kind:'token', action:'added',     target:'progress.fill', impact:'minor',
    diffType:'new', to:'{color.accent.default}' },
  { id:'c4', kind:'token', action:'renamed',   target:'color.text.secondary', impact:'minor',
    diffType:'rename', from:'color.text.muted', to:'color.text.secondary' },
  { id:'c5', kind:'token', action:'modified',  target:'button.bg', impact:'patch',
    diffType:'refChange', from:'changed', to:'{color.accent.default}' },
  { id:'c6', kind:'asset', action:'added',     target:'icon.sensor.vibration', impact:'minor',
    diffType:'newIcon', icon:'vibration' },
  { id:'c7', kind:'asset', action:'modified',  target:'image.illustration.empty-dashboard', impact:'patch',
    diffType:'imageSwap' },
  { id:'c8', kind:'asset', action:'deprecated',target:'icon.action.export-legacy', impact:'minor',
    diffType:'deprecateIcon', icon:'external' },
  { id:'c9',  kind:'component', action:'modified', target:'Button', impact:'minor',
    diffType:'compDocs',   summary:'Reworked overview; added “danger” usage guidance.' },
  { id:'c10', kind:'component', action:'modified', target:'Modal',  impact:'minor',
    diffType:'compCaution', on:true, note:'High blast radius — used by dialogs across all four products.' },
  { id:'c11', kind:'component', action:'modified', target:'Tabs',   impact:'patch',
    diffType:'compDocs',   summary:'Documented overflow scrolling + keyboard navigation.' },
  { id:'c12', kind:'theme', action:'modified', target:'Cortex AI', impact:'minor',
    diffType:'themeEdit', swatch:'#5B5BD6' },
];

/* session / branch meta */
const SESSION = {
  product: 'SiCARRIER UI',
  branch: 'ds/2026.06-token-refresh',
  user: { name: 'Lin Zhang', handle: 'lin.zhang', role: 'Design Systems' },
  reviewers: ['m.okafor', 'p.narang', 'j.reyes'],
  cycle: 'June 2026 release',
};

/* team & access roles (manager > editor > viewer).
   `discipline` (product | dev | design) is a separate identity tag, NOT a permission. */
const MEMBERS = [
  { handle:'lin.zhang', name:'Lin Zhang', role:'manager', team:'Design Systems', discipline:'design' },
  { handle:'m.okafor',  name:'Mara Okafor', role:'editor', team:'UI Platform', discipline:'dev' },
  { handle:'p.narang',  name:'Priya Narang', role:'editor', team:'Forge HMI', discipline:'design' },
  { handle:'j.reyes',   name:'Jay Reyes', role:'editor', team:'Atlas Analytics', discipline:'design' },
  { handle:'s.lin',     name:'Sam Lin', role:'viewer', team:'Product', discipline:'product' },
  { handle:'a.koch',    name:'Ana Koch', role:'viewer', team:'Quality', discipline:'dev' },
];

/* product surfaces that consume the design system (targets for feedback) */
const PRODUCTS = [
  { id:'atlas',    name:'Atlas Analytics', nameZh:'Atlas 数据分析' },
  { id:'forge',    name:'Forge HMI',       nameZh:'Forge 半导体 HMI' },
  { id:'cortex',   name:'Cortex AI',       nameZh:'Cortex AI 应用' },
  { id:'sentinel', name:'Sentinel',        nameZh:'Sentinel 监控告警' },
  { id:'system',   name:'Design System',   nameZh:'设计系统' },
];
const PRODUCT_MAP = Object.fromEntries(PRODUCTS.map(p => [p.id, p]));

/* requirements & feedback — social-feed style; anyone (incl. viewers) can post */
const REQUESTS = [
  { id:'rq1', product:'atlas', author:'s.lin', status:'open',
    text:'The data table needs a compact density variant for row-heavy dashboards — the current row height wastes vertical space.',
    textZh:'数据表格需要一个紧凑密度的变体——当前行高在高密度看板里太浪费纵向空间了。',
    image:{ label:'fab-overview.png', labelZh:'fab-overview.png' }, time:'2026-06-06T09:14',
    replies:[
      { author:'j.reyes', time:'2026-06-06T10:02', text:'Agreed — we hit the same thing on the fab overview board. +1', textZh:'同意——我们在 fab 总览看板上遇到了同样的问题。+1' },
    ] },
  { id:'rq2', product:'forge', author:'a.koch', status:'open',
    text:"The Switch off-state contrast is too low on the dark HMI theme — operators can't tell on from off at a glance.",
    textZh:'开关「关闭态」在深色 HMI 主题下对比度太低——操作员一眼分不清开和关。',
    image:{ label:'hmi-switch.png', labelZh:'hmi-switch.png' }, time:'2026-06-05T16:40',
    replies:[
      { author:'m.okafor', time:'2026-06-05T17:20', text:"Logged. We'll raise the off-state border weight in the next token pass.", textZh:'已记录。下个 token 迭代会加深关闭态的描边。' },
    ] },
  { id:'rq3', product:'system', author:'j.reyes', status:'resolved',
    text:"Please add a semantic token for 'caution' surfaces, distinct from 'warning'.",
    textZh:'希望新增一个表示「谨慎 / caution」的语义 token，与「warning」区分开。',
    image:null, time:'2026-05-28T11:05',
    replies:[
      { author:'lin.zhang', time:'2026-05-29T09:30', text:'Shipped in the 2026.05 release as color.feedback.caution.*', textZh:'已在 2026.05 版本发布，token 为 color.feedback.caution.*' },
      { author:'s.lin', time:'2026-05-29T10:00', text:'Thanks, works great!', textZh:'感谢，很好用！' },
    ] },
  { id:'rq4', product:'cortex', author:'s.lin', status:'open',
    text:'We need a streaming / typing indicator component for the AI chat surface — everyone is rebuilding it locally.',
    textZh:'AI 对话界面需要一个「流式 / 正在输入」的指示组件——现在大家都在各自本地重复造。',
    image:null, time:'2026-06-01T13:48', replies:[] },
  { id:'rq5', product:'sentinel', author:'a.koch', status:'resolved',
    text:'The alert badge count overflows past 99 and breaks the layout — it needs a 99+ treatment.',
    textZh:'告警徽标数字超过 99 会撑破布局——需要 99+ 的处理。',
    image:{ label:'badge-overflow.png', labelZh:'badge-overflow.png' }, time:'2026-05-12T15:22',
    replies:[
      { author:'p.narang', time:'2026-05-13T08:10', text:'Fixed in Badge v1.3 — added a maxCount prop (defaults to 99).', textZh:'已在 Badge v1.3 修复——新增 maxCount 属性（默认 99）。' },
    ] },
  { id:'rq6', product:'atlas', author:'j.reyes', status:'open',
    text:'Charts should expose the full categorical palette as tokens so teams stop hardcoding hex values.',
    textZh:'图表应把完整的分类色板作为 token 暴露出来，免得各团队继续硬编码 hex 值。',
    image:null, time:'2026-04-26T10:15', replies:[] },
];

/* shipped change-sets (review history) */
const CHANGE_HISTORY = [
  { id:'pr-481', number:481, title:'May release — Cortex accent refresh + caution token', titleZh:'5 月发版 —— Cortex 强调色刷新 + caution token', date:'2026-05-30', rollup:'minor', author:'Lin Zhang', status:'merged', items:[
    { id:'h481-1', kind:'token',     action:'modified',  target:'color.accent.hover',     impact:'patch', diffType:'colorRef', from:'brand.accent.500', to:'brand.accent.600', fromVal:'#0A59F7', toVal:'#0A47C9' },
    { id:'h481-2', kind:'token',     action:'added',     target:'color.feedback.caution', impact:'minor', diffType:'new', to:'{color.amber.500}' },
    { id:'h481-3', kind:'token',     action:'renamed',   target:'color.text.secondary',   impact:'minor', diffType:'rename', from:'color.text.muted', to:'color.text.secondary' },
    { id:'h481-4', kind:'token',     action:'modified',  target:'color.bg.surface',       impact:'patch', diffType:'color', from:'#FFFFFF', to:'#FAFAFA' },
    { id:'h481-5', kind:'theme',     action:'modified',  target:'Cortex AI',              impact:'minor', diffType:'themeEdit', swatch:'#5B5BD6' },
    { id:'h481-6', kind:'component', action:'modified',  target:'Button',                 impact:'minor', diffType:'compDocs', summary:'Reworked overview; added “danger” usage guidance.' },
    { id:'h481-7', kind:'component', action:'modified',  target:'Modal',                  impact:'minor', diffType:'compCaution', on:true, note:'High blast radius — used by dialogs across all four products.' },
  ] },
  { id:'pr-468', number:468, title:'April release — chart palette tokens, compact table density', titleZh:'4 月发版 —— 图表色板 token、紧凑表格密度', date:'2026-04-29', rollup:'minor', author:'Jay Reyes', status:'merged', items:[
    { id:'h468-1', kind:'token',     action:'added',     target:'color.chart.cat.1',      impact:'minor', diffType:'new', to:'#0A59F7' },
    { id:'h468-2', kind:'token',     action:'modified',  target:'spacing.row',            impact:'minor', diffType:'num', from:'44px', to:'36px' },
    { id:'h468-3', kind:'token',     action:'modified',  target:'color.chart.grid',       impact:'patch', diffType:'color', from:'#E8E8ED', to:'#D2D2D7' },
    { id:'h468-4', kind:'component', action:'modified',  target:'Table',                  impact:'minor', diffType:'compStatus', from:'beta', to:'stable', note:'Density variant shipped.' },
    { id:'h468-5', kind:'asset',     action:'added',     target:'icon.data.scatter',      impact:'minor', diffType:'newIcon', icon:'chartLine' },
  ] },
  { id:'pr-455', number:455, title:'March release — Forge HMI dark-mode contrast fixes', titleZh:'3 月发版 —— Forge HMI 深色模式对比度修复', date:'2026-03-31', rollup:'major', author:'Priya Narang', status:'merged', items:[
    { id:'h455-1', kind:'token',     action:'modified',   target:'switch.off.bg',         impact:'minor', diffType:'refChange', from:'changed', to:'{color.border.default}' },
    { id:'h455-2', kind:'token',     action:'deprecated', target:'color.border.subtle',   impact:'major', diffType:'deprecate', from:'{color.gray.200}' },
    { id:'h455-3', kind:'component', action:'deprecated', target:'Drawer (legacy)',       impact:'major', diffType:'compStatus', from:'beta', to:'deprecated', note:'Superseded by Sheet.' },
    { id:'h455-4', kind:'asset',     action:'deprecated', target:'icon.legacy.gauge',     impact:'minor', diffType:'deprecateIcon', icon:'gauge' },
    { id:'h455-5', kind:'theme',     action:'removed',    target:'Legacy HMI',            impact:'major', diffType:'themeRemove' },
  ] },
  { id:'pr-441', number:441, title:'February release — semantic spacing scale, radius tokens, Sentinel theme', titleZh:'2 月发版 —— 语义间距阶梯、圆角 token、Sentinel 主题', date:'2026-02-27', rollup:'minor', author:'Mara Okafor', status:'merged', items:[
    { id:'h441-1', kind:'token', action:'added',    target:'radius.control',  impact:'minor', diffType:'new', to:'{radius.md}' },
    { id:'h441-2', kind:'token', action:'modified', target:'spacing.inset.lg', impact:'minor', diffType:'num', from:'20px', to:'24px' },
    { id:'h441-3', kind:'theme', action:'added',    target:'Sentinel',        impact:'minor', diffType:'themeNew', swatch:'#0E8A7A' },
    { id:'h441-4', kind:'asset', action:'modified', target:'image.banner.maintenance', impact:'patch', diffType:'imageSwap' },
  ] },
];

Object.assign(window, {
  THEMES, LAYERS, CATEGORIES, TOKENS, TOKEN_MAP, ASSETS, CHANGES, SESSION, MEMBERS,
  PRODUCTS, PRODUCT_MAP, REQUESTS, CHANGE_HISTORY,
  resolve, wouldCycle, dependentsOf, nextRef, sourceHash, applyOverride,
});
