youiyoux 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,8 @@ No config, no account. You get:
16
16
  - `youiyoux-card.png`: a 1200×630 card with the headline numbers.
17
17
  - `youiyoux-summary.md`: paste it into a PR or issue.
18
18
 
19
+ New to it? [Getting started](https://github.com/ap9000/youiyoux/blob/main/docs/getting-started.md): the report, the top fix, your agent and CI, in five steps.
20
+
19
21
  ## For your agent
20
22
 
21
23
  ```sh
@@ -3,7 +3,7 @@ import { createRequire as __youiRequire } from "node:module"; import { dirname a
3
3
  // src/hub-packages.js
4
4
  var PACKAGES = {
5
5
  core: () => import("./src-AXF5XUQE.js"),
6
- scanner: () => import("./src-Q3ZJHEUQ.js"),
6
+ scanner: () => import("./src-DUBN6O7T.js"),
7
7
  discover: () => import("./src-ULAQV67E.js"),
8
8
  tokens: () => import("./src-HBKEBCH6.js")
9
9
  };
@@ -9,7 +9,7 @@ import {
9
9
  scanProject,
10
10
  screenSlug,
11
11
  tokenImpact
12
- } from "./chunk-75LXBAYB.js";
12
+ } from "./chunk-RQUTV7SO.js";
13
13
  import {
14
14
  buildTokens,
15
15
  contrastText,
@@ -9,7 +9,7 @@ import {
9
9
  startRuleEditor,
10
10
  style,
11
11
  truncate
12
- } from "./chunk-2F2HM7FE.js";
12
+ } from "./chunk-ZXINIRJ5.js";
13
13
  import {
14
14
  BASELINE_FILE,
15
15
  DEFAULT_EXCLUDE,
@@ -64,7 +64,7 @@ import {
64
64
  vendoredPath,
65
65
  whereUsed,
66
66
  withFile
67
- } from "./chunk-75LXBAYB.js";
67
+ } from "./chunk-RQUTV7SO.js";
68
68
  import {
69
69
  escapingLinks,
70
70
  within
@@ -1382,6 +1382,8 @@ async function themeTokens(root, modules) {
1382
1382
  const theme = await buildTheme(root, sheets, false);
1383
1383
  const tokens = [];
1384
1384
  const byVar = /* @__PURE__ */ new Map();
1385
+ const themeKeys = /* @__PURE__ */ new Map();
1386
+ const ids = /* @__PURE__ */ new Set();
1385
1387
  for (const [name, raw] of theme.declared) {
1386
1388
  const value = substituteVars(raw, (key2) => theme.variable(key2));
1387
1389
  if (value === null) continue;
@@ -1390,11 +1392,16 @@ async function themeTokens(root, modules) {
1390
1392
  const px = kind ? theme.length(value) : null;
1391
1393
  if (!color && px === null) continue;
1392
1394
  const light = color ?? { value: px, unit: "px" };
1393
- const token = { id: `${color ? "color" : kind}.${name.slice(2)}`, type: color ? "color" : "dimension", cssName: name.slice(2), light, dark: light };
1395
+ const group = color ? "color" : kind;
1396
+ const short = `${group}.${name.slice(2).replace(new RegExp(`^${group}-`), "")}`;
1397
+ const id = ids.has(short) ? `${group}.${name.slice(2)}` : short;
1398
+ ids.add(id);
1399
+ const token = { id, type: color ? "color" : "dimension", cssName: name.slice(2), light, dark: light };
1394
1400
  tokens.push(token);
1395
1401
  byVar.set(name, token);
1402
+ if (theme.utilities.has(name)) themeKeys.set(id, [name.slice(2)]);
1396
1403
  }
1397
- return { prefix: "", tokens, byVar, themeKeys: /* @__PURE__ */ new Map() };
1404
+ return { prefix: "", tokens, byVar, themeKeys };
1398
1405
  }
1399
1406
  async function runRepoCheck(project2, change) {
1400
1407
  const target = await loadInventoryTarget(project2.root);
@@ -1729,7 +1736,7 @@ function renderDiffText(result) {
1729
1736
  lines.push("");
1730
1737
  if (diff.findings.length > 0) lines.push(style.error(`\u2716 ${plural(diff.findings.length, "finding")} in this change: apply each fix (\u2192), then run \`youiyoux check --diff\` again`));
1731
1738
  else if (gaps.length > 0) lines.push(style.warn("! Not a pass: parts of this change could not be checked (above). Fix what could not be read, then run `youiyoux check --diff` again"));
