> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 04-2 rules：模組化規則檔，路徑範圍與分工

> `.claude/rules/` 把 CLAUDE.md 的常駐約束切成可依路徑範圍觸達的模組。本單元講 rules 與 CLAUDE.md 的分工邊界、path-scoped 觸達機制、命名與分層組織，並對照其他工具的同類機制。

export const ToolCompare = ({lang = "zh", tools = [], dimensions = [], defaultSelected, notes = []}) => {
  const UI = lang === "en" ? {
    allTools: "All",
    recommend: "Recommended",
    noSupport: "N/A",
    minOneToolW: "Select at least one tool",
    mobileLabel: "Tool",
    dimensionLbl: "Dimension",
    notes: "Notes"
  } : {
    allTools: "全選",
    recommend: "推薦",
    noSupport: "無對應",
    minOneToolW: "至少選一個工具",
    mobileLabel: "工具",
    dimensionLbl: "維度",
    notes: "注意事項"
  };
  const ACCENT_L = "#bf7551";
  const ACCENT_D = "#cf8a68";
  const REC_BDR_L = "rgba(191,117,81,0.45)";
  const REC_BG_L = "rgba(191,117,81,0.06)";
  const REC_BDR_D = "rgba(207,138,104,0.45)";
  const REC_BG_D = "rgba(207,138,104,0.08)";
  const looksLikePath = val => typeof val === "string" && (/[/\\.*:]/).test(val);
  const safeTools = Array.isArray(tools) ? tools : [];
  const safeDimensions = Array.isArray(dimensions) ? dimensions : [];
  if (safeTools.length === 0 || safeDimensions.length === 0) return null;
  const allIds = safeTools.map(t => t.id);
  const initSelected = Array.isArray(defaultSelected) && defaultSelected.length > 0 ? defaultSelected.filter(id => allIds.includes(id)) : allIds;
  const [selectedIds, setSelectedIds] = useState(initSelected.length > 0 ? initSelected : allIds);
  const [expandedCell, setExpandedCell] = useState(null);
  const [mobileTool, setMobileTool] = useState(initSelected[0] || allIds[0]);
  const [isDark, setIsDark] = useState(false);
  useEffect(() => {
    const detect = () => setIsDark(document.documentElement.classList.contains("dark"));
    detect();
    const obs = new MutationObserver(detect);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => obs.disconnect();
  }, []);
  const toggleTool = id => {
    setSelectedIds(prev => {
      const next = prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id];
      if (next.length === 0) return prev;
      if (expandedCell) {
        const [, cellToolId] = expandedCell.split(":");
        if (!next.includes(cellToolId)) setExpandedCell(null);
      }
      return next;
    });
  };
  const isAllSelected = selectedIds.length === allIds.length;
  const toggleAll = () => {
    if (isAllSelected) {
      setSelectedIds([allIds[0]]);
    } else {
      setSelectedIds(allIds);
    }
    setExpandedCell(null);
  };
  const visibleTools = safeTools.filter(t => selectedIds.includes(t.id));
  const cellKey = (dimId, toolId) => dimId + ":" + toolId;
  const toggleExpand = (dimId, toolId) => {
    const key = cellKey(dimId, toolId);
    setExpandedCell(prev => prev === key ? null : key);
  };
  const renderValue = cell => {
    if (cell.na) {
      return <span className="tc-na">{UI.noSupport}</span>;
    }
    const useMono = cell.mono === true || cell.mono !== false && looksLikePath(cell.value);
    return useMono ? <code className="tc-code">{cell.value}</code> : <span>{cell.value}</span>;
  };
  const css = `
  /* ── 根容器 ── */
  .tc-root {
    --tc-bg:           #FAF8F3;
    --tc-surface:      rgba(0,0,0,0.022);
    --tc-stripe:       rgba(0,0,0,0.028);
    --tc-border:       rgba(0,0,0,0.09);
    --tc-text:         #2b2722;
    --tc-dim:          #6f6a62;
    --tc-faint:        #8a8378;
    --tc-accent:       ${ACCENT_L};
    --tc-accent-bg:    ${REC_BG_L};
    --tc-accent-bdr:   ${REC_BDR_L};
    --tc-expand-bg:    rgba(0,0,0,0.016);
    --tc-code-bg:      rgba(0,0,0,0.055);
    --tc-na-color:     #9a9490;
    --tc-pill-on-bg:   rgba(191,117,81,0.10);
    --tc-pill-on-bdr:  rgba(191,117,81,0.35);
    --tc-pill-on-txt:  #a05c38;
    border: 1px solid var(--tc-border);
    border-radius: 14px;
    background: var(--tc-bg);
    color: var(--tc-text);
    overflow: hidden;
    font-size: 14px;
  }
  .dark .tc-root {
    --tc-bg:           #1b1a18;
    --tc-surface:      rgba(255,255,255,0.03);
    --tc-stripe:       rgba(255,255,255,0.025);
    --tc-border:       rgba(255,255,255,0.08);
    --tc-text:         #e7e3da;
    --tc-dim:          #a8a299;
    --tc-faint:        #706b64;
    --tc-accent:       ${ACCENT_D};
    --tc-accent-bg:    ${REC_BG_D};
    --tc-accent-bdr:   ${REC_BDR_D};
    --tc-expand-bg:    rgba(255,255,255,0.022);
    --tc-code-bg:      rgba(255,255,255,0.08);
    --tc-na-color:     #6e6b65;
    --tc-pill-on-bg:   rgba(207,138,104,0.12);
    --tc-pill-on-bdr:  rgba(207,138,104,0.35);
    --tc-pill-on-txt:  ${ACCENT_D};
  }

  /* ── 精簡篩選列 ── */
  .tc-filter-bar {
    display: flex;
    align-items: center;
    gap: 0;
    padding: 0 14px;
    border-bottom: 1px solid var(--tc-border);
    background: var(--tc-surface);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    /* 單行、不折行，高度由內容決定（約 36px） */
    flex-wrap: nowrap;
    white-space: nowrap;
    min-height: 36px;
  }
  .tc-filter-bar::-webkit-scrollbar { display: none; }

  /* 分隔竿（All 後面） */
  .tc-filter-sep {
    flex-shrink: 0;
    width: 1px;
    height: 14px;
    background: var(--tc-border);
    margin: 0 10px 0 6px;
    align-self: center;
  }

  /* pill 基底 — 極輕量文字標籤 */
  .tc-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 5px 9px;
    border-radius: 5px;
    border: 1px solid transparent;
    background: transparent;
    color: var(--tc-faint);
    font: inherit;
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.01em;
    cursor: pointer;
    transition: color 0.12s, background 0.12s, border-color 0.12s;
    flex-shrink: 0;
    white-space: nowrap;
    /* 行高對齊 filter-bar */
    margin: 5px 1px;
  }
  .tc-pill:hover {
    color: var(--tc-text);
    background: rgba(0,0,0,0.04);
  }
  .dark .tc-pill:hover {
    background: rgba(255,255,255,0.05);
  }
  /* 選中態：細框 + 淡底 + 文字加深（非按鈕感，保持輕量） */
  .tc-pill-on {
    color: var(--tc-pill-on-txt);
    background: var(--tc-pill-on-bg);
    border-color: var(--tc-pill-on-bdr);
    font-weight: 600;
  }
  /* 全選 pill — 略小一點字型 */
  .tc-pill-all {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    padding: 4px 8px;
    color: var(--tc-dim);
  }
  .tc-pill-all.tc-pill-on {
    color: var(--tc-pill-on-txt);
  }
  /* 選中小圓點 */
  .tc-pill-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--tc-accent);
    flex-shrink: 0;
  }

  /* ── 桌面表格 ── */
  .tc-table-wrap {
    overflow: hidden;
  }
  /* Mintlify 的 MDX 渲染器會自動把 <table> 包進 data-table-wrapper：
   * 加 -mx-[var(--page-padding)] 負邊距 + w-[calc(100%+padding*2)] 全寬 + py-[1em]，
   * 讓表格往外溢出，撐破 tc-root 的圓角容器（2026-06-12 線上實證跑版）。
   * 中和它：邊距歸零、寬度收回 100%、把橫向捲動容器設在這層（sticky 左欄靠它）。 */
  .tc-root [data-table-wrapper] {
    margin: 0 !important;
    width: 100% !important;
    max-width: 100% !important;
    padding: 0 !important;
    overflow-x: auto;
    contain: none !important;
  }
  .tc-root [data-table-wrapper] > div {
    padding: 0 !important;
    margin: 0 !important;
  }
  .tc-root [data-table-wrapper]::-webkit-scrollbar { height: 4px; }
  .tc-root [data-table-wrapper]::-webkit-scrollbar-thumb { background: var(--tc-border); border-radius: 2px; }
  .tc-table {
    width: 100%;
    border-collapse: collapse;
    margin: 0 !important;
  }

  /* 表頭 */
  .tc-thead th {
    padding: 9px 15px;
    text-align: left;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--tc-faint);
    border-bottom: 2px solid var(--tc-border);
    background: var(--tc-surface);
    white-space: nowrap;
  }
  /* 維度欄（sticky 左欄） */
  .tc-thead th:first-child,
  .tc-td-dim {
    position: sticky;
    left: 0;
    z-index: 1;
  }
  .tc-thead th:first-child {
    width: 160px;
    min-width: 130px;
    background: var(--tc-surface);
    border-right: 1px solid var(--tc-border);
    z-index: 2;
  }

  /* 維度標籤欄 */
  .tc-td-dim {
    padding: 13px 15px;
    font-size: 12.5px;
    font-weight: 700;
    color: var(--tc-dim);
    vertical-align: middle;
    white-space: nowrap;
    border-bottom: 1px solid var(--tc-border);
    border-right: 1px solid var(--tc-border);
    background: var(--tc-surface);
  }

  /* 斑馬紋：奇數維度列 */
  .tc-row-even .tc-td-dim,
  .tc-row-even .tc-td {
    background-color: var(--tc-stripe);
  }
  .tc-row-even .tc-td-dim {
    background: color-mix(in srgb, var(--tc-surface) 70%, var(--tc-stripe) 30%);
  }

  /* 資料儲存格 */
  .tc-td {
    padding: 0;
    border-bottom: 1px solid var(--tc-border);
    border-left: 1px solid var(--tc-border);
    vertical-align: top;
    min-width: 150px;
  }
  .tc-td-inner {
    display: flex;
    flex-direction: column;
  }

  /* 主值行（可點擊） */
  .tc-cell-btn {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    width: 100%;
    text-align: left;
    background: transparent;
    border: none;
    padding: 13px 15px;
    color: inherit;
    font: inherit;
    font-size: 13px;
    line-height: 1.5;
    cursor: pointer;
    transition: background 0.11s;
    -webkit-tap-highlight-color: transparent;
  }
  .tc-cell-btn:hover {
    background: rgba(0,0,0,0.03);
  }
  .dark .tc-cell-btn:hover {
    background: rgba(255,255,255,0.03);
  }
  .tc-cell-btn-on {
    background: var(--tc-expand-bg);
  }

  /* 推薦標記：左 3px accent 邊框 + 淡底 */
  .tc-td-recommend {
    border-left: 3px solid var(--tc-accent-bdr);
    background: var(--tc-accent-bg);
  }
  .tc-td-recommend .tc-cell-btn:hover {
    background: rgba(191,117,81,0.05);
  }

  /* recommend 角標：超小 badge */
  .tc-rec-badge {
    flex-shrink: 0;
    margin-top: 1px;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--tc-accent);
    border: 1px solid var(--tc-accent-bdr);
    border-radius: 3px;
    padding: 1px 4px;
    white-space: nowrap;
    opacity: 0.85;
  }

  /* na 樣式 */
  .tc-na {
    font-style: italic;
    color: var(--tc-na-color);
    font-size: 12.5px;
    opacity: 0.7;
  }

  /* 等寬值 */
  .tc-code {
    font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
    font-size: 12px;
    background: var(--tc-code-bg);
    border-radius: 3px;
    padding: 1px 4px;
    word-break: break-all;
  }

  /* 展開詳情列 */
  .tc-detail-row td {
    padding: 0;
    border-bottom: 1px solid var(--tc-border);
  }
  .tc-detail-cell {
    padding: 10px 15px 12px;
    font-size: 12.5px;
    line-height: 1.65;
    color: var(--tc-dim);
    background: var(--tc-expand-bg);
    border-top: 1px dashed var(--tc-border);
    border-left: 1px solid var(--tc-border);
  }
  .tc-detail-cell:first-child {
    border-left: none;
    font-weight: 700;
    color: var(--tc-faint);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    vertical-align: top;
    padding-top: 12px;
    background: var(--tc-surface);
    white-space: nowrap;
    width: 160px;
  }
  .tc-detail-recommend {
    border-left: 3px solid var(--tc-accent-bdr) !important;
    background: var(--tc-accent-bg) !important;
  }

  /* 展開箭頭 */
  .tc-chevron {
    flex-shrink: 0;
    margin-top: 3px;
    opacity: 0.35;
    transition: transform 0.14s, opacity 0.14s;
  }
  .tc-chevron-open {
    transform: rotate(90deg);
    opacity: 0.65;
  }

  /* ── 注腳 ── */
  .tc-notes {
    border-top: 1px solid var(--tc-border);
    padding: 10px 16px 12px;
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .tc-notes-label {
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--tc-faint);
    margin-bottom: 3px;
  }
  .tc-note-item {
    font-size: 12.5px;
    color: var(--tc-dim);
    line-height: 1.55;
    padding-left: 14px;
    position: relative;
  }
  .tc-note-item::before {
    content: "*";
    position: absolute;
    left: 2px;
    color: var(--tc-faint);
  }

  /* ── 手機模式 ─────────────────────────────────────────── */
  .tc-mobile { display: none; }
  @media (max-width: 700px) {
    .tc-filter-bar { display: none; }
    .tc-table-wrap  { display: none; }
    .tc-mobile      { display: block; }

    .tc-mob-tabs {
      display: flex;
      overflow-x: auto;
      border-bottom: 1px solid var(--tc-border);
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
    }
    .tc-mob-tabs::-webkit-scrollbar { display: none; }
    .tc-mob-tab {
      flex: 1 0 auto;
      padding: 10px 16px;
      background: transparent;
      border: none;
      border-bottom: 2px solid transparent;
      color: var(--tc-dim);
      font: inherit;
      font-size: 13px;
      font-weight: 500;
      cursor: pointer;
      white-space: nowrap;
      transition: color 0.12s, border-color 0.12s;
    }
    .tc-mob-tab-on {
      color: var(--tc-accent);
      border-bottom-color: var(--tc-accent);
      font-weight: 700;
    }
    .tc-mob-cards {
      padding: 12px 0 4px;
    }
    .tc-mob-card {
      border-bottom: 1px solid var(--tc-border);
      padding: 12px 16px;
    }
    .tc-mob-card:last-child { border-bottom: none; }
    .tc-mob-dim {
      font-size: 11px;
      font-weight: 700;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      color: var(--tc-faint);
      margin-bottom: 6px;
    }
    .tc-mob-val {
      font-size: 13.5px;
      line-height: 1.55;
    }
    .tc-mob-rec {
      display: inline-block;
      margin-top: 6px;
      font-size: 9.5px;
      font-weight: 700;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      color: var(--tc-accent);
      border: 1px solid var(--tc-accent-bdr);
      border-radius: 3px;
      padding: 1px 5px;
    }
    .tc-mob-detail {
      margin-top: 7px;
      font-size: 12.5px;
      line-height: 1.65;
      color: var(--tc-dim);
    }
    .tc-notes { padding: 11px 16px 13px; }
  }
  `;
  const renderDesktopTable = () => <div className="tc-table-wrap">
      <table className="tc-table">
        <thead className="tc-thead">
          <tr>
            <th>{UI.dimensionLbl}</th>
            {visibleTools.map(tool => <th key={tool.id}>{tool.label}</th>)}
          </tr>
        </thead>
        <tbody>
          {}
          {safeDimensions.flatMap((dim, dimIdx) => {
    const isAnyExpanded = visibleTools.some(t => expandedCell === cellKey(dim.id, t.id));
    const stripeClass = dimIdx % 2 === 1 ? " tc-row-even" : "";
    const rows = [];
    rows.push(<tr key={dim.id} className={"tc-row" + stripeClass}>
                <td className="tc-td-dim">{dim.label}</td>
                {visibleTools.map(tool => {
      const cell = (dim.cells || ({}))[tool.id] || ({});
      const key = cellKey(dim.id, tool.id);
      const isOpen = expandedCell === key;
      const hasDetail = !!cell.detail;
      const isRec = !!cell.recommend;
      return <td key={tool.id} className={"tc-td" + (isRec ? " tc-td-recommend" : "")}>
                      <div className="tc-td-inner">
                        <button type="button" className={"tc-cell-btn" + (isOpen ? " tc-cell-btn-on" : "")} onClick={hasDetail ? () => toggleExpand(dim.id, tool.id) : undefined} style={hasDetail ? {} : {
        cursor: "default"
      }} aria-expanded={hasDetail ? String(isOpen) : undefined}>
                          <span style={{
        flex: "1 1 0",
        minWidth: 0
      }}>
                            {renderValue(cell)}
                          </span>
                          {isRec && <span className="tc-rec-badge">{UI.recommend}</span>}
                          {hasDetail && <svg className={"tc-chevron" + (isOpen ? " tc-chevron-open" : "")} width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                              <polyline points="9 18 15 12 9 6" />
                            </svg>}
                        </button>
                      </div>
                    </td>;
    })}
              </tr>);
    if (isAnyExpanded) {
      const openToolId = visibleTools.find(t => expandedCell === cellKey(dim.id, t.id))?.id;
      const openCell = openToolId ? (dim.cells || ({}))[openToolId] || ({}) : {};
      if (openCell.detail) {
        const openTool = safeTools.find(t => t.id === openToolId);
        const isRec = !!openCell.recommend;
        rows.push(<tr key={dim.id + "-detail"} className="tc-detail-row">
                    <td className="tc-detail-cell">
                      {openTool ? openTool.label : ""}
                    </td>
                    <td colSpan={visibleTools.length} className={"tc-detail-cell" + (isRec ? " tc-detail-recommend" : "")}>
                      {openCell.detail}
                    </td>
                  </tr>);
      }
    }
    return rows;
  })}
        </tbody>
      </table>
    </div>;
  const renderMobile = () => {
    const activeTool = safeTools.find(t => t.id === mobileTool) || safeTools[0];
    return <div className="tc-mobile">
        <div className="tc-mob-tabs" role="tablist">
          {safeTools.map(tool => <button key={tool.id} type="button" role="tab" aria-selected={tool.id === mobileTool ? "true" : "false"} className={"tc-mob-tab" + (tool.id === mobileTool ? " tc-mob-tab-on" : "")} onClick={() => setMobileTool(tool.id)}>
              {tool.label}
            </button>)}
        </div>

        <div className="tc-mob-cards">
          {safeDimensions.map(dim => {
      const cell = (dim.cells || ({}))[activeTool.id] || ({});
      const isRec = !!cell.recommend;
      return <div key={dim.id} className="tc-mob-card">
                <div className="tc-mob-dim">{dim.label}</div>
                <div className="tc-mob-val">{renderValue(cell)}</div>
                {isRec && <div className="tc-mob-rec">{UI.recommend}</div>}
                {cell.detail && <div className="tc-mob-detail">{cell.detail}</div>}
              </div>;
    })}
        </div>
      </div>;
  };
  return <div className="tc-root">
      <style>{css}</style>

      {}
      <div className="tc-filter-bar">
        <button type="button" className={"tc-pill tc-pill-all" + (isAllSelected ? " tc-pill-on" : "")} onClick={toggleAll} aria-pressed={String(isAllSelected)}>
          {UI.allTools}
        </button>

        {}
        <span className="tc-filter-sep" aria-hidden="true" />

        {safeTools.map(tool => {
    const isOn = selectedIds.includes(tool.id);
    return <button key={tool.id} type="button" className={"tc-pill" + (isOn ? " tc-pill-on" : "")} onClick={() => toggleTool(tool.id)} aria-pressed={String(isOn)}>
              {isOn && <span className="tc-pill-dot" aria-hidden="true" />}
              {tool.label}
            </button>;
  })}
      </div>

      {}
      {visibleTools.length > 0 && renderDesktopTable()}

      {}
      {renderMobile()}

      {}
      {notes.length > 0 && <div className="tc-notes">
          <div className="tc-notes-label">{UI.notes}</div>
          {notes.map((note, i) => <div key={i} className="tc-note-item">{note}</div>)}
        </div>}
    </div>;
};

