-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathkit.mjs
More file actions
131 lines (122 loc) · 5.11 KB
/
Copy pathkit.mjs
File metadata and controls
131 lines (122 loc) · 5.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// Copyright (c) Meta Platforms, Inc. and affiliates.
/**
* @file build.kit leaf — the grouped composition kit for an idea.
*
* Runs the unified search for the query and groups the results into a
* composition KIT: the closest page templates, the blocks that cover parts,
* and the domain components to fill gaps, plus the always-on frame + foundation.
*
* The kit carries RAW `SearchResultEntry` objects and static name arrays only —
* never pre-formatted command strings. All CLI prefixing (formatCliCommand /
* getCliInvocation) and the section prose live in the command renderer, so the
* JSON shape stays package-manager-agnostic and stable across environments.
*/
import {search} from '../../search/search.mjs';
/** A page at/above this score is a confident direct match. */
const PAGE_DIRECT = 95;
/** Below this a page is too weak to offer even as a layout reference. */
const PAGE_FLOOR = 50;
/** Below this a block/domain-component match is incidental noise. */
const DOMAIN_FLOOR = 55;
/**
* How much of a multi-word query a result must cover to be offered as a PAGE.
*
* Score alone cannot carry this. A page's keywords include every component its
* source renders, so `build "actionable warning banner"` scored `login`,
* `contact-form` and `documentation-design` at 95 apiece — an exact keyword hit
* (90) on "banner" alone, plus the coverage garnish, lands exactly on
* PAGE_DIRECT. Three pages that are not warnings, presented as a direct match,
* because each happens to render a Banner somewhere.
*
* Coverage has to gate rather than garnish: matching one of three concepts is
* not the same claim as matching three.
*/
const PAGE_COVERAGE = 0.5;
/** Fewer results than this and the kit says how to look further. */
const THIN_KIT = 3;
/**
* Always-surfaced primitives. Every page needs a shell + layout/typography/
* action atoms, but these never keyword-match an idea ("dashboard" != "Stack"),
* so search alone never returns them. Kept here (not the renderer) because they
* are ALSO used to exclude these names from the idea-specific `domain` group.
*/
const FRAME = ['AppShell', 'TopNav', 'SideNav', 'Layout'];
const FOUNDATION = [
'VStack', 'HStack', 'Grid', 'StackItem', 'Card', 'Section',
'Text', 'Heading', 'Button', 'Icon', 'Badge', 'Divider',
];
const ALWAYS = new Set([...FRAME, ...FOUNDATION]);
/**
* The grouped composition kit for what you're building.
*
* @param {string} query what you're building (e.g. "analytics dashboard")
* @param {{cwd?: string, type?: import('../../search/search.type.mjs').SearchDomain, limit?: number}} [options]
* @returns {Promise<import('../build.type.mjs').BuildKitResponse>}
*/
export async function buildKit(query, options = {}) {
const {cwd = process.cwd(), type, limit = 60} = options;
// search()'s JSDoc @returns widens results to object[]; the SearchResponse
// shape is the contract (api/search/search.type.mjs). Cast locally rather than
// tightening the search @returns (a separate follow-up).
const result = /** @type {import('../../search/search.type.mjs').SearchResponse} */ (
await search(query, {cwd, type, limit})
);
const results = result.data.results;
/**
* Did this result answer enough of the query to stand as a page?
* Single-concept queries have nothing to cover, so they always pass.
* @param {{matchedTerms?: number, queryTerms?: number}} r
*/
const covers = r => {
const total = r.queryTerms ?? 1;
if (total <= 1) return true;
return (r.matchedTerms ?? 0) / total >= PAGE_COVERAGE;
};
const pages = results
.filter(
r =>
r.domain === 'template' &&
r.kind !== 'block' &&
r.score >= PAGE_FLOOR &&
covers(r),
)
.slice(0, 3);
const blocks = results
.filter(r => r.domain === 'template' && r.kind === 'block' && r.score >= DOMAIN_FLOOR)
.slice(0, 5);
const domain = results
.filter(
r =>
(r.domain === 'component' || r.domain === 'hook') &&
r.score >= DOMAIN_FLOOR &&
!ALWAYS.has(r.name),
)
.slice(0, 6);
const directMatch = pages.length > 0 && pages[0].score >= PAGE_DIRECT;
// What to try when the kit comes back thin. Keyword search over a design
// system misses in a predictable way — the reader's words and the package's
// often do not overlap — and an agent reading an empty kit concludes the
// package has nothing and falls back on its own memory of it, which is the
// failure this command exists to prevent. Say so, and name the way to browse.
const hint =
pages.length + blocks.length + domain.length < THIN_KIT
? 'Few matches. This is keyword search, not semantic — try other wordings, ' +
'or browse with `astryx component --list` and `astryx template --list`.'
: undefined;
return {
type: 'build.kit',
data: {
query: result.data.query,
// Distinguishes "search found nothing" (renderer shows "No matches")
// from a weak-but-non-empty result set (renderer still shows the kit).
hasResults: results.length > 0,
directMatch,
pages,
blocks,
domain,
frame: FRAME,
foundation: FOUNDATION,
hint,
},
};
}