iter(g326): ua search 补同义词回退
This commit is contained in:
@@ -9,6 +9,7 @@ import { auditContrast, formatReport, PAIRS } from '../lib/contrast-audit.mjs';
|
||||
import { shellPalette } from '../lib/shell-palette.mjs';
|
||||
import { auditMotion, formatMotionReport } from '../lib/motion-audit.mjs';
|
||||
import { auditSurface, formatSurfaceReport, LADDER } from '../lib/surface-audit.mjs';
|
||||
import { SYNONYMS, synonymIds } from '../lib/synonyms.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -258,6 +259,20 @@ function matchOf(kw, comp) {
|
||||
return best;
|
||||
}
|
||||
|
||||
/*** 同义回退:整组关键词在 id / 标题 / 参数 / 步骤与配方里都落空时,按同义词表换算出候选组件
|
||||
* CLI search 与 MCP search_components 共用这一份,避免两处口径漂移;返回空数组表示「回退也无解」 ***/
|
||||
function fallbackBySynonym(kws) {
|
||||
const sets = kws.map((kw) => synonymIds(kw));
|
||||
if (sets.some((s) => !s)) return []; // 有词未被同义词表覆盖:整组回退无从谈起
|
||||
const all = collectComponents();
|
||||
let cands = all.filter((x) => sets.every((s) => s.has(x.comp.id))); // 多词取交集,与搜索的 AND 语义保持一致
|
||||
if (!cands.length) { // 交集为空(两个词各指不同组件)退化为并集:说了两个诉求就把相关项都给到
|
||||
const union = new Set(sets.flatMap((s) => [...s]));
|
||||
cands = all.filter((x) => union.has(x.comp.id));
|
||||
}
|
||||
return cands;
|
||||
}
|
||||
|
||||
/*** 按关键词搜组件:多词取最弱命中(AND 语义),并标出该词命中的字段,便于判断为何被召回 ***/
|
||||
/*** 单个配方对多关键词的命中:标题 3 / 组件 2 / 要点 1,返回最弱档(与组件搜索同一排序口径),未命中返回 null ***/
|
||||
function recipeMatch(kws, r) {
|
||||
@@ -292,6 +307,10 @@ function cmdSearch(words = [], args = []) {
|
||||
.map((r) => ({ r, m: recipeMatch(kws, r) }))
|
||||
.filter((x) => x.m)
|
||||
.sort((a, b) => b.m.weight - a.m.weight || Number(a.r.id) - Number(b.r.id));
|
||||
// 常规两域全空才动用同义词:命中正常时不掺水,免得「搜 select 冒出一堆 drawer」
|
||||
if (!hits.length && !recipeHits.length) {
|
||||
for (const x of fallbackBySynonym(kws)) hits.push({ pkg: x.pkg, comp: x.comp, score: 1, field: '同义词', keyword: kws.join(' ') });
|
||||
}
|
||||
if (!hits.length && !recipeHits.length) {
|
||||
const near = nearIds(kws.join(''));
|
||||
console.error(`no match: ${words.join(' ')}${near.length ? `(相近:${near.join(' / ')})` : ''}`);
|
||||
@@ -310,7 +329,7 @@ function cmdSearch(words = [], args = []) {
|
||||
);
|
||||
}
|
||||
for (const h of hits) {
|
||||
const why = h.field === 'id' || h.field === '标题' ? '' : ` ← ${h.field}含「${h.keyword}」`;
|
||||
const why = h.field === '同义词' ? ` ← 同义词「${h.keyword}」` : h.field === 'id' || h.field === '标题' ? '' : ` ← ${h.field}含「${h.keyword}」`;
|
||||
console.log(`${String(h.comp.id).padEnd(16)} ${h.comp.title ?? ''} (${h.pkg.name})${why}`);
|
||||
}
|
||||
for (const { r, m } of recipeHits) {
|
||||
@@ -320,7 +339,8 @@ function cmdSearch(words = [], args = []) {
|
||||
const parts = [];
|
||||
if (hits.length) parts.push(`${hits.length} 个组件`);
|
||||
if (recipeHits.length) parts.push(`${recipeHits.length} 条配方`);
|
||||
console.log(`\n共 ${parts.join('、')}命中;查参数:ua info <组件id>,看骨架:ua recipes <编号>`);
|
||||
const viaSyn = hits.length && hits.every((h) => h.field === '同义词'); // 全靠同义回退才算「口语命中」
|
||||
console.log(`\n共 ${parts.join('、')}命中${viaSyn ? '(常规字段无命中,按同义词表回退)' : ''};查参数:ua info <组件id>,看骨架:ua recipes <编号>`);
|
||||
}
|
||||
|
||||
/*** 输出包或组件信息:默认人读摘要,--json 输出原始 JSON;name 支持逗号分隔一次查多个(单值保持原输出结构不破坏脚本) ***/
|
||||
@@ -910,7 +930,12 @@ function cmdMcp() {
|
||||
.map((r) => ({ r, m: recipeMatch([q], r) }))
|
||||
.filter((x) => x.m)
|
||||
.map(({ r, m }) => ({ id: r.id, title: r.title, components: recipeComps(r), matched: { field: m.label, keyword: m.keyword } }));
|
||||
if (!pkgs.length && !comps.length && !recs.length) throw new Error(`no match: ${args.query}`);
|
||||
if (!pkgs.length && !comps.length && !recs.length) {
|
||||
// 三域全空时与 CLI 同源回退:Agent 常拿到中文口语词(「弹窗」),不兜底就只能反复试错
|
||||
const syn = fallbackBySynonym([q]).map(({ pkg, comp }) => ({ package: pkg.name, id: comp.id, title: comp.title, matched: { field: '同义词', keyword: q } }));
|
||||
if (!syn.length) throw new Error(`no match: ${args.query}`);
|
||||
return { packages: pkgs, components: syn, recipes: recs };
|
||||
}
|
||||
return { packages: pkgs, components: comps, recipes: recs };
|
||||
}
|
||||
if (name === 'get_tokens') {
|
||||
@@ -1605,6 +1630,24 @@ function checkVisualChecklist() {
|
||||
}
|
||||
|
||||
/*** 清单一致性校验:组件文档块的 id / props 说明 / demo 锚点必须与代码和演示产物对齐 ***/
|
||||
/*** 校验同义词表:目标 id 必须真实存在(改名后残留会让回退指到空气),同一个词不得归两组(歧义会让结果随表序漂移) ***/
|
||||
function checkSynonyms(ids) {
|
||||
const issues = [];
|
||||
const seen = new Map(); // 词 → 已登记的组件 id(用于报重复)
|
||||
for (const group of SYNONYMS) {
|
||||
const tag = `同义词「${group.words[0]}」`;
|
||||
if (!(group.words ?? []).length) issues.push('同义词表有一组缺少 words');
|
||||
if (!(group.ids ?? []).length) issues.push(`${tag}: 缺少 ids`);
|
||||
for (const id of group.ids ?? []) if (!ids.has(id)) issues.push(`${tag}: 目标组件 ${id} 不在清单里(改名后忘改同义词表)`);
|
||||
for (const w of group.words ?? []) {
|
||||
if (seen.has(w)) issues.push(`${tag}: 词「${w}」已归入同义词「${seen.get(w)}」(一词只许一组,否则回退结果随表序变化)`);
|
||||
seen.set(w, group.words[0]);
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
/*** 校验清单 ↔ 文档 ↔ 演示一致,任一项不符即退出码 1(口径见 AGENTS.md 「门禁一条龙」) ***/
|
||||
function cmdVerify() {
|
||||
const issues = [];
|
||||
const pkgs = collectManifests(); // 一次收集,供组件校验与 README 区块校验共用
|
||||
@@ -1667,6 +1710,7 @@ function cmdVerify() {
|
||||
.flatMap((p) => p.docs?.components ?? []).length;
|
||||
issues.push(...checkCountDocs(skinTotal));
|
||||
issues.push(...checkReadme(pkgs));
|
||||
issues.push(...checkSynonyms(new Set(pkgs.flatMap((p) => (p.docs?.components ?? []).map((c) => c.id))))); // 同义词表指向的组件必须真实存在
|
||||
checkDarkTokens(issues);
|
||||
checkTokenParity(issues);
|
||||
checkLlmsFull(pkgs, issues);
|
||||
@@ -1678,7 +1722,7 @@ function cmdVerify() {
|
||||
console.error(`${issues.length} violation(s)`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('verify OK (manifest ↔ docs ↔ demo 一致,文档 props ↔ 入参接口双向一致,皮肤无硬编码颜色与未定义令牌、暗色令牌齐全、tokens.json ↔ tokens.css 名字与值双源一致、@keyframes 无复制粘贴的重复帧、滚动容器全在细条规则内,README 组件清单与分组同步,backlog ↔ 留痕一致(编号严格递增、无重复 id、留痕时间线单调且带 +08:00),组合配方结构完整,MCP 工具清单三处一致,视觉回归抽查清单十八组件 × 五维度齐全)');
|
||||
console.log('verify OK (manifest ↔ docs ↔ demo 一致,文档 props ↔ 入参接口双向一致,皮肤无硬编码颜色与未定义令牌、暗色令牌齐全、tokens.json ↔ tokens.css 名字与值双源一致、@keyframes 无复制粘贴的重复帧、滚动容器全在细条规则内,README 组件清单与分组同步,backlog ↔ 留痕一致(编号严格递增、无重复 id、留痕时间线单调且带 +08:00),组合配方结构完整,MCP 工具清单三处一致,搜索同义词表目标存在且无歧义,视觉回归抽查清单十八组件 × 五维度齐全)');
|
||||
}
|
||||
|
||||
/*** 校验 git 提交信息是否符合 COMMITTING.md 规范(参数为 message 文件路径或直接 message 文本),违规退出码 1 ***/
|
||||
|
||||
Reference in New Issue
Block a user