export const LearnerPrimer = ({items = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    title: "Before this unit, be honest with yourself",
    sub: "If you can't answer these, that gap is exactly what this unit closes."
  } : {
    title: "讀這個單元前，先誠實面對",
    sub: "這幾題答不出來，正是這個單元要替你補的洞。"
  };
  const css = `
  .lp-root{--lp-bg:#FAF7F1;--lp-surface:rgba(191,117,81,0.05);--lp-border:rgba(0,0,0,0.09);--lp-edge:rgba(191,117,81,0.42);--lp-text:#2b2722;--lp-dim:#6f6a62;--lp-accent:#bf7551;border:1px solid var(--lp-border);border-left:3px solid var(--lp-edge);border-radius:13px;background:var(--lp-bg);color:var(--lp-text);overflow:hidden;margin:1.25rem 0;}
  .dark .lp-root{--lp-bg:#1b1a18;--lp-surface:rgba(207,138,104,0.07);--lp-border:rgba(255,255,255,0.08);--lp-edge:rgba(207,138,104,0.5);--lp-text:#e7e3da;--lp-dim:#a8a299;--lp-accent:#cf8a68;}
  .lp-head{display:flex;align-items:center;gap:9px;padding:13px 18px 11px;border-bottom:1px solid var(--lp-border);background:var(--lp-surface);}
  .lp-ic{color:var(--lp-accent);flex-shrink:0;}
  .lp-htx{display:flex;flex-direction:column;gap:1px;min-width:0;}
  .lp-title{font-size:14px;font-weight:650;line-height:1.3;letter-spacing:.01em;}
  .lp-sub{font-size:12px;color:var(--lp-dim);line-height:1.4;}
  .lp-list{list-style:none;margin:0;padding:10px 18px 14px;display:flex;flex-direction:column;gap:0;}
  .lp-item{display:flex;align-items:baseline;gap:11px;padding:7px 0;font-size:14px;line-height:1.6;border-top:1px solid var(--lp-border);}
  .lp-item:first-child{border-top:none;}
  .lp-mark{flex-shrink:0;color:var(--lp-accent);font-size:13px;font-weight:700;line-height:1.55;font-variant-numeric:tabular-nums;opacity:.85;}
  .lp-q{flex:1 1 0;min-width:0;color:var(--lp-text);}
  @media (max-width:620px){.lp-head{padding:12px 14px 10px;}.lp-list{padding:8px 14px 12px;}}
  `;
  return <div className="lp-root">
      <style>{css}</style>
      <div className="lp-head">
        <svg className="lp-ic" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></svg>
        <span className="lp-htx">
          <span className="lp-title">{t.title}</span>
          <span className="lp-sub">{t.sub}</span>
        </span>
      </div>
      <ul className="lp-list">
        {items.map((q, i) => <li className="lp-item" key={i}>
            <span className="lp-mark">{String(i + 1).padStart(2, "0")}</span>
            <span className="lp-q">{q}</span>
          </li>)}
      </ul>
    </div>;
};