1732
- else if (diff.files.length === 0) lines.push(style.ok(`\u2713 No checked files changed against ${diff.base}: nothing to check. Compare with a branch: youiyoux check --diff main`));
1739
+ else if (diff.files.length === 0) lines.push(style.ok(`\u2713 No checked files changed against ${diff.base}: nothing to check${diff.base === "HEAD" ? ". Compare with a branch: youiyoux check --diff main" : ""}`));
1733
1740
  else lines.push(style.ok(`\u2713 No drift in this change: ${plural(diff.files.length, "file")} checked against ${diff.base}. Nothing to fix`));
1734
1741
  return lines.join("\n");
1735
1742
  }
@@ -1747,9 +1754,10 @@ function diffJson(result) {
1747
1754
  identity: result.identity
1748
1755
  };
1749
1756
  }
1750
- function githubDiffAnnotations(result, prefix) {
1751
- const lines = (result.diff?.findings ?? []).map((f) => command(f.severity === "error" ? "error" : "warning", `${f.message} Fix: ${f.fix}`, { file: `${prefix}${f.file}`, line: f.line, col: f.column, title: `youi ${kindOf(f)}` }));
1752
- for (const gap of diffGaps(result)) lines.push(command("error", `${gap}; a clean result here is not a pass`, { title: "youi incomplete" }));
1757
+ function githubDiffAnnotations(result, prefix, strict) {
1758
+ const level = (severity) => strict && severity === "error" ? "error" : "warning";
1759
+ const lines = (result.diff?.findings ?? []).map((f) => command(level(f.severity), `${f.message} Fix: ${f.fix}`, { file: `${prefix}${f.file}`, line: f.line, col: f.column, title: `youi ${kindOf(f)}` }));
1760
+ for (const gap of diffGaps(result)) lines.push(command(level("error"), `${gap}; a clean result here is not a pass`, { title: "youi incomplete" }));
1753
1761
  return lines;
1754
1762
  }
1755
1763
  function githubDiffSummary(result) {
@@ -3409,7 +3417,7 @@ async function projectStatus(project2, cwd = process.cwd()) {
3409
3417
  const comparison = check.baseline?.comparison;
3410
3418
  const counted = comparison ? [...comparison.new, ...comparison.regressed] : check.violations;
3411
3419
  const usages = check.scan.report.usages;
3412
- const { RequestStore } = await import("./src-VSWKGVFT.js");
3420
+ const { RequestStore } = await import("./src-LP3MFGA4.js");
3413
3421
  const requests = await new RequestStore(project2.root, project2.config.id).list();
3414
3422
  const status = {
3415
3423
  id: project2.config.id,
@@ -4735,17 +4743,23 @@ async function modulesOf(project2, target, failed3) {
4735
4743
  }
4736
4744
  return modules;
4737
4745
  }
4746
+ function groupKey(finding3) {
4747
+ if (finding3.kind === "off-library") return finding3.message;
4748
+ if (finding3.kind !== "hardcoded-value") return finding3.fix;
4749
+ const { word, value, tokens } = hardcodedValue(finding3);
4750
+ return `${word}\0${value}\0${tokens}`;
4751
+ }
4738
4752
  async function grouped(project2, found, lines, describe) {
4739
4753
  const groups = /* @__PURE__ */ new Map();
4740
4754
  for (const finding3 of found) {
4741
- const key2 = `${finding3.kind}\0${finding3.kind === "off-library" ? finding3.message : finding3.fix}`;
4755
+ const key2 = `${finding3.kind}\0${groupKey(finding3)}`;
4742
4756
  groups.set(key2, [...groups.get(key2) ?? [], finding3]);
4743
4757
  }
4744
4758
  const findings2 = [];
4745
4759
  for (const list of groups.values()) {
4746
4760
  const first = list[0];
4747
4761
  const sites = await Promise.all(list.slice(0, SITES_PER_FINDING).map((f) => lines.site(`${repoPath(project2.dir, f.file)}:${f.line}`, repoPath(project2.dir, f.file), f.line)));
4748
- const { title, next: next2 } = describe(first, list.length);
4762
+ const { title, next: next2 } = describe(first, list.length, list);
4749
4763
  findings2.push({ kind: first.kind, project: project2.dir, count: list.length, sites, title, detail: null, next: next2 });
4750
4764
  }
4751
4765
  return findings2.sort((a, b) => b.count - a.count || a.title.localeCompare(b.title));
@@ -4812,9 +4826,14 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
4812
4826
  if (tokens.tokens.length === 0) notes.push("No theme variables (CSS custom properties or Tailwind @theme) were found, so hardcoded values had nothing to be compared with");
4813
4827
  const usage = tokenUsage(modules);
4814
4828
  const found = (await Promise.all(modules.map((module) => reportTokens(module, tokens, usage)))).flat();
4815
- hardcoded = await grouped(project2, found, lines, (first, count) => {
4829
+ hardcoded = await grouped(project2, found, lines, (first, count, list) => {
4816
4830
  const { word, value, tokens: names } = hardcodedValue(first);
4817
- return { title: `${sentence(word)} ${value} is hardcoded in ${plural(count, "place")}; the theme already has it as ${names}`, next: `${sentence(first.fix)} in each place` };
4831
+ const fixes = [...new Map(list.map((f) => [f.fix, f.text]))];
4832
+ const tail = / \([^()]*\)$/.exec(first.fix)?.[0] ?? "";
4833
+ const shared = tail !== "" && fixes.every(([fix]) => fix.endsWith(tail));
4834
+ const each = fixes.slice(0, 3).map(([fix, text], i) => `${(shared ? fix.slice(0, -tail.length) : fix).replace(i > 0 ? /^use / : /^$/, "")} for ${text}`);
4835
+ const next2 = fixes.length === 1 ? `${sentence(first.fix)} in each place` : `${sentence(each.join(", "))}${fixes.length > 3 ? ", \u2026" : ""}${shared ? tail : ""}`;
4836
+ return { title: `${sentence(word)} ${value} is hardcoded in ${plural(count, "place")}; the theme already has it as ${names}`, next: next2 };
4818
4837
  });
4819
4838
  } catch (error) {
4820
4839
  incomplete.push(`Hardcoded values were not checked (${errorText(error)}); run ${NPX} check --diff to see why`);
@@ -5965,7 +5984,7 @@ async function cmdCheckDiff(values, positionals, p, format) {
5965
5984
  const result = configured(p) ? await runCheck(p, { ...checkScope(values, positionals), diff: change }) : await runRepoCheck(p, change);
5966
5985
  if (format === "json") json3(diffJson(result));
5967
5986
  else if (format === "github") {
5968
- for (const line of githubDiffAnnotations(result, await repoPrefix(p.root))) print(line);
5987
+ for (const line of githubDiffAnnotations(result, await repoPrefix(p.root), values.strict)) print(line);
5969
5988
  print(renderDiffText(result));
5970
5989
  if (process.env.GITHUB_STEP_SUMMARY) await appendFile2(process.env.GITHUB_STEP_SUMMARY, githubDiffSummary(result));
5971
5990
  } else print(renderDiffText(result));
@@ -6048,7 +6067,7 @@ async function cmdLibrary(values, positionals) {
6048
6067
  return 0;
6049
6068
  }
6050
6069
  if (action === "ingest" && query && positionals.length === 2) {
6051
- const { fileIngestDraft, readLink, renderIngest } = await import("./ingest-X4MIF5F6.js");
6070
+ const { fileIngestDraft, readLink, renderIngest } = await import("./ingest-4C3TRSBA.js");
6052
6071
  const p = values.draft ? await project(values) : null;
6053
6072
  const reading = await readLink(query, { allowLocal: true });
6054
6073
  const filed = p ? await fileIngestDraft(p, reading, values.state) : null;
@@ -6507,7 +6526,7 @@ async function cmdRequests(values, positionals) {
6507
6526
  const usage = "youiyoux requests [--status open|addressed|closed|all] [--route r] or youiyoux requests show <id> | reply <id> --message <text> | addressed <id> --message <what you changed>";
6508
6527
  const [action, id, ...rest] = positionals;
6509
6528
  if (rest.length > 0) throw new UsageError(usage);
6510
- const { RequestStore, requestBrief } = await import("./src-VSWKGVFT.js");
6529
+ const { RequestStore, requestBrief } = await import("./src-LP3MFGA4.js");
6511
6530
  const { project: project2, root } = await product(values);
6512
6531
  const store = new RequestStore(root, project2?.config.id ?? basename5(root));
6513
6532
  const agent = { kind: "agent", name: "youiyoux CLI" };
@@ -6606,7 +6625,7 @@ async function cmdReport(values, positionals, io) {
6606
6625
  return result.code;
6607
6626
  }
6608
6627
  async function cmdMcp(values) {
6609
- const { startStdioServer } = await import("./src-LGHUKRL2.js");
6628
+ const { startStdioServer } = await import("./src-JTMKM6KB.js");
6610
6629
  await startStdioServer({ projectRoot: values.project ?? process.cwd(), ...loadOptions(values), ...values.cache ? { discoverCache: values.cache } : {} });
6611
6630
  return -1;
6612
6631
  }
@@ -4,7 +4,7 @@ import {
4
4
  loadPackage,
5
5
  readTryIndex,
6
6
  send
7
- } from "./chunk-NG73FB4O.js";
7
+ } from "./chunk-55I4SXOA.js";
8
8
  import {
9
9
  writeFileAtomic
10
10
  } from "./chunk-M56633DV.js";
@@ -1224,13 +1224,16 @@ var Theme = class {
1224
1224
  /** Project declarations only: excludes implicit Tailwind defaults. */
1225
1225
  declared;
1226
1226
  forms;
1227
+ /** Declared variables Tailwind makes utilities from (`--color-muted` → `text-muted`): an `@theme` block's, or the Tailwind config's. */
1228
+ utilities;
1227
1229
  vars;
1228
- constructor(vars, tailwind, warnings, declared = /* @__PURE__ */ new Map(), forms = null) {
1230
+ constructor(vars, tailwind, warnings, declared = /* @__PURE__ */ new Map(), forms = null, utilities = /* @__PURE__ */ new Set()) {
1229
1231
  this.vars = vars;
1230
1232
  this.declared = declared;
1231
1233
  this.tailwind = tailwind;
1232
1234
  this.warnings = warnings;
1233
1235
  this.forms = forms;
1236
+ this.utilities = utilities;
1234
1237
  }
1235
1238
  variable(name) {
1236
1239
  return this.vars.get(name);
@@ -1399,17 +1402,20 @@ async function buildTheme(root, sheets, usesTailwindPackage) {
1399
1402
  const version = v4 ? 4 : v3 || usesTailwindPackage ? 3 : null;
1400
1403
  const vars = version ? defaults(version) : /* @__PURE__ */ new Map();
1401
1404
  const declared = new Map(config.vars);
1405
+ const utilities = new Set(version ? config.vars.keys() : []);
1402
1406
  for (const [name, value] of config.vars) vars.set(name, value);
1403
1407
  for (const sheet of sheets) {
1404
1408
  for (const rule of sheet.rules) {
1405
- if (rule.preludes.length !== 1 || !THEME_SCOPES.has(rule.preludes[0].replace(/\s+/g, " "))) continue;
1409
+ const scope = rule.preludes.length === 1 ? rule.preludes[0].replace(/\s+/g, " ") : null;
1410
+ if (!scope || !THEME_SCOPES.has(scope)) continue;
1406
1411
  for (const declaration of rule.declarations) if (declaration.property.startsWith("--")) {
1407
1412
  vars.set(declaration.property, declaration.value);
1408
1413
  declared.set(declaration.property, declaration.value);
1414
+ if (version === 4 && scope.startsWith("@theme")) utilities.add(declaration.property);
1409
1415
  }
1410
1416
  }
1411
1417
  }
1412
- return new Theme(vars, version, config.warnings, declared, version ? await formsPluginOf(root, sheets, config.file) : null);
1418
+ return new Theme(vars, version, config.warnings, declared, version ? await formsPluginOf(root, sheets, config.file) : null, utilities);
1413
1419
  }
1414
1420
 
1415
1421
  // ../scanner/src/inventory/style.ts
@@ -1793,6 +1799,35 @@ function resolveClasses(classes, tag, theme, rules) {
1793
1799
  // ../scanner/src/drift.ts
1794
1800
  var cssVar = (token, index) => `var(${[...index.byVar].find(([, t]) => t === token)?.[0] ?? cssVariable(index.prefix, token.cssName)})`;
1795
1801
  var tokenList = (tokens) => tokens.slice(0, 3).map((t) => t.id).join(" or ");
1802
+ var viewportOfWord = (word) => /^(phone|mobile|handset)$/.test(word) ? "narrow" : /^(tablet|desktop)$/.test(word) ? "wide" : null;
1803
+ var tokenViewport = (token) => token.id.split(/[.-]/).map(viewportOfWord).find((v) => v !== null) ?? null;
1804
+ function preludeViewports(preludes) {
1805
+ const found = /* @__PURE__ */ new Set();
1806
+ for (const prelude of preludes) {
1807
+ if (/^@media\b/.test(prelude) && /max-width|width\s*<=?/.test(prelude)) found.add("narrow");
1808
+ if (/^@media\b/.test(prelude) && /min-width|width\s*>=?/.test(prelude)) found.add("wide");
1809
+ for (const word of prelude.toLowerCase().match(/[a-z]+/g) ?? []) {
1810
+ const viewport = viewportOfWord(word);
1811
+ if (viewport) found.add(viewport);
1812
+ }
1813
+ }
1814
+ return found;
1815
+ }
1816
+ function variantViewports(variants) {
1817
+ const found = /* @__PURE__ */ new Set();
1818
+ for (const variant of variants) {
1819
+ const viewport = viewportOfWord(variant) ?? (/^max-/.test(variant) ? "narrow" : /^(min-|@)?(sm|md|lg|xl|[2-9]xl)$/.test(variant) ? "wide" : null);
1820
+ if (viewport) found.add(viewport);
1821
+ }
1822
+ return found;
1823
+ }
1824
+ function fitting(tokens, viewports) {
1825
+ const own = tokens.filter((t) => {
1826
+ const v = tokenViewport(t);
1827
+ return v !== null && viewports.has(v);
1828
+ });
1829
+ return [...own, ...tokens.filter((t) => tokenViewport(t) === null)];
1830
+ }
1796
1831
  function utilityBefore(lineText, column) {
1797
1832
  return /(?:^|[\s"'`:])!?(-?[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*)-\[$/.exec(lineText.slice(0, column - 1))?.[1] ?? null;
1798
1833
  }
@@ -1823,11 +1858,12 @@ function arbitraryFindings(module, index, added) {
1823
1858
  const offset = match.index + whole.indexOf(`${utility}-[`);
1824
1859
  const at = offsetToPosition(text, offset, start);
1825
1860
  if (!added(module.file, at.line)) continue;
1826
- const tokens = arbitraryTokens(utility, value, index);
1861
+ const variants = whole.slice(0, whole.indexOf(`${utility}-[`));
1862
+ const tokens = fitting(arbitraryTokens(utility, value, index), variantViewports(variants.split(":")));
1827
1863
  if (tokens.length === 0) continue;
1828
- const utilities = tokenUtilities(utility, tokens, index);
1864
+ const utilities = tokenUtilities(utility, tokens, index).map((u) => `${variants}${u}`);
1829
1865
  const kind = /^-?rounded/.test(utility) ? "radius" : utility === "text" ? "type" : "spacing";
1830
- const fix = utilities.length > 0 ? `use ${utilities.slice(0, 2).join(" or ")} (${tokenList(tokens)}, ${cssVar(tokens[0], index)})` : `use ${utility}-[${cssVar(tokens[0], index)}] (${tokenList(tokens)})`;
1866
+ const fix = utilities.length > 0 ? `use ${utilities.slice(0, 2).join(" or ")} (${tokenList(tokens)}, ${cssVar(tokens[0], index)})` : `use ${variants}${utility}-[${cssVar(tokens[0], index)}] (${tokenList(tokens)})`;
1831
1867
  findings.push({ kind: "hardcoded-value", file: module.file, ...at, text: `${utility}-[${value}]`, message: `Hardcoded ${kind} ${utility}-[${value}]: the token ${tokenList(tokens)} has this value.`, fix });
1832
1868
  }
1833
1869
  }
@@ -1845,9 +1881,10 @@ function cssDimensionFindings(module, index, isTokenSource, added) {
1845
1881
  if (!added(module.file, declaration.line) || isTokenSource && declaration.property.startsWith("--")) continue;
1846
1882
  const kind = CSS_DIMENSIONS.find(([pattern]) => pattern.test(declaration.property))?.[1];
1847
1883
  if (!kind) continue;
1884
+ const viewports = preludeViewports(rule.preludes);
1848
1885
  const matched = [];
1849
1886
  const value = declaration.value.replace(/(^|[\s(,])(-?)(\d*\.?\d+)px\b/g, (whole, lead, sign, n) => {
1850
- const token = dimensionTokens(kind, Number(n), index)[0];
1887
+ const token = fitting(dimensionTokens(kind, Number(n), index), viewports)[0];
1851
1888
  if (!token || Number(n) === 0) return whole;
1852
1889
  matched.push(token);
1853
1890
  return `${lead}${sign ? `calc(-1 * ${cssVar(token, index)})` : cssVar(token, index)}`;
@@ -1900,7 +1937,6 @@ function nearDuplicates(input) {
1900
1937
  const { inventory, added, importOf } = input;
1901
1938
  const definitions = new Map(inventory.definitions.map((d) => [d.id, d]));
1902
1939
  const fresh = inventory.definitions.filter((d) => added(d.file, d.line));
1903
- if (fresh.length === 0) return [];
1904
1940
  const freshIds = new Set(fresh.map((d) => d.id));
1905
1941
  const ends = /* @__PURE__ */ new Map();
1906
1942
  for (const definition of fresh) {
@@ -1921,7 +1957,8 @@ function nearDuplicates(input) {
1921
1957
  if (root?.kind === "primitive" || root?.kind === "definition" && definitions.has(root.id) && !freshIds.has(root.id)) continue;
1922
1958
  const own = inventory.looks.filter((l) => hasShape(l.shape) && inside(l, definition) && l.definition !== definition.id);
1923
1959
  const roots = inventory.looks.filter((l) => hasShape(l.shape) && inside(l, definition) && l.definition === definition.id);
1924
- const looks = roots.length > 0 ? roots : own.length > 0 ? own : inventory.looks.filter((l) => hasShape(l.shape) && l.definition === definition.id);
1960
+ if (roots.length === 0 && own.length > 0) continue;
1961
+ const looks = roots.length > 0 ? roots : inventory.looks.filter((l) => hasShape(l.shape) && l.definition === definition.id);
1925
1962
  let best = null;
1926
1963
  for (const mine of looks) {
1927
1964
  const shape = clusterShapeOf(mine);
@@ -1944,6 +1981,39 @@ function nearDuplicates(input) {
1944
1981
  fix: reuse.fix
1945
1982
  });
1946
1983
  }
1984
+ const reported = fresh.filter((d) => findings.some((f) => f.file === d.file && f.line === d.line));
1985
+ const componentOf = (look) => look.recipe?.kind === "component" ? `${look.recipe.file}#${look.recipe.export}` : null;
1986
+ const uses = /* @__PURE__ */ new Map();
1987
+ for (const look of inventory.looks) if (look.recipe?.kind === "component" && look.file !== look.recipe.file && !look.story) uses.set(componentOf(look), (uses.get(componentOf(look)) ?? 0) + 1);
1988
+ const components = inventory.looks.filter((l) => hasShape(l.shape) && uses.has(componentOf(l) ?? "") && !(l.definition !== null && freshIds.has(l.definition)));
1989
+ const linkRoot = (look) => {
1990
+ const root = definitions.get(look.definition ?? "")?.root;
1991
+ return root?.kind === "intrinsic" ? root.tag === "a" : root?.kind === "primitive" ? /link/i.test(root.name === "default" ? root.module : root.name) : false;
1992
+ };
1993
+ for (const mine of inventory.looks) {
1994
+ if (mine.source !== "intrinsic" || mine.definition !== null || mine.story || !hasShape(mine.shape) || !added(mine.file, mine.line)) continue;
1995
+ if ([mine.shape.height, mine.shape.paddingX || null, mine.shape.radius, mine.shape.fontSize].filter((v) => v !== null).length < 3) continue;
1996
+ if (reported.some((d) => inside(mine, d))) continue;
1997
+ const shape = clusterShapeOf(mine);
1998
+ const mineClasses = classesOf(mine.recipe);
1999
+ let best = null;
2000
+ for (const other of components) {
2001
+ if (other.role !== mine.role || mine.tag === "a" !== linkRoot(other) || !compatible(shape, clusterShapeOf(other))) continue;
2002
+ const rank = [shapeDistance(shape, clusterShapeOf(other)), -uses.get(componentOf(other)), -overlap(mineClasses, classesOf(other.recipe))];
2003
+ if (!best || before(rank, best.rank)) best = { look: other, rank };
2004
+ }
2005
+ if (!best) continue;
2006
+ const reuse = reuseOf(best.look, definitions, importOf, mine.file);
2007
+ findings.push({
2008
+ kind: "near-duplicate",
2009
+ file: mine.file,
2010
+ line: mine.line,
2011
+ column: mine.column,
2012
+ text: `<${mine.tag}>`,
2013
+ message: `A hand-styled <${mine.tag}> that looks like the existing ${reuse.name} (${best.look.file}:${best.look.line}): ${describeShape(mine)}. Confirm it is the same role; if so, render ${reuse.name} instead.`,
2014
+ fix: reuse.fix
2015
+ });
2016
+ }
1947
2017
  return findings.sort(byPosition);
1948
2018
  }
1949
2019
  function importedNames(statement) {
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __youiRequire } from "node:module"; import { dirname as __youiDir } from "node:path"; import { fileURLToPath as __youiPath } from "node:url"; const require = __youiRequire(import.meta.url); const __youiDirname = __youiDir(__youiPath(import.meta.url));
2
2
  import {
3
3
  loadProject
4
- } from "./chunk-75LXBAYB.js";
4
+ } from "./chunk-RQUTV7SO.js";
5
5
  import {
6
6
  shortHash
7
7
  } from "./chunk-M56633DV.js";
@@ -79,11 +79,11 @@ function workflowStateDir(project, state) {
79
79
  return resolve(state ?? join(homedir(), ".youi", "workflow", shortHash(`${project.repo.root}\0${project.root}`)));
80
80
  }
81
81
  async function ingest(link) {
82
- const { readLink } = await import("./ingest-X4MIF5F6.js");
82
+ const { readLink } = await import("./ingest-4C3TRSBA.js");
83
83
  return readLink(link, { allowLocal: false });
84
84
  }
85
85
  async function startRuleEditor(options) {
86
- const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-VSWKGVFT.js");
86
+ const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-LP3MFGA4.js");
87
87
  const projectRoot = resolve(options.project);
88
88
  const load = { ...options.config ? { configPath: resolve(options.config) } : {}, ...options.design ? { designRoot: resolve(options.design) } : {}, ...options.root ? { productRoot: resolve(options.root) } : {} };
89
89
  const project = await loadProject(projectRoot, load);
@@ -4,8 +4,8 @@ import {
4
4
  plural,
5
5
  style,
6
6
  workflowStateDir
7
- } from "./chunk-2F2HM7FE.js";
8
- import "./chunk-75LXBAYB.js";
7
+ } from "./chunk-ZXINIRJ5.js";
8
+ import "./chunk-RQUTV7SO.js";
9
9
  import "./chunk-42YJX3W5.js";
10
10
  import "./chunk-4BQPCZLL.js";
11
11
  import "./chunk-M56633DV.js";
@@ -23,7 +23,7 @@ async function readLink(link, options) {
23
23
  return { link: result.link, source: result.source, license: result.license, fetchedAt: result.fetchedAt, entries: result.entries, skipped: result.skipped, notes: result.notes };
24
24
  }
25
25
  async function fileIngestDraft(project, reading, state) {
26
- const { FileStore, RuleWorkflow } = await import("./src-VSWKGVFT.js");
26
+ const { FileStore, RuleWorkflow } = await import("./src-LP3MFGA4.js");
27
27
  const workflow = new RuleWorkflow({ projectRoot: project.root, store: new FileStore(workflowStateDir(project, state)), ...project.configFile ? { configPath: project.configFile } : {}, ...project.designOverride ? { designRoot: project.repo.root } : {}, ...project.rootOverride ? { productRoot: project.root } : {}, ingest: async () => reading });
28
28
  const draft = await workflow.create({ kind: "person", name: userInfo().username }, { kind: "ingest", link: reading.link });
29
29
  return { id: draft.id, problems: draft.problems };
@@ -188,7 +188,7 @@ import {
188
188
  whyNoLines,
189
189
  withFile,
190
190
  withoutCredentials
191
- } from "./chunk-75LXBAYB.js";
191
+ } from "./chunk-RQUTV7SO.js";
192
192
  import {
193
193
  escapingLinks,
194
194
  relativeInside,
@@ -2,7 +2,7 @@ import { createRequire as __youiRequire } from "node:module"; import { dirname a
2
2
  import {
3
3
  RequestStore,
4
4
  requestBrief
5
- } from "./chunk-OE4RYJOS.js";
5
+ } from "./chunk-ALD2RX3X.js";
6
6
  import {
7
7
  PROJECT_FILE,
8
8
  ScanCache,
@@ -30,7 +30,7 @@ import {
30
30
  shotsText,
31
31
  tokenRows,
32
32
  whereUsed
33
- } from "./chunk-75LXBAYB.js";
33
+ } from "./chunk-RQUTV7SO.js";
34
34
  import "./chunk-42YJX3W5.js";
35
35
  import {
36
36
  SHADCN_SLOTS,
@@ -64,8 +64,8 @@ import {
64
64
  validateIngestContent,
65
65
  validateLibraryContent,
66
66
  validateStyleContent
67
- } from "./chunk-OE4RYJOS.js";
68
- import "./chunk-75LXBAYB.js";
67
+ } from "./chunk-ALD2RX3X.js";
68
+ import "./chunk-RQUTV7SO.js";
69
69
  import "./chunk-42YJX3W5.js";
70
70
  import "./chunk-4BQPCZLL.js";
71
71
  import "./chunk-M56633DV.js";
@@ -17,8 +17,8 @@ import {
17
17
  selectCandidates,
18
18
  thumbsDir,
19
19
  writeThumbIndex
20
- } from "./chunks/chunk-AFEQZT2N.js";
21
- import "./chunks/chunk-NG73FB4O.js";
20
+ } from "./chunks/chunk-O77MKELR.js";
21
+ import "./chunks/chunk-55I4SXOA.js";
22
22
  import "./chunks/chunk-FVVL5JCY.js";
23
23
  import "./chunks/chunk-M56633DV.js";
24
24
  import "./chunks/chunk-WNTS7ZO4.js";
@@ -2,7 +2,7 @@
2
2
  import { createRequire as __youiRequire } from "node:module"; import { dirname as __youiDir } from "node:path"; import { fileURLToPath as __youiPath } from "node:url"; const require = __youiRequire(import.meta.url); const __youiDirname = __youiDir(__youiPath(import.meta.url));
3
3
  import {
4
4
  discoverThumbsMiddleware
5
- } from "./chunks/chunk-AFEQZT2N.js";
5
+ } from "./chunks/chunk-O77MKELR.js";
6
6
  import {
7
7
  isLocalHost,
8
8
  loadPackage,
@@ -10,7 +10,7 @@ import {
10
10
  readTryIndex,
11
11
  send,
12
12
  tryMiddleware
13
- } from "./chunks/chunk-NG73FB4O.js";
13
+ } from "./chunks/chunk-55I4SXOA.js";
14
14
  import "./chunks/chunk-FVVL5JCY.js";
15
15
  import "./chunks/chunk-M56633DV.js";
16
16
  import "./chunks/chunk-WNTS7ZO4.js";
@@ -3,7 +3,7 @@ import { createRequire as __youiRequire } from "node:module"; import { dirname a
3
3
  import {
4
4
  loadPackage,
5
5
  tryProject
6
- } from "./chunks/chunk-NG73FB4O.js";
6
+ } from "./chunks/chunk-55I4SXOA.js";
7
7
  import "./chunks/chunk-FVVL5JCY.js";
8
8
  import "./chunks/chunk-WNTS7ZO4.js";
9
9
 
package/dist/youi.js CHANGED
@@ -2,9 +2,9 @@
2
2
  import { createRequire as __youiRequire } from "node:module"; import { dirname as __youiDir } from "node:path"; import { fileURLToPath as __youiPath } from "node:url"; const require = __youiRequire(import.meta.url); const __youiDirname = __youiDir(__youiPath(import.meta.url));
3
3
  import {
4
4
  main
5
- } from "./chunks/chunk-DHZDMZXE.js";
6
- import "./chunks/chunk-2F2HM7FE.js";
7
- import "./chunks/chunk-75LXBAYB.js";
5
+ } from "./chunks/chunk-FYG6USCG.js";
6
+ import "./chunks/chunk-ZXINIRJ5.js";
7
+ import "./chunks/chunk-RQUTV7SO.js";
8
8
  import "./chunks/chunk-42YJX3W5.js";
9
9
  import "./chunks/chunk-5D7PLY7A.js";
10
10
  import "./chunks/chunk-UJMXSTUF.js";
package/dist/youiyoux.js CHANGED
@@ -3,9 +3,9 @@ import { createRequire as __youiRequire } from "node:module"; import { dirname a
3
3
  import {
4
4
  COMMAND_NAMES,
5
5
  main
6
- } from "./chunks/chunk-DHZDMZXE.js";
7
- import "./chunks/chunk-2F2HM7FE.js";
8
- import "./chunks/chunk-75LXBAYB.js";
6
+ } from "./chunks/chunk-FYG6USCG.js";
7
+ import "./chunks/chunk-ZXINIRJ5.js";
8
+ import "./chunks/chunk-RQUTV7SO.js";
9
9
  import "./chunks/chunk-42YJX3W5.js";
10
10
  import "./chunks/chunk-5D7PLY7A.js";
11
11
  import "./chunks/chunk-UJMXSTUF.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "youiyoux",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Your agent rebuilds components you already have. youiyoux catches it: npx youiyoux turns any repo into a shareable report of near-duplicate components, hardcoded token values and off-library imports, with the fixes.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,9 +46,9 @@
46
46
  "typescript": "6.0.3"
47
47
  },
48
48
  "devDependencies": {
49
- "@youiyoux/cli": "0.1.19",
50
- "@youiyoux/core": "0.1.19",
51
- "@youiyoux/scanner": "0.1.19"
49
+ "@youiyoux/cli": "0.1.21",
50
+ "@youiyoux/scanner": "0.1.21",
51
+ "@youiyoux/core": "0.1.21"
52
52
  },
53
53
  "exports": {
54
54
  "./package.json": "./package.json"