@md-code/react-component-uniqueness 0.1.0

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/resolve.js ADDED
@@ -0,0 +1,374 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * resolve.js
5
+ *
6
+ * String-literal resolution machinery shared by the rule (ESTree AST) and the
7
+ * catalog scanner (TypeScript AST).
8
+ *
9
+ * - ESTree (rule): stringLiteralsOf, resolveExportedFromFile,
10
+ * resolveStringCandidates — resolve className/style expressions to
11
+ * candidate string literals, following local constants, cn()/clsx()
12
+ * calls, ternaries, and imported helpers (read from disk).
13
+ * - TS AST (scanner): tsStringLiteralsOf — the same job over a TypeScript
14
+ * AST node (used by the catalog generator).
15
+ */
16
+
17
+ const fs = require("node:fs");
18
+ const path = require("node:path");
19
+ const ts = require("typescript");
20
+
21
+ /**
22
+ * Collect string literals from an ESTree expression (shallow, capped).
23
+ */
24
+ function stringLiteralsOf(expr, out, depth) {
25
+ if (!expr || depth > 4 || out.length >= 24) return;
26
+ switch (expr.type) {
27
+ case "Literal":
28
+ if (typeof expr.value === "string") out.push(expr.value);
29
+ break;
30
+ case "TemplateLiteral":
31
+ for (const q of expr.quasis) {
32
+ if (q.value && q.value.cooked) out.push(q.value.cooked);
33
+ }
34
+ break;
35
+ case "ObjectExpression":
36
+ for (const p of expr.properties) {
37
+ if (p.type === "Property") stringLiteralsOf(p.value, out, depth + 1);
38
+ }
39
+ break;
40
+ case "ArrayExpression":
41
+ for (const el of expr.elements) {
42
+ if (el) stringLiteralsOf(el, out, depth + 1);
43
+ }
44
+ break;
45
+ case "CallExpression":
46
+ for (const arg of expr.arguments) stringLiteralsOf(arg, out, depth + 1);
47
+ break;
48
+ case "ConditionalExpression":
49
+ stringLiteralsOf(expr.consequent, out, depth + 1);
50
+ stringLiteralsOf(expr.alternate, out, depth + 1);
51
+ break;
52
+ case "LogicalExpression":
53
+ stringLiteralsOf(expr.left, out, depth + 1);
54
+ stringLiteralsOf(expr.right, out, depth + 1);
55
+ break;
56
+ case "BinaryExpression":
57
+ if (expr.operator === "+") {
58
+ stringLiteralsOf(expr.left, out, depth + 1);
59
+ stringLiteralsOf(expr.right, out, depth + 1);
60
+ }
61
+ break;
62
+ default:
63
+ break;
64
+ }
65
+ }
66
+
67
+ /** Collect string literals from a TypeScript AST node (shallow, capped). */
68
+ function tsStringLiteralsOf(node, out, depth, root) {
69
+ if (!node || depth > 4 || out.length >= 24) return;
70
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
71
+ out.push(node.text);
72
+ return;
73
+ }
74
+ if (ts.isTemplateExpression(node)) {
75
+ for (const s of node.head && node.head.text ? [node.head.text] : []) out.push(s);
76
+ for (const span of node.templateSpans) {
77
+ if (span.literal && span.literal.text) out.push(span.literal.text);
78
+ tsStringLiteralsOf(span.expression, out, depth + 1, root);
79
+ }
80
+ return;
81
+ }
82
+ if (ts.isObjectLiteralExpression(node)) {
83
+ for (const p of node.properties) tsStringLiteralsOf(p.initializer, out, depth + 1, root);
84
+ return;
85
+ }
86
+ if (ts.isArrayLiteralExpression(node)) {
87
+ for (const el of node.elements) tsStringLiteralsOf(el, out, depth + 1, root);
88
+ return;
89
+ }
90
+ if (ts.isCallExpression(node)) {
91
+ for (const arg of node.arguments) tsStringLiteralsOf(arg, out, depth + 1, root);
92
+ return;
93
+ }
94
+ if (ts.isConditionalExpression(node)) {
95
+ tsStringLiteralsOf(node.whenTrue, out, depth + 1, root);
96
+ tsStringLiteralsOf(node.whenFalse, out, depth + 1, root);
97
+ return;
98
+ }
99
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.Plus) {
100
+ tsStringLiteralsOf(node.left, out, depth + 1, root);
101
+ tsStringLiteralsOf(node.right, out, depth + 1, root);
102
+ return;
103
+ }
104
+ if (ts.isBlock(node)) {
105
+ ts.forEachChild(node, (n) => tsStringLiteralsOf(n, out, depth + 1, root));
106
+ return;
107
+ }
108
+ if (ts.isReturnStatement(node) && node.expression) {
109
+ tsStringLiteralsOf(node.expression, out, depth + 1, root);
110
+ return;
111
+ }
112
+ if (ts.isIdentifier(node) && root) {
113
+ // A bare identifier in the initializer (e.g. const cls = baseCls) —
114
+ // resolve it within the same file.
115
+ const findDecl = (n) => {
116
+ if (out.length >= 24) return;
117
+ if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.name.text === node.text && n.initializer) {
118
+ tsStringLiteralsOf(n.initializer, out, depth + 1, root);
119
+ return;
120
+ }
121
+ ts.forEachChild(n, findDecl);
122
+ };
123
+ findDecl(root);
124
+ }
125
+ }
126
+
127
+ // Cache of imported-value lookups: absPath -> { name: result }.
128
+ const importedCache = new Map();
129
+
130
+ /**
131
+ * Read a sibling file from disk and resolve one of its exported constants to
132
+ * string literals. Handles: const X = "...", template literals, cn()/clsx()
133
+ * calls, object literals, ternaries, and re-exports (export { Y as X }).
134
+ * Cached per file. Returns null when the file cannot be read or the export is
135
+ * not a static string.
136
+ */
137
+ function resolveExportedFromFile(fileAbs, exportName) {
138
+ let byFile = importedCache.get(fileAbs);
139
+ if (!byFile) {
140
+ byFile = {};
141
+ importedCache.set(fileAbs, byFile);
142
+ }
143
+ if (Object.prototype.hasOwnProperty.call(byFile, exportName)) return byFile[exportName];
144
+ let result = null;
145
+ try {
146
+ const source = fs.readFileSync(fileAbs, "utf8");
147
+ const sf = ts.createSourceFile(fileAbs, source, ts.ScriptTarget.ES2020, true, fileAbs.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
148
+ const visit = (node) => {
149
+ if (result) return;
150
+ // export function NAME(...) { ... }
151
+ if (ts.isFunctionDeclaration(node) && node.name && node.name.text === exportName && ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) {
152
+ const out = [];
153
+ if (node.body) tsStringLiteralsOf(node.body, out, 0, sf);
154
+ result = out.length > 0 ? out : null;
155
+ return;
156
+ }
157
+ // export const NAME = ... / export let / export var
158
+ if (ts.isVariableStatement(node) && ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) {
159
+ for (const d of node.declarationList.declarations) {
160
+ if (ts.isIdentifier(d.name) && d.name.text === exportName && d.initializer) {
161
+ const out = [];
162
+ tsStringLiteralsOf(d.initializer, out, 0, sf);
163
+ // Arrow/function initializer: also collect the body
164
+ // (const rowCls = (w) => w ? "a" : "b").
165
+ if (out.length === 0 && (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))) {
166
+ if (d.initializer.body) tsStringLiteralsOf(d.initializer.body, out, 0, sf);
167
+ }
168
+ result = out.length > 0 ? out : null;
169
+ return;
170
+ }
171
+ }
172
+ }
173
+ // export { Y as NAME } / export { Y as NAME } from "./other"
174
+ if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
175
+ for (const el of node.exportClause.elements) {
176
+ const exported = el.name && el.name.text;
177
+ if (exported === exportName) {
178
+ if (node.moduleSpecifier) {
179
+ // Re-export from another file — resolve there.
180
+ const localName = el.propertyName ? el.propertyName.text : exportName;
181
+ const target = path.resolve(path.dirname(fileAbs), node.moduleSpecifier.text);
182
+ const exts = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
183
+ for (const ext of exts) {
184
+ const candidate = target + ext;
185
+ try {
186
+ if (fs.statSync(candidate).isFile()) {
187
+ result = resolveExportedFromFile(candidate, localName);
188
+ return;
189
+ }
190
+ } catch {
191
+ // try next extension
192
+ }
193
+ }
194
+ } else {
195
+ // Local re-export: find the local declaration.
196
+ const localName = el.propertyName ? el.propertyName.text : exportName;
197
+ const findLocal = (n) => {
198
+ if (result) return;
199
+ if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.name.text === localName && n.initializer) {
200
+ const out = [];
201
+ tsStringLiteralsOf(n.initializer, out, 0, sf);
202
+ result = out.length > 0 ? out : null;
203
+ }
204
+ ts.forEachChild(n, findLocal);
205
+ };
206
+ findLocal(sf);
207
+ return;
208
+ }
209
+ }
210
+ }
211
+ }
212
+ ts.forEachChild(node, visit);
213
+ };
214
+ visit(sf);
215
+ } catch {
216
+ result = null;
217
+ }
218
+ byFile[exportName] = result;
219
+ return result;
220
+ }
221
+
222
+ /**
223
+ * Resolve an expression to candidate string values (dynamic values: local
224
+ * variables, ternaries, cn()/clsx() calls, template literals, imported
225
+ * constants). Returns null when the expression is not a statically known
226
+ * string.
227
+ */
228
+ function resolveStringCandidates(expr, scope, depth, fileDir) {
229
+ if (!expr || depth > 3) return null;
230
+ switch (expr.type) {
231
+ case "Literal":
232
+ // Strings and numbers: inline style values are often numeric
233
+ // (padding: 16, borderRadius: 8). The catalog scanner
234
+ // (scanner/extract.js) records numeric literals the same way, so
235
+ // the runtime candidate must too — otherwise a numeric-styled
236
+ // element loses those style keys and falls below the shared-key
237
+ // threshold.
238
+ return typeof expr.value === "string" || typeof expr.value === "number" ? [String(expr.value)] : null;
239
+ case "TemplateLiteral": {
240
+ if (expr.expressions.length === 0) {
241
+ const cooked = expr.quasis.map((q) => q.value.cooked).join("");
242
+ return [cooked];
243
+ }
244
+ const out = [];
245
+ for (const q of expr.quasis) {
246
+ if (q.value.cooked) out.push(q.value.cooked);
247
+ }
248
+ for (const e of expr.expressions) {
249
+ const c = resolveStringCandidates(e, scope, depth + 1, fileDir);
250
+ if (c) out.push(...c);
251
+ }
252
+ return out.length ? out : null;
253
+ }
254
+ case "ConditionalExpression": {
255
+ const a = resolveStringCandidates(expr.consequent, scope, depth + 1, fileDir);
256
+ const b = resolveStringCandidates(expr.alternate, scope, depth + 1, fileDir);
257
+ const out = [];
258
+ if (a) out.push(...a);
259
+ if (b) out.push(...b);
260
+ return out.length ? out : null;
261
+ }
262
+ case "LogicalExpression": {
263
+ const a = resolveStringCandidates(expr.left, scope, depth + 1, fileDir);
264
+ const b = resolveStringCandidates(expr.right, scope, depth + 1, fileDir);
265
+ const out = [];
266
+ if (a) out.push(...a);
267
+ if (b) out.push(...b);
268
+ return out.length ? out : null;
269
+ }
270
+ case "BinaryExpression": {
271
+ if (expr.operator !== "+") return null;
272
+ const a = resolveStringCandidates(expr.left, scope, depth + 1, fileDir);
273
+ const b = resolveStringCandidates(expr.right, scope, depth + 1, fileDir);
274
+ if (a && b) return [...a, ...b];
275
+ return a || b;
276
+ }
277
+ case "CallExpression": {
278
+ const callee = expr.callee;
279
+ const isJoiner =
280
+ (callee.type === "Identifier" && /^(cn|clsx|cx|classNames|classnames|joinClasses|mergeClasses)$/.test(callee.name)) ||
281
+ (callee.type === "MemberExpression" && callee.object && callee.object.name === "classNames");
282
+ if (isJoiner) {
283
+ const out = [];
284
+ for (const arg of expr.arguments) {
285
+ const c = resolveStringCandidates(arg, scope, depth + 1, fileDir);
286
+ if (c) out.push(...c);
287
+ }
288
+ return out.length ? out : null;
289
+ }
290
+ // Call of an imported function (e.g. rowCls(wide)) — resolve the
291
+ // exported function body in the sibling file.
292
+ if (callee.type === "Identifier" && fileDir) {
293
+ let imp = null;
294
+ for (let s = scope; s; s = s.upper) {
295
+ imp = (s.variables || []).find((v) => v.name === callee.name && v.defs && v.defs.some((d) => d.type === "ImportBinding"));
296
+ if (imp) break;
297
+ }
298
+ if (imp) {
299
+ const binding = (imp.defs.find((d) => d.type === "ImportBinding") || imp.defs[0] || {}).node;
300
+ const source = binding.parent && binding.parent.source && binding.parent.source.value;
301
+ if (source && source.startsWith(".")) {
302
+ const target = path.resolve(fileDir, source);
303
+ const exts = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
304
+ for (const ext of exts) {
305
+ const candidate = target + ext;
306
+ try {
307
+ if (fs.statSync(candidate).isFile()) {
308
+ const c = resolveExportedFromFile(candidate, imp.name);
309
+ if (c) return c;
310
+ break;
311
+ }
312
+ } catch {
313
+ // try next extension
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+ return null;
320
+ }
321
+ case "Identifier": {
322
+ // Local declaration in an enclosing scope (const cls = ...).
323
+ for (let s = scope; s; s = s.upper) {
324
+ const v = s.variables.find((x) => x.name === expr.name);
325
+ if (!v) continue;
326
+ for (const def of v.defs || []) {
327
+ const node = def && def.node;
328
+ if (node && node.type === "VariableDeclarator" && node.init) {
329
+ const c = resolveStringCandidates(node.init, scope, depth + 1, fileDir);
330
+ if (c) return c;
331
+ }
332
+ }
333
+ }
334
+ // Imported from a sibling file — read the file, find the exported
335
+ // constant, collect its string literals. The import binding lives
336
+ // in the module scope, so walk up.
337
+ let imp = null;
338
+ for (let s = scope; s; s = s.upper) {
339
+ imp = (s.variables || []).find((v) => v.name === expr.name && v.defs && v.defs.some((d) => d.type === "ImportBinding"));
340
+ if (imp) break;
341
+ }
342
+ if (imp && fileDir) {
343
+ const binding = (imp.defs.find((d) => d.type === "ImportBinding") || imp.defs[0] || {}).node;
344
+ const source = binding.parent && binding.parent.source && binding.parent.source.value;
345
+ if (source && source.startsWith(".")) {
346
+ const target = path.resolve(fileDir, source);
347
+ const exts = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
348
+ for (const ext of exts) {
349
+ const candidate = target + ext;
350
+ try {
351
+ if (fs.statSync(candidate).isFile()) {
352
+ const c = resolveExportedFromFile(candidate, imp.name);
353
+ if (c) return c;
354
+ break;
355
+ }
356
+ } catch {
357
+ // try next extension
358
+ }
359
+ }
360
+ }
361
+ }
362
+ return null;
363
+ }
364
+ default:
365
+ return null;
366
+ }
367
+ }
368
+
369
+ module.exports = {
370
+ stringLiteralsOf,
371
+ tsStringLiteralsOf,
372
+ resolveExportedFromFile,
373
+ resolveStringCandidates,
374
+ };
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * scanner/attribute.js
5
+ *
6
+ * Component attribution: map a source file to the canonical component it
7
+ * belongs to.
8
+ *
9
+ * The component is the nearest PascalCase directory at or above the file's
10
+ * directory (e.g. packages/components/default/forms/Button/index.tsx ->
11
+ * Button). When the component registry is available, the registry's public
12
+ * name for that directory wins (the registry is the source of truth for
13
+ * public names).
14
+ */
15
+
16
+ const fs = require("node:fs");
17
+ const path = require("node:path");
18
+
19
+ /** Build the dir -> public component name map from the registry (if present). */
20
+ function buildDirNameMap(repoRoot, registryPath) {
21
+ const map = new Map();
22
+ const regPath = registryPath || path.join(repoRoot, "reports", "component-registry.json");
23
+ if (!fs.existsSync(regPath)) return map;
24
+ try {
25
+ const reg = JSON.parse(fs.readFileSync(regPath, "utf8"));
26
+ for (const [name, dirs] of Object.entries((reg && reg.names) || {})) {
27
+ for (const d of dirs) {
28
+ if (!map.has(d)) map.set(d, name);
29
+ }
30
+ }
31
+ } catch {
32
+ // registry unreadable — fall back to directory basenames
33
+ }
34
+ return map;
35
+ }
36
+
37
+ /**
38
+ * Attribute a file to a component name.
39
+ *
40
+ * @param {string} fileAbs absolute path to the source file
41
+ * @param {string} repoRoot absolute repo root
42
+ * @param {Map<string,string>} dirNameMap registry dir -> public name
43
+ * @returns {string} component name
44
+ */
45
+ function componentNameFor(fileAbs, repoRoot, dirNameMap) {
46
+ let dir = path.dirname(fileAbs);
47
+ // Fallback: the file's own basename (e.g. useCampaignRun.tsx -> useCampaignRun).
48
+ // The old fallback was the directory basename, which produced garbage names
49
+ // like "hooks" or "components" for files sitting in generic folders.
50
+ let name = path.basename(fileAbs).replace(/\.[^.]+$/, "");
51
+ // index.tsx / styles.tsx inside a PascalCase dir belong to that dir's component
52
+ // (e.g. Pagination/index.tsx -> Pagination, Button/styles.ts -> Button).
53
+ const isFileScoped = /^(index|styles|types|constants|utils|hooks|test|stories)$/i.test(name);
54
+ // The file's OWN dir is the most likely component dir (Pagination/index.tsx).
55
+ let up = dir;
56
+ if (/^[A-Z][A-Za-z0-9]*$/.test(path.basename(dir)) && isFileScoped) {
57
+ name = path.basename(dir);
58
+ } else {
59
+ for (let i = 0; i < 4; i += 1) {
60
+ const parent = path.dirname(up);
61
+ if (parent === up) break;
62
+ const bn = path.basename(parent);
63
+ if (/^[A-Z][A-Za-z0-9]*$/.test(bn)) {
64
+ up = parent;
65
+ if (isFileScoped) name = bn; // index/styles/etc. -> dir component name
66
+ break;
67
+ }
68
+ up = parent;
69
+ }
70
+ }
71
+ // Prefer the registry's public name for this directory.
72
+ const relDir = path.relative(repoRoot, dir).split(path.sep).join("/");
73
+ const relUp = path.relative(repoRoot, up).split(path.sep).join("/");
74
+ const regName = dirNameMap.get(relDir) || dirNameMap.get(relUp);
75
+ if (regName) name = regName;
76
+ return name;
77
+ }
78
+
79
+ module.exports = { buildDirNameMap, componentNameFor };
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+
3
+ const { computeGenericTokens, distinctiveTokens } = require("./matching");
4
+ const { resolveThresholds } = require("../thresholds");
5
+
6
+ const JACCARD_MIN = 0.3;
7
+ const FAMILY_MATCH_MIN = resolveThresholds({}).familyMatchMin;
8
+ const MIN_FAMILY_TOKENS = 3;
9
+
10
+ const componentTokenSet = (c, generic) => {
11
+ const s = new Set();
12
+ for (const e of c.elementProps || []) for (const t of e.tokens || []) s.add(t);
13
+ return distinctiveTokens([...s], generic);
14
+ };
15
+
16
+ const jaccard = (a, b) => {
17
+ if (a.size === 0 || b.size === 0) return 0;
18
+ let inter = 0;
19
+ for (const t of a) if (b.has(t)) inter += 1;
20
+ const union = a.size + b.size - inter;
21
+ return union === 0 ? 0 : inter / union;
22
+ };
23
+
24
+ function clusterComponents(canonComponents) {
25
+ const generic = computeGenericTokens(canonComponents);
26
+ const sets = canonComponents.map((c) => componentTokenSet(c, generic));
27
+ const clusters = [];
28
+ for (let i = 0; i < canonComponents.length; i += 1) {
29
+ const s = sets[i];
30
+ if (s.size < MIN_FAMILY_TOKENS) continue;
31
+ let best = null;
32
+ for (const cl of clusters) {
33
+ const j = jaccard(s, cl.tokenSet);
34
+ if (j >= JACCARD_MIN && (!best || j > best.j)) best = { cl, j };
35
+ }
36
+ if (best) best.cl.members.push(i);
37
+ else clusters.push({ members: [i], tokenSet: new Set(s) });
38
+ }
39
+ const families = [];
40
+ for (const cl of clusters) {
41
+ if (cl.members.length < 2) continue;
42
+ const count = new Map();
43
+ for (const i of cl.members) for (const t of sets[i]) count.set(t, (count.get(t) || 0) + 1);
44
+ const half = cl.members.length / 2;
45
+ const tokenSet = new Set();
46
+ for (const [t, n] of count) if (n >= half) tokenSet.add(t);
47
+ if (tokenSet.size < MIN_FAMILY_TOKENS) continue;
48
+ families.push({ members: cl.members, tokenSet, sets });
49
+ }
50
+ return { families, sets, generic };
51
+ }
52
+
53
+ function nearestMember(family, appTokens) {
54
+ let best = null;
55
+ for (const i of family.members) {
56
+ const j = jaccard(appTokens, family.sets[i]);
57
+ if (!best || j > best.j) best = { idx: i, j };
58
+ }
59
+ return best;
60
+ }
61
+
62
+ function matchFamilies(appComponents, canonComponents, cluster, opts) {
63
+ const { families, sets, generic } = cluster;
64
+ const th = resolveThresholds(opts);
65
+ const matches = [];
66
+ const seen = new Set();
67
+ for (const app of appComponents) {
68
+ const appTokens = componentTokenSet(app, generic);
69
+ if (appTokens.size < MIN_FAMILY_TOKENS) continue;
70
+ let best = null;
71
+ for (const family of families) {
72
+ const j = jaccard(appTokens, family.tokenSet);
73
+ if (j < th.familyMatchMin) continue;
74
+ const near = nearestMember(family, appTokens);
75
+ if (!near || near.j < th.familyMatchMin) continue;
76
+ const member = canonComponents[near.idx];
77
+ if ((app.usesCanonical || []).includes(member.name)) continue;
78
+ if ((app.elementProps || []).length > 3 * (member.elementProps || []).length) continue;
79
+ if (!best || near.j > best.j) best = { canon: member, j: near.j, familyJ: j };
80
+ }
81
+ if (best) {
82
+ const key = `${app.path}:${app.line}->${best.canon.path}:${best.canon.name}`;
83
+ if (seen.has(key)) continue;
84
+ seen.add(key);
85
+ matches.push({
86
+ app,
87
+ canon: best.canon,
88
+ tier: "family",
89
+ reason: `token profile matches the ${best.canon.name} family (${(best.j * 100).toFixed(0)}% overlap)`,
90
+ });
91
+ }
92
+ }
93
+ return matches;
94
+ }
95
+
96
+ module.exports = { clusterComponents, matchFamilies, componentTokenSet, jaccard, JACCARD_MIN, FAMILY_MATCH_MIN, MIN_FAMILY_TOKENS };