<LearnerPrimer
  lang="zh"
  items={[
"rules 不是把 CLAUDE.md 拆碎，是依路徑觸達。",
"paths 寫 **/* 等於沒寫 path-scoped。",
"跨 domain 規則不拆，互相稀釋指令。",
"rule 是 context 層，模型可能漏；要保證執行請走 Hook。",
"寫了 rules 不 review，三個月後沒一條是對的。",
"判準就一條：唸給不相關任務會不會干擾。",
"你把 paths 寫太寬，每次 session 全量載入。",
]}
/>

<Info>
  **這個單元解決什麼問題**

  `.claude/rules/` 把 `CLAUDE.md` 的常駐約束切成可依路徑範圍觸達的模組。當 `CLAUDE.md` 膨脹、跨 domain 規則互相干擾、或某些規則只對特定副檔名有意義時，rules 是正解。本單元講 rules 與 `CLAUDE.md` 的分工邊界、path-scoped 觸達機制、命名與分層組織，並對照其他工具的同類機制。
</Info>

## 學習目標

* [ ] 說出 rules 與 `CLAUDE.md` 的差別，以及何時該拆到 rules。
* [ ] 用 `paths` frontmatter 讓規則只在符合 glob 的檔案觸及時生效。
* [ ] 依 domain 或語言把約束拆進獨立規則檔，避免單一巨檔。
* [ ] 判斷一條約束該放 `CLAUDE.md`、獨立 rule 檔、Skill 還是 Hook，並說出判準。
* [ ] 對照 OpenAI Codex、Google Antigravity、GitHub Copilot、Cursor 對等機制的差異。

***

## 1. rules 是什麼

Rules 是放在 `.claude/rules/` 目錄下的 Markdown 檔，與 `CLAUDE.md` 同一層級載入（截至 2026-06 依官方記憶章節 \[1]）。基本特性：

* 每個 `.md` 為一個模組，命名依主題（`testing.md`、`api-design.md`、`python-style.md`）。
* 不寫 `paths` frontmatter 時，與 `CLAUDE.md` 等價：每次 session 啟動全量載入。
* 寫了 `paths` frontmatter 時，**只在 Claude 觸及符合 glob 的檔案時才載入**。
* 與 `CLAUDE.md` 同屬「context 層次」，不是「強制執行」。需要保證執行的約束，請交給 Hook（見 [04-6 Hooks](/code-agent/customization/hooks)）。

`.claude/rules/` 內的所有 `.md` 檔都會被遞迴讀取，可以再分子目錄（`frontend/`、`backend/`、`ml/`）做階層化組織 \[1]。

<Tip>
  **rules 是「主動常駐」，不是「按需呼叫」**

  寫了 `paths` 之後雖然只在相關檔案時載入，但**仍會進入 Claude 觸達該檔案那一刻的 context**。這跟 Skill 的「使用者或模型叫用時才注入完整內容」不同（見 [04-4 Skills](/code-agent/customization/skills)）。Rules 是「在某個子集的工作階段裡常駐」，不是「像工具一樣被叫一次」。
</Tip>

## 2. 與 `CLAUDE.md` 的分工

`CLAUDE.md`（使用者級、專案級、本地層）放**跨任務、跨語言、跨目錄**都適用的通識約束。rules 放**情境化**約束：只在某個路徑或副檔名才有意義的規則。

判準只有一個：把這條規則**唸給一個不相關任務的 agent 聽，會不會造成干擾或矛盾？** 會 → 拆到 rules。不會 → 留在 `CLAUDE.md`。

<Note>
  **怎麼拆**

  留在 `CLAUDE.md`：

  * 「回應一律繁體中文，第二人稱」
  * 「不要 commit `.env`」
  * 「建構指令：`make build`、測試：`make test`」
  * 「commit 前跑 `make lint`」

  拆到 `.claude/rules/python-style.md`（path-scoped 到 `**/*.py`）：

  * 「Python 用 type hints，公開函式不接受 `Any`」
  * 「pytest fixture 集中放在 `conftest.py`，不在單一測試檔內定義跨檔 fixture」
  * 「公開 API 函式必須有 docstring（Google 風格）」

  拆到 `.claude/rules/api-design.md`（path-scoped 到 `src/api/**`）：

  * 「每個 endpoint 必須在 OpenAPI spec 同步登記」
  * 「輸入驗證在 handler 邊界就做完，不要丟到內部函式」
  * 「錯誤回應統一使用 `{code, message, details}` 結構」

  這三塊互相不干擾：寫 React 時 Python 風格不會被載入、寫 Python 時 API 設計規範不會被載入。每次 session 載入的 token 預算比把全塞進 `CLAUDE.md` 少一個量級。
</Note>

## 3. path-scoped 規則：觸達機制

`.claude/rules/` 內的 `.md` 檔可以用 YAML frontmatter 的 `paths` 欄位限定觸達範圍。Claude Code 在工作階段中**讀到符合 glob 的檔案**時，才把該 rule 注入 context \[1]。

基本寫法：

```markdown theme={null}
---
paths:
  - "src/api/**/*.ts"
---

# API Development Rules

- All API endpoints must include input validation
- Use the standard error response format
- Include OpenAPI documentation comments
```

glob 語法（與 `.gitignore` 風格類似）：

| 模式                     | 匹配                                 |
| ---------------------- | ---------------------------------- |
| `**/*.ts`              | 任何目錄下的所有 `.ts` 檔                   |
| `src/**/*`             | `src/` 下所有檔案                       |
| `*.md`                 | 專案根目錄下的 `.md` 檔                    |
| `src/components/*.tsx` | `src/components/` 下的 `.tsx`（不含子目錄） |
| `src/**/*.{ts,tsx}`    | `src/` 下所有 `.ts` 與 `.tsx`（含子目錄）    |
| `tests/**/*.test.ts`   | 測試檔                                |

可同時列多個模式，或用 brace expansion 一次匹配多副檔名 \[1]。

<Warning>
  **path-scoped 規則只在讀檔時載入**

  path-scoped rule 不是「每次 tool call 觸及都載入」，而是「Claude 讀到符合 glob 的檔案時載入」。意思是：如果你叫 Claude 改檔案前它還沒讀過符合 glob 的檔案，rule 就不在 context 裡。對話脈絡累積到這個點之前，行為可能不一致。
</Warning>

## 4. 命名與分層組織

命名原則：用具體語意名稱，不用具體行為描述（不要叫 `do-not-touch.md`，要叫 `api-migration-safety.md`）。

常見分層：

<Tree>
  <Tree.Folder name=".claude" defaultOpen>
    <Tree.File name="CLAUDE.md" />

    <Tree.Folder name="rules" defaultOpen>
      <Tree.Folder name="common" defaultOpen>
        <Tree.File name="code-style.md · 通用語言風格" />

        <Tree.File name="testing.md · 測試原則" />

        <Tree.File name="security.md · 安全禁則（path-scoped 到 src/）" />
      </Tree.Folder>

      <Tree.Folder name="frontend" defaultOpen>
        <Tree.File name="react-style.md · React 慣例" />

        <Tree.File name="a11y.md · 無障礙" />
      </Tree.Folder>

      <Tree.Folder name="backend" defaultOpen>
        <Tree.File name="api-design.md · API 設計" />

        <Tree.File name="db-migrations.md · 資料庫遷移" />
      </Tree.Folder>

      <Tree.Folder name="ml" defaultOpen>
        <Tree.File name="experiment-tracking.md" />
      </Tree.Folder>
    </Tree.Folder>
  </Tree.Folder>
</Tree>

單一 rule 檔的長度：官方建議 `SKILL.md` 在 500 行以內（這是針對 skill 的建議，但同一精神也適用於 rules）。超過這個長度考慮再拆或升級為 Skill。

## 5. 跨專案共享：用 symlink

`.claude/rules/` 支援 symlink，所以你可以在 `~/shared-claude-rules/` 維護一份共用規則集，跨多個專案 link 進來 \[1]：

```bash theme={null}
ln -s ~/shared-claude-rules .claude/rules/shared
ln -s ~/company-standards/security.md .claude/rules/security.md
```

circular symlink 會被偵測並繞過，不會無窮遞迴 \[1]。Windows 上建立 symlink 需要 Administrator 或 Developer Mode；如果不能 symlink，`.claude/rules/` 內部用 `@path` 匯入 `~/shared-*.md` 是另一條路。

## 6. 使用者級 rules

放在 `~/.claude/rules/` 的 rules 適用於**所有專案**，優先序在使用者級 `CLAUDE.md` 之後、專案級 rules 之前 \[1]：

<Tree>
  <Tree.Folder name="~/.claude/rules" defaultOpen>
    <Tree.File name="preferences.md · 個人編碼偏好" />

    <Tree.File name="workflows.md · 個人常用工作流" />
  </Tree.Folder>
</Tree>

常見放法：個人工具捷徑、編輯器設定偏好、跨專案都適用的回應風格。專案 rules 則覆寫使用者 rules 的同名檔案。

## 7. 與 Skill、Hook 的界線

三者職責不重疊，判斷準則：

| 需求                  | 放哪裡                   |
| ------------------- | --------------------- |
| 「每次編輯 Python 都要遵守」  | rules（path-scoped）    |
| 「讓模型在適當時機自動跑這套程序」   | Skill                 |
| 「每次 commit 前一定要跑測試」 | Hook（確定性保證）           |
| 「我知道現在要跑這套程序」       | Command（`/name` 主動觸發） |
| 「這是專案通識，所有任務都該知道」   | `CLAUDE.md`           |

決策樹的快速版：

* 需要模型**判斷什麼時候插入** → Skill
* 需要使用者**主動觸發** → Command
* 需要**每次某個動作後都跑**（不論模型判斷）→ Hook
* **常駐、每次 session 都載入**且跨任務通用 → `CLAUDE.md`
* **常駐、只在特定路徑時載入** → rules（path-scoped）
* **要記的是事實而非指令** → 自動記憶

## 8. 工具對照

各家工具的規則檔機制在 path-scoped 支援與 frontmatter 細節上差異不小（截至 2026-06）：

<ToolCompare
  lang="zh"
  tools={[
{ id: "claude", label: "Claude Code" },
{ id: "codex",  label: "OpenAI Codex" },
{ id: "gemini", label: "Google Gemini CLI" },
{ id: "copilot",label: "GitHub Copilot" },
{ id: "cursor", label: "Cursor" },
]}
  dimensions={[
{
  id: "rule-location",
  label: "規則檔位置",
  cells: {
    claude:  { value: ".claude/rules/*.md",                        recommend: true, detail: "子目錄亦可（frontend/、backend/、ml/），遞迴讀取。" },
    codex:   { value: "政策檔（Starlark *.rules）", detail: "OpenAI Codex 的 rules 是 Starlark 撰寫的可執行政策檔，用來控制 agent 行為的白名單/黑名單，語意上接近 Hook 而非 Markdown 規則檔。" },
    gemini:  { value: "~/.gemini/settings.json、<workspace>/.agent/rules/", detail: "MCP server 與工具規則設定在 settings.json 的 mcpServers 區段；自訂工具規則可放 .agent/rules/ 子目錄。" },
    copilot: { value: ".github/copilot-instructions.md + .github/instructions/*.instructions.md" },
    cursor:  { value: ".cursor/rules/*.mdc", detail: ".mdc 為強制副檔名，.md 無效。frontmatter 欄位：description / globs / alwaysApply。" },
  },
},
{
  id: "path-scoped",
  label: "path-scoped 支援",
  cells: {
    claude:  { value: "支援（paths frontmatter，glob）",             recommend: true, detail: "寫了 paths 後只在 Claude 讀到符合 glob 的檔案時才載入，是省 token 的核心。paths 寫 **/* 等於沒寫。" },
    codex:   { value: "不適用（Starlark 是 policy 邏輯）", detail: "Starlark 政策檔處理 allowlist/denylist 邏輯，不是依路徑 glob 載入的 Markdown 規則。" },
    gemini:  { value: "透過多檔 .agent/rules/ 與目錄結構" },
    copilot: { value: "支援（applyTo glob，可選 excludeAgent）", detail: ".instructions.md frontmatter 的 applyTo 欄位可限定 glob，excludeAgent 可排除特定 agent 載入。" },
    cursor:  { value: "支援（.mdc frontmatter globs + alwaysApply）", detail: "alwaysApply: true 無條件載入；globs 設定時自動載入；都不設只能 @-mention 手動引用。" },
  },
},
{
  id: "modular",
  label: "模組化分檔",
  cells: {
    claude:  { value: "多 .md 分檔，子目錄亦可",                   recommend: true },
    codex:   { value: "多 *.rules 分檔" },
    gemini:  { value: "支援多檔" },
    copilot: { value: "支援多 .instructions.md 分檔" },
    cursor:  { value: "支援多 .mdc 分檔" },
  },
},
{
  id: "cross-tool",
  label: "跨工具共通層",
  cells: {
    claude:  { value: "讀 CLAUDE.md；@AGENTS.md 匯入可用" },
    codex:   { value: "AGENTS.md 原生" },
    gemini:  { value: "AGENTS.md 原生" },
    copilot: { value: "AGENTS.md 原生" },
    cursor:  { value: "AGENTS.md 原生" },
  },
},
{
  id: "precedence",
  label: "規則優先序",
  cells: {
    claude:  { value: "使用者級先載入，衝突時專案層覆寫",           recommend: true },
    codex:   { value: "managed（requirements.toml）為封頂" },
    gemini:  { value: "System Rules（DeepMind 不可變）> 使用者全域 > 專案" },
    copilot: { value: "所有命中指令檔併用，無嚴格優先序" },
    cursor:  { value: "依 .mdc frontmatter 設定與官方慣例" },
  },
},
]}
  notes={[
"OpenAI Codex 的 rules 語意上接近 Hook（可執行政策），不是 Markdown 規則。",
"GitHub Copilot 的 .instructions.md 的 applyTo 欄位限定 glob，語意與 Claude 的 paths frontmatter 相近。",
"Cursor 為第三方 IDE（Anysphere），本 Playbook 僅短提一欄。",
]}
/>

<Note>
  **命名澄清**

  * **OpenAI Codex 的 "rules"** 是 Starlark 撰寫的可執行政策檔（用來控制 agent 行為的白名單/黑名單），不是 Markdown 規則檔。語意上接近 Hook 而非本單元討論的 `CLAUDE.md` 模組化。
  * **Cursor** 用 `.mdc` 副檔名（`.md` 不行），frontmatter 欄位為 `description` / `globs` / `alwaysApply` 三個 \[3]。`alwaysApply: true` 等同「無條件載入」；`globs` 設定時只在匹配檔案時自動載入；只有 `description` 時由模型語意判斷載入時機；都不寫時只能 `@`-mention 手動引用。
  * **GitHub Copilot** 的 `.instructions.md` 用 `applyTo` 限定 glob，並可加 `excludeAgent: "code-review"` 或 `"cloud-agent"` 排除特定 agent 載入 \[4]。
</Note>

## 9. 動手做

<Steps>
  <Step title="拆一條">
    把 `CLAUDE.md` 裡的 Python 編碼風格段落整段搬到 `.claude/rules/python-style.md`，加 `paths: ["**/*.py"]`。
  </Step>

  <Step title="驗證觸達">
    在同一個 session 跑 `/memory`，確認 `python-style.md` 出現且附帶 path 標註。
  </Step>

  <Step title="測試無載入情境">
    叫 Claude 改一個 `*.md` 檔（不在 `**/*.py` glob 內），問它 Python 風格規則；它應該不引用 `python-style.md`。
  </Step>

  <Step title="跨專案共享">
    把使用者級 `~/.claude/rules/preferences.md` 建出來；在兩個不同專案啟動 session 確認它都被載入。
  </Step>
</Steps>

## 10. 常見誤區

<Warning>
  **反模式清單**

  * **所有約束全塞 `CLAUDE.md`**：跨 domain 規則互相干擾、每次 session 全量載入、token 預算飆高，且長規則容易被模型「分心忽略」。
  * **照抄別人的 rules 設定**：別人 repo 不需要、甚至不該有的約束，會被你的 model 強行套用。
  * **把「保證執行」寫成 rule 的祈使句**：rule 只是 context，模型可能在多步驟任務中遺漏。需要確定性保證的事必須走 Hook。
  * **`paths` 寫太寬**：用 `**/*` 當 paths，等於沒寫 path-scoped。每次 session 啟動就全量載入，浪費這個機制。
  * **混淆 rules 與 Skill 的觸發時機**：rule 是 Claude 讀到符合檔案時被注入；Skill 是 Claude 判斷語意相關時叫用（見 [04-4 Skills](/code-agent/customization/skills)）。把「每次編輯 Python 都要跑 ruff」寫成 rule 對；寫成 Skill 會等到使用者或模型主動叫用。
</Warning>

## 自我檢核

<Check>
  **通過本單元的標準**

  1. 你能說出 rules 與 `CLAUDE.md` 的差異，以及何時該拆嗎？
  2. 你能寫出正確的 `paths` frontmatter，並用 glob 模式限定觸達範圍嗎？
  3. 你能依需求判斷一條約束該放 `CLAUDE.md`、獨立 rule 檔、Skill 還是 Hook 嗎？
  4. 你能在五個工具的對照表上填出你的主力工具的規則檔機制嗎？
</Check>

## 來源與延伸閱讀

事實主張依官方文件，快變動項標註截至 2026-05。

<div className="references">
  * \[1] Anthropic, "How Claude remembers your project," code.claude.com, 2026. Available: [https://code.claude.com/docs/en/memory](https://code.claude.com/docs/en/memory) （截至 2026-06；含 `.claude/rules/`、`paths` frontmatter、symlink、auto memory 完整章節）

  * \[2] Anthropic, "Extend Claude with skills," code.claude.com, 2026. Available: [https://code.claude.com/docs/en/skills](https://code.claude.com/docs/en/skills) （截至 2026-06；含 Skill 與 `CLAUDE.md` / rules 的分工）

  * \[3] Cursor, "Rules," cursor.com, 2026. Available: [https://cursor.com/docs/context/rules](https://cursor.com/docs/context/rules) （截至 2026-06；`.mdc` 與 frontmatter `description` / `globs` / `alwaysApply`）

  * \[4] GitHub Docs, "Adding custom instructions for GitHub Copilot," docs.github.com, 2026. Available: [https://docs.github.com/en/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot](https://docs.github.com/en/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot) （截至 2026-06；`.instructions.md` 與 `applyTo` glob）

  * \[5] Agentic AI Foundation (Linux Foundation), "AGENTS.md," 2026. Available: [https://agents.md/](https://agents.md/) （截至 2026-06；跨工具共通專案規則檔標準）
</div>

* 設定層級模型見 [02-1 設定的層級模型](/code-agent/configuration/config-layer-model)。
* `CLAUDE.md` 與自動記憶見 [04-1 CLAUDE.md 與記憶檔](/code-agent/customization/claude-md-memory)。
* Skill（按需叫用程序）見 [04-4 Skills](/code-agent/customization/skills)。
* Hook（確定性保證）見 [04-6 Hooks](/code-agent/customization/hooks)。
* 上下文工程與規則載入成本見 [01-4 上下文工程](/code-agent/foundations/context-engineering)。
