@astryxdesign/cli 0.7.2 → 0.7.3-canary.06872b4
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 +99 -66
- package/api/build/_adapter.d.mts +58 -0
- package/api/build/_adapter.mjs +115 -3
- package/api/build/blocks/blocks.d.mts +42 -0
- package/api/build/blocks/blocks.mjs +169 -0
- package/api/build/blocks/blocks.test.mjs +108 -0
- package/api/build/blocks/blocks.weights.json +1 -0
- package/api/build/build.mjs +25 -6
- package/api/build/kit/answer.d.mts +14 -0
- package/api/build/kit/answer.mjs +85 -0
- package/api/build/kit/answer.test.mjs +38 -0
- package/api/build/kit/kind.d.mts +25 -0
- package/api/build/kit/kind.mjs +170 -0
- package/api/build/kit/kit.d.mts +2 -4
- package/api/build/kit/kit.mjs +8 -398
- package/api/build/kit/rank.d.mts +3 -32
- package/api/build/kit/rank.mjs +25 -342
- package/api/build/kit/rank.test.mjs +2 -1
- package/api/build/kit/signals.d.mts +60 -0
- package/api/build/kit/signals.mjs +284 -0
- package/api/build/kit/start.d.mts +32 -0
- package/api/build/kit/start.mjs +163 -0
- package/api/build/kit/words.d.mts +72 -0
- package/api/build/kit/words.mjs +202 -0
- package/api/build/tree.d.mts +20 -0
- package/api/build/tree.mjs +52 -0
- package/api/build/tree.test.mjs +177 -0
- package/api/build/tree.weights.json +1 -0
- package/api/component/component.mjs +16 -2
- package/api/component/component.test.mjs +15 -0
- package/api/discover/_adapter.mjs +36 -9
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/docs/_adapter.mjs +17 -5
- package/api/docs/docs.test.mjs +7 -1
- package/api/doctor/doctor.mjs +86 -12
- package/api/doctor/doctor.test.mjs +28 -3
- package/api/init/init.doc.mjs +20 -6
- package/api/init/init.test.mjs +18 -2
- package/api/init/init.type.d.mts +3 -3
- package/api/init/init.type.mjs +3 -3
- package/api/init/remove/remove.mjs +17 -4
- package/api/init/remove/remove.test.mjs +114 -2
- package/api/init/run/run.mjs +69 -6
- package/api/integration/authoring-checks.mjs +83 -9
- package/api/integration/pack-check.mjs +123 -4
- package/api/integration/pack-check.test.mjs +87 -0
- package/api/integration/validate-integration-case-disk.test.mjs +111 -0
- package/api/integration/validate-integration-gaps.test.mjs +1964 -0
- package/api/integration/validate-integration.d.mts +11 -2
- package/api/integration/validate-integration.mjs +226 -32
- package/api/integration/validate-integration.type.d.mts +58 -1
- package/api/integration/validate-integration.type.mjs +45 -2
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/search/ORDERING.md +28 -0
- package/api/search/_adapter.d.mts +66 -2
- package/api/search/_adapter.mjs +118 -10
- package/api/search/_candidate-score.d.mts +40 -0
- package/api/search/_candidate-score.mjs +238 -0
- package/api/search/_cards.json +6098 -0
- package/api/search/_compound.d.mts +50 -0
- package/api/search/_compound.mjs +170 -0
- package/api/search/_doc-searcher.d.mts +4 -0
- package/api/search/_doc-searcher.mjs +23 -0
- package/api/search/_doc-searcher.test.mjs +36 -0
- package/api/search/_doc-searcher.weights.json +1 -0
- package/api/search/_doc-text.d.mts +30 -0
- package/api/search/_doc-text.mjs +87 -0
- package/api/search/_entries.d.mts +52 -0
- package/api/search/_entries.mjs +158 -0
- package/api/search/_features.d.mts +35 -0
- package/api/search/_features.mjs +155 -0
- package/api/search/_gather-components.d.mts +26 -0
- package/api/search/_gather-components.mjs +284 -0
- package/api/search/_gather-docs.d.mts +20 -0
- package/api/search/_gather-docs.mjs +254 -0
- package/api/search/_gather-hooks.d.mts +12 -0
- package/api/search/_gather-hooks.mjs +52 -0
- package/api/search/_gather-templates.d.mts +25 -0
- package/api/search/_gather-templates.mjs +107 -0
- package/api/search/_lexical.d.mts +135 -0
- package/api/search/_lexical.mjs +351 -0
- package/api/search/_main-searcher.d.mts +19 -0
- package/api/search/_main-searcher.mjs +115 -0
- package/api/search/_main-searcher.test.mjs +106 -0
- package/api/search/_main-searcher.weights.json +1 -0
- package/api/search/_module-doc.d.mts +32 -0
- package/api/search/_module-doc.mjs +78 -0
- package/api/search/_order.d.mts +32 -0
- package/api/search/_order.mjs +162 -0
- package/api/search/_order.test.mjs +72 -0
- package/api/search/_priority.d.mts +50 -0
- package/api/search/_priority.mjs +139 -0
- package/api/search/_query-words.d.mts +25 -0
- package/api/search/_query-words.mjs +92 -0
- package/api/search/_query.d.mts +65 -0
- package/api/search/_query.mjs +176 -0
- package/api/search/_result.d.mts +97 -0
- package/api/search/_result.mjs +150 -0
- package/api/search/_score.d.mts +60 -0
- package/api/search/_score.mjs +283 -0
- package/api/search/_synonyms.d.mts +21 -0
- package/api/search/_synonyms.mjs +67 -0
- package/api/search/_template-searcher.d.mts +4 -0
- package/api/search/_template-searcher.mjs +23 -0
- package/api/search/_template-searcher.test.mjs +40 -0
- package/api/search/_template-searcher.weights.json +1 -0
- package/api/search/_titles.d.mts +33 -0
- package/api/search/_titles.mjs +65 -0
- package/api/search/_words.d.mts +64 -0
- package/api/search/_words.mjs +157 -0
- package/api/search/search.d.mts +22 -305
- package/api/search/search.doc.mjs +7 -5
- package/api/search/search.mjs +53 -2483
- package/api/search/search.type.d.mts +6 -1
- package/api/search/search.type.mjs +6 -1
- package/api/search/tree.d.mts +15 -0
- package/api/search/tree.mjs +40 -0
- package/api/search/tree.test.mjs +66 -0
- package/api/search/tree.weights.json +1 -0
- package/api/theme/build/build.packed-old-core.test.mjs +1 -0
- package/api/upgrade/_adapter.d.mts +57 -9
- package/api/upgrade/_adapter.mjs +198 -28
- package/api/upgrade/registry/registry.d.mts +10 -4
- package/api/upgrade/registry/registry.mjs +33 -16
- package/api/upgrade/registry/registry.test.mjs +85 -0
- package/api/upgrade/run/run.d.mts +4 -2
- package/api/upgrade/run/run.mjs +13 -2
- package/api/upgrade/run/run.test.mjs +417 -3
- package/api/upgrade/upgrade.doc.mjs +10 -5
- package/api/upgrade/upgrade.mjs +13 -2
- package/api/upgrade/upgrade.test.mjs +194 -0
- package/api/upgrade/upgrade.type.d.mts +5 -3
- package/api/upgrade/upgrade.type.mjs +8 -6
- package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
- package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
- package/assets/docs/is-my-code-good.doc.mjs +287 -0
- package/assets/docs/tree/api.doc.mjs +6 -0
- package/assets/docs/tree/host-entry.doc.mjs +128 -0
- package/assets/docs/tree/template-grading-rubric.doc.mjs +16 -37
- package/assets/docs/tree/tokens-data.doc.mjs +2 -2
- package/assets/docs/working-with-ai.doc.mjs +6 -2
- package/assets/templates/themes/butter/butterTheme.ts +4 -2
- package/assets/templates/themes/stone/stoneTheme.ts +4 -2
- package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
- package/authoring/debug/parse.d.mts +1 -1
- package/clients/cli/bin/astryx.mjs +7 -78
- package/clients/cli/commands/doctor.mjs +30 -0
- package/clients/cli/commands/doctor.test.mjs +65 -0
- package/clients/cli/commands/init.behavior.test.mjs +230 -1
- package/clients/cli/commands/init.doc.mjs +28 -9
- package/clients/cli/commands/init.flag-help.test.mjs +1 -0
- package/clients/cli/commands/init.mjs +2 -2
- package/clients/cli/commands/outside-project.test.mjs +2 -1
- package/clients/cli/commands/setup-nudge.test.mjs +61 -0
- package/clients/cli/commands/upgrade.doc.mjs +4 -2
- package/clients/cli/commands/upgrade.mjs +9 -2
- package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
- package/clients/cli/index.mjs +19 -1
- package/clients/cli/run.mjs +108 -0
- package/foundation/agent-docs/agent-doc-state.d.mts +20 -7
- package/foundation/agent-docs/agent-doc-state.mjs +35 -8
- package/foundation/agent-docs/agent-docs.d.mts +100 -12
- package/foundation/agent-docs/agent-docs.mjs +1010 -99
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +678 -35
- package/foundation/config/project.mjs +20 -22
- package/foundation/config/project.test.mjs +56 -0
- package/foundation/discovery/component-discovery.d.mts +37 -0
- package/foundation/discovery/component-discovery.mjs +80 -5
- package/foundation/discovery/component-loader.d.mts +1 -1
- package/foundation/discovery/docs-discovery.mjs +14 -1
- package/foundation/discovery/module-exports.d.mts +41 -0
- package/foundation/discovery/module-exports.mjs +253 -0
- package/foundation/discovery/template-adapter.mjs +18 -1
- package/foundation/discovery/template-needs.mjs +1 -44
- package/foundation/discovery/theme-discovery.mjs +147 -3
- package/foundation/doc-compiler/doc-loads.test.mjs +8 -0
- package/foundation/doc-compiler/read.mjs +59 -1
- package/foundation/doc-compiler/read.test.mjs +11 -2
- package/foundation/fs/paths.d.mts +70 -8
- package/foundation/fs/paths.mjs +556 -38
- package/foundation/fs/paths.test.mjs +430 -54
- package/foundation/integrations/autolink.d.mts +2 -1
- package/foundation/integrations/autolink.mjs +4 -13
- package/foundation/integrations/computer-integrations.d.mts +6 -3
- package/foundation/integrations/computer-integrations.mjs +38 -9
- package/foundation/integrations/contribution-inventory.d.mts +15 -20
- package/foundation/integrations/contribution-inventory.mjs +41 -5
- package/foundation/integrations/integrations.d.mts +27 -1
- package/foundation/integrations/integrations.mjs +61 -2
- package/foundation/integrations/validate-contributions.d.mts +115 -0
- package/foundation/integrations/validate-contributions.mjs +1168 -13
- package/foundation/release/handoff.d.mts +23 -4
- package/foundation/release/handoff.mjs +175 -8
- package/foundation/release/run-context.mjs +14 -0
- package/foundation/response/response-types.doc.mjs +1 -1
- package/foundation/route/route.d.mts +285 -0
- package/foundation/route/route.mjs +324 -0
- package/foundation/route/route.test.mjs +153 -0
- package/foundation/runtime/hosted-runtime.test.mjs +403 -0
- package/foundation/runtime/locked-versions.d.mts +32 -0
- package/foundation/runtime/locked-versions.mjs +65 -0
- package/foundation/runtime/package-root.d.mts +91 -0
- package/foundation/runtime/package-root.mjs +174 -0
- package/foundation/runtime/package-set.d.mts +62 -0
- package/foundation/runtime/package-set.mjs +130 -0
- package/foundation/runtime/package-set.test.mjs +432 -0
- package/host/door-conformance.test.mjs +274 -0
- package/host/index.d.ts +116 -0
- package/host/index.mjs +295 -0
- package/host/index.test.mjs +302 -0
- package/package.json +14 -9
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file What build weighs for one request, computed once: search's matches
|
|
5
|
+
* grouped into pages, blocks, and domain components; the page template to
|
|
6
|
+
* start from and its alternatives (start.mjs, rank.mjs); and search's whole
|
|
7
|
+
* block list with its scores, for the block picker. answer.mjs turns these and
|
|
8
|
+
* a block list into the build.kit response.
|
|
9
|
+
* @input A request and build's options; templates and components through the
|
|
10
|
+
* adapter.
|
|
11
|
+
* @output {@link KitSignals}
|
|
12
|
+
* @position api/build/kit, beside kit.mjs and answer.mjs; the main searcher in
|
|
13
|
+
* tree.mjs calls it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
searchCore as search,
|
|
18
|
+
searchedComponents,
|
|
19
|
+
} from '../../search/search.mjs';
|
|
20
|
+
import {coreDirOrThrow} from '../../error.mjs';
|
|
21
|
+
import {getResultCoverage} from '../../search/coverage.mjs';
|
|
22
|
+
import {CORE_PROVIDER_ID} from '../../../foundation/identity/providers.mjs';
|
|
23
|
+
import {
|
|
24
|
+
loadComponents,
|
|
25
|
+
loadTemplates,
|
|
26
|
+
requireBuildCoreDir,
|
|
27
|
+
templateSetupNotes,
|
|
28
|
+
} from '../_adapter.mjs';
|
|
29
|
+
import {ideaKind} from './kind.mjs';
|
|
30
|
+
import {pickAlternatives, rankPages} from './rank.mjs';
|
|
31
|
+
import {asTemplate, chooseStart} from './start.mjs';
|
|
32
|
+
|
|
33
|
+
/** A page at/above this score is a confident direct match. */
|
|
34
|
+
const PAGE_DIRECT = 95;
|
|
35
|
+
|
|
36
|
+
/** Below this a page is too weak to offer at all. */
|
|
37
|
+
const PAGE_FLOOR = 50;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Below this a block/domain-component match is incidental noise. A single
|
|
41
|
+
* description word in a multi-word idea scores 50 plus at most 7.5 of coverage
|
|
42
|
+
* garnish, so it stays below; a name or keyword hit clears it. `build "weekly
|
|
43
|
+
* brief"` used to offer Toast, Popover and TextInput because their
|
|
44
|
+
* descriptions say "brief".
|
|
45
|
+
*/
|
|
46
|
+
const DOMAIN_FLOOR = 60;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How much of a multi-word query a page must cover to be offered on breadth.
|
|
50
|
+
*
|
|
51
|
+
* Score alone cannot carry this. A page's derived keywords include every
|
|
52
|
+
* component its source renders, so `build "actionable warning banner"` once
|
|
53
|
+
* scored `login`, `contact-form` and `documentation-design` at 95 apiece — an
|
|
54
|
+
* exact hit on "banner" alone, plus the coverage garnish. Matching one of
|
|
55
|
+
* three concepts is not the same claim as matching three.
|
|
56
|
+
*/
|
|
57
|
+
const PAGE_COVERAGE = 0.5;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Always-surfaced primitives. Every page needs a shell + layout/typography/
|
|
61
|
+
* action atoms, but these never keyword-match an idea ("dashboard" != "Stack"),
|
|
62
|
+
* so search alone never returns them. Kept here (not the renderer) because they
|
|
63
|
+
* are ALSO used to exclude these names from the idea-specific `domain` group.
|
|
64
|
+
* Every page template already uses them.
|
|
65
|
+
*/
|
|
66
|
+
export const FRAME = ['AppShell', 'TopNav', 'SideNav', 'Layout'];
|
|
67
|
+
|
|
68
|
+
export const FOUNDATION = [
|
|
69
|
+
'VStack',
|
|
70
|
+
'HStack',
|
|
71
|
+
'Grid',
|
|
72
|
+
'StackItem',
|
|
73
|
+
'Card',
|
|
74
|
+
'Section',
|
|
75
|
+
'Text',
|
|
76
|
+
'Heading',
|
|
77
|
+
'Button',
|
|
78
|
+
'Icon',
|
|
79
|
+
'Badge',
|
|
80
|
+
'Divider',
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const ALWAYS = new Set([...FRAME, ...FOUNDATION]);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @typedef {import('../../search/search.type.mjs').SearchResultEntry} SearchResultEntry
|
|
87
|
+
* @typedef {import('../build.type.mjs').BuildStart} BuildStart
|
|
88
|
+
* @typedef {[name: string, score: number, matched: number | null, total: number | null]} BlockScore
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Everything build weighs for one request.
|
|
93
|
+
* @typedef {object} KitSignals
|
|
94
|
+
* @property {string} query search's echo of the query
|
|
95
|
+
* @property {number} matchCount
|
|
96
|
+
* @property {boolean} directMatch
|
|
97
|
+
* @property {BuildStart | null} start the start, its alternatives, and its notes
|
|
98
|
+
* @property {SearchResultEntry[]} pages
|
|
99
|
+
* @property {SearchResultEntry[]} blocks the blocks build names itself
|
|
100
|
+
* @property {SearchResultEntry[]} domain
|
|
101
|
+
* @property {string[]} kitBlocks the names of `blocks`
|
|
102
|
+
* @property {BlockScore[]} searched every block search returned, in its order
|
|
103
|
+
* @property {string[]} foreign the blocks, named, searched or scaffoldable, whose package is not Core's
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A block result's name, score, and how much of the query it matched.
|
|
108
|
+
* @param {SearchResultEntry} r
|
|
109
|
+
* @returns {BlockScore}
|
|
110
|
+
*/
|
|
111
|
+
function blockScore(r) {
|
|
112
|
+
const coverage = getResultCoverage(r);
|
|
113
|
+
return [r.name, r.score, coverage?.matched ?? null, coverage?.total ?? null];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build's signals for one request (see {@link KitSignals}).
|
|
118
|
+
*
|
|
119
|
+
* @param {string} query what you're building (e.g. "analytics dashboard")
|
|
120
|
+
* @param {{cwd?: string, type?: import('../../search/search.type.mjs').SearchDomain, limit?: number}} [options]
|
|
121
|
+
* @returns {Promise<KitSignals>}
|
|
122
|
+
*/
|
|
123
|
+
export async function kitSignals(query, options = {}) {
|
|
124
|
+
const {cwd = process.cwd(), type, limit = 60} = options;
|
|
125
|
+
// A kit is built from Core's components, hooks, and templates. An open
|
|
126
|
+
// search without core covers the docs alone, so the kit asks for core here.
|
|
127
|
+
if (type !== 'doc') coreDirOrThrow(requireBuildCoreDir, cwd);
|
|
128
|
+
// search()'s JSDoc @returns widens results to object[]; the SearchResponse
|
|
129
|
+
// shape is the contract (api/search/search.type.mjs). Cast locally rather than
|
|
130
|
+
// tightening the search @returns (a separate follow-up).
|
|
131
|
+
const result =
|
|
132
|
+
/** @type {import('../../search/search.type.mjs').SearchResponse} */ (
|
|
133
|
+
await search(query, {
|
|
134
|
+
cwd,
|
|
135
|
+
type,
|
|
136
|
+
// Search wider than the surfaced kit so a flood of doc matches cannot
|
|
137
|
+
// bury the page templates past the cutoff; the caller's `limit` still
|
|
138
|
+
// caps the kit below. A non-positive or non-integer limit is passed
|
|
139
|
+
// through unchanged so search rejects it (ERR_INVALID_ARGUMENT).
|
|
140
|
+
limit:
|
|
141
|
+
Number.isInteger(limit) && limit > 0 ? Math.max(limit, 200) : limit,
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
const results = result.data.results;
|
|
145
|
+
// The TOTAL number of matches, not the number that survived `limit`. The kit
|
|
146
|
+
// below is deliberately small (≤3 pages, ≤5 blocks, ≤6 components) and
|
|
147
|
+
// `results` is itself capped, so every other count here is a cap; this is the
|
|
148
|
+
// one field that says how much the query actually matched.
|
|
149
|
+
const matchCount = result.data.matchCount;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Did this result answer enough of the query to stand as a page on breadth?
|
|
153
|
+
* Single-concept queries have nothing to cover, so they always pass. Coverage
|
|
154
|
+
* stays in a module-private WeakMap and never enters public search/build JSON.
|
|
155
|
+
* @param {object} r
|
|
156
|
+
*/
|
|
157
|
+
const covers = r => {
|
|
158
|
+
const coverage = getResultCoverage(r);
|
|
159
|
+
const total = coverage?.total ?? 1;
|
|
160
|
+
if (total <= 1) return true;
|
|
161
|
+
return (coverage?.matched ?? 0) / total >= PAGE_COVERAGE;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const matchedPages = results
|
|
165
|
+
.filter(
|
|
166
|
+
r =>
|
|
167
|
+
r.domain === 'template' &&
|
|
168
|
+
r.kind !== 'block' &&
|
|
169
|
+
r.score >= PAGE_FLOOR &&
|
|
170
|
+
covers(r),
|
|
171
|
+
)
|
|
172
|
+
.slice(0, 3);
|
|
173
|
+
const searchedBlocks = results.filter(
|
|
174
|
+
r => r.domain === 'template' && r.kind === 'block',
|
|
175
|
+
);
|
|
176
|
+
const blocks = searchedBlocks
|
|
177
|
+
.filter(r => r.score >= DOMAIN_FLOOR)
|
|
178
|
+
.slice(0, 5);
|
|
179
|
+
const domain = results
|
|
180
|
+
.filter(
|
|
181
|
+
r =>
|
|
182
|
+
(r.domain === 'component' || r.domain === 'hook') &&
|
|
183
|
+
r.score >= DOMAIN_FLOOR &&
|
|
184
|
+
!ALWAYS.has(r.name),
|
|
185
|
+
)
|
|
186
|
+
.slice(0, 6);
|
|
187
|
+
const directMatch =
|
|
188
|
+
matchedPages.length > 0 && matchedPages[0].score >= PAGE_DIRECT;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* On a loose match, a page entry's `command` previews the layout rather
|
|
192
|
+
* than printing the whole template: `--skeleton` gives the shape without
|
|
193
|
+
* presenting the page as the thing to build. The recommendation itself is
|
|
194
|
+
* `start`, which always scaffolds. Copied rather than mutated: these entries
|
|
195
|
+
* come from `search()` and are not this function's to modify.
|
|
196
|
+
*/
|
|
197
|
+
const pages = directMatch
|
|
198
|
+
? matchedPages
|
|
199
|
+
: matchedPages.map(page => ({
|
|
200
|
+
...page,
|
|
201
|
+
command: `${page.command} --skeleton`,
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
// The caller's `limit` caps the surfaced kit, even though the search above
|
|
205
|
+
// ran wider to find templates that a flood of doc matches would otherwise
|
|
206
|
+
// bury past the cutoff. Keep pages first, then blocks, then components.
|
|
207
|
+
let budget = limit;
|
|
208
|
+
/**
|
|
209
|
+
* @template T
|
|
210
|
+
* @param {T[]} arr
|
|
211
|
+
* @returns {T[]}
|
|
212
|
+
*/
|
|
213
|
+
const toLimit = arr => {
|
|
214
|
+
const out = arr.slice(0, Math.max(0, budget));
|
|
215
|
+
budget -= out.length;
|
|
216
|
+
return out;
|
|
217
|
+
};
|
|
218
|
+
const pagesKept = toLimit(pages);
|
|
219
|
+
const blocksKept = toLimit(blocks);
|
|
220
|
+
const domainKept = toLimit(domain);
|
|
221
|
+
|
|
222
|
+
// A kit narrowed to components or hooks has no page to start from; every
|
|
223
|
+
// other kit does, so the reader is never left to compose a page from scratch.
|
|
224
|
+
const wantsPages = !type || type === 'template';
|
|
225
|
+
const templates = wantsPages
|
|
226
|
+
? await loadTemplates(cwd)
|
|
227
|
+
: {pages: [], blocks: []};
|
|
228
|
+
const catalog = templates.pages;
|
|
229
|
+
const ranked = wantsPages ? rankPages(query, catalog) : [];
|
|
230
|
+
// A part of a page starts where it lives (spec:AST-048/FR3); the project's
|
|
231
|
+
// own components say what a part is. The search above already gathered them
|
|
232
|
+
// unless it was narrowed to templates.
|
|
233
|
+
const kind = wantsPages
|
|
234
|
+
? ideaKind(
|
|
235
|
+
query,
|
|
236
|
+
catalog,
|
|
237
|
+
searchedComponents(result) ?? (await loadComponents(cwd)),
|
|
238
|
+
)
|
|
239
|
+
: 'page';
|
|
240
|
+
const chosen = wantsPages
|
|
241
|
+
? chooseStart(ranked, kind, matchedPages, directMatch, catalog, query)
|
|
242
|
+
: null;
|
|
243
|
+
// Name the ranker's next two templates beside the start: the reader judges
|
|
244
|
+
// meaning better than keywords do, and an acceptable template is in these
|
|
245
|
+
// three far more often than it is the start alone.
|
|
246
|
+
/** @type {BuildStart | null} */
|
|
247
|
+
const start = chosen && {
|
|
248
|
+
...chosen,
|
|
249
|
+
alternatives: pickAlternatives(ranked, chosen.name).flatMap(r => {
|
|
250
|
+
const t = catalog.find(c => c.name === r.name);
|
|
251
|
+
return t ? [asTemplate(t)] : [];
|
|
252
|
+
}),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Analyze what the start template needs that the project lacks (external
|
|
256
|
+
// packages, StyleX compiler). Only the start — alternatives are suggestions,
|
|
257
|
+
// not commitments, so reporting their needs would be noise.
|
|
258
|
+
if (start) {
|
|
259
|
+
const startTemplate = catalog.find(t => t.name === start.name);
|
|
260
|
+
if (startTemplate) {
|
|
261
|
+
const notes = templateSetupNotes(startTemplate, cwd);
|
|
262
|
+
if (notes.length > 0) start.notes = notes;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
query: result.data.query,
|
|
268
|
+
matchCount,
|
|
269
|
+
directMatch,
|
|
270
|
+
start,
|
|
271
|
+
pages: pagesKept,
|
|
272
|
+
blocks: blocksKept,
|
|
273
|
+
domain: domainKept,
|
|
274
|
+
kitBlocks: blocksKept.map(r => r.name),
|
|
275
|
+
searched: searchedBlocks.map(blockScore),
|
|
276
|
+
foreign: [
|
|
277
|
+
...new Set(
|
|
278
|
+
[...blocksKept, ...searchedBlocks, ...templates.blocks]
|
|
279
|
+
.filter(b => b.package !== CORE_PROVIDER_ID)
|
|
280
|
+
.map(b => b.name),
|
|
281
|
+
),
|
|
282
|
+
],
|
|
283
|
+
};
|
|
284
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The template to start from: the ready page the ranker puts first when it
|
|
6
|
+
* has the evidence to lead, else the first fallback shell the project can
|
|
7
|
+
* scaffold. Null only when the project has no page template to offer at all.
|
|
8
|
+
*
|
|
9
|
+
* `direct` means two independent signals agree: the ranker's pick is also
|
|
10
|
+
* search's direct match. The ranker sees only ready templates, so a template
|
|
11
|
+
* still marked not ready is never the start; when search matched one directly
|
|
12
|
+
* the reason names it, so the reader knows why the kit starts elsewhere.
|
|
13
|
+
*
|
|
14
|
+
* @param {import('./rank.mjs').RankedPage[]} ranked
|
|
15
|
+
* @param {import('./rank.mjs').IdeaKind} kind
|
|
16
|
+
* @param {SearchResultEntry[]} pages
|
|
17
|
+
* @param {boolean} directMatch
|
|
18
|
+
* @param {PageTemplate[]} catalog
|
|
19
|
+
* @param {string} idea
|
|
20
|
+
* @returns {Omit<BuildStart, 'alternatives'> | null}
|
|
21
|
+
*/
|
|
22
|
+
export function chooseStart(ranked: import("./rank.mjs").RankedPage[], kind: import("./rank.mjs").IdeaKind, pages: SearchResultEntry[], directMatch: boolean, catalog: PageTemplate[], idea: string): Omit<BuildStart, "alternatives"> | null;
|
|
23
|
+
export function asTemplate(t: PageTemplate): {
|
|
24
|
+
name: string;
|
|
25
|
+
package: string;
|
|
26
|
+
displayName: string;
|
|
27
|
+
description: string;
|
|
28
|
+
command: string;
|
|
29
|
+
};
|
|
30
|
+
export type SearchResultEntry = import("../../search/search.type.mjs").SearchResultEntry;
|
|
31
|
+
export type BuildStart = import("../build.type.mjs").BuildStart;
|
|
32
|
+
export type PageTemplate = import("../_adapter.mjs").PageTemplate;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The page template a kit starts from: the page ranker's pick when it has
|
|
5
|
+
* the evidence to lead, the template a part lives in, else the app shell.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {loadWeights} from '../_adapter.mjs';
|
|
9
|
+
import {asksForNewPage} from './kind.mjs';
|
|
10
|
+
import {pickStart} from './rank.mjs';
|
|
11
|
+
import {weighStart} from './weights.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {import('../../search/search.type.mjs').SearchResultEntry} SearchResultEntry
|
|
15
|
+
* @typedef {import('../build.type.mjs').BuildStart} BuildStart
|
|
16
|
+
* @typedef {import('../_adapter.mjs').PageTemplate} PageTemplate
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Where a page starts when no page template matched: the first of these the
|
|
21
|
+
* project can scaffold. A top nav over empty, full-width content is the least
|
|
22
|
+
* opinionated frame that still has navigation; `blank` is the floor, with no
|
|
23
|
+
* chrome at all. Either one still hands the reader a page frame and its
|
|
24
|
+
* padding, which composing from components does not.
|
|
25
|
+
*/
|
|
26
|
+
const FALLBACK_STARTS = ['shell-top-nav', 'blank'];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A page template as the kit names it: the command that selects exactly that
|
|
30
|
+
* template, scaffolding into `<path>`, a placeholder for the file or folder to
|
|
31
|
+
* write it to.
|
|
32
|
+
* @param {PageTemplate} t
|
|
33
|
+
*/
|
|
34
|
+
export const asTemplate = t => ({
|
|
35
|
+
name: t.name,
|
|
36
|
+
package: t.package,
|
|
37
|
+
displayName: t.displayName,
|
|
38
|
+
description: t.description,
|
|
39
|
+
command: `${t.command} <path>`,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Why a part of a page, or a change to a page the builder already has, starts
|
|
44
|
+
* where it does (spec:AST-048/FR3, FR9): the rest of the start's reason, or
|
|
45
|
+
* null for a whole page.
|
|
46
|
+
* @param {import('./rank.mjs').IdeaKind} kind
|
|
47
|
+
* @param {boolean} inPage whether the start is the page the idea names
|
|
48
|
+
* @returns {string | null}
|
|
49
|
+
*/
|
|
50
|
+
function placement(kind, inPage) {
|
|
51
|
+
if (kind === 'edit')
|
|
52
|
+
return 'the idea changes a page you already have, so keep it and add blocks to it; a new page starts from the app shell.';
|
|
53
|
+
if (kind === 'part')
|
|
54
|
+
return inPage
|
|
55
|
+
? 'the idea is a part of a page, so it starts from the page it names.'
|
|
56
|
+
: 'the idea is a part of a page and names no page, so it starts from the app shell.';
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The template to start from: the ready page the ranker puts first when it
|
|
62
|
+
* has the evidence to lead, else the first fallback shell the project can
|
|
63
|
+
* scaffold. Null only when the project has no page template to offer at all.
|
|
64
|
+
*
|
|
65
|
+
* `direct` means two independent signals agree: the ranker's pick is also
|
|
66
|
+
* search's direct match. The ranker sees only ready templates, so a template
|
|
67
|
+
* still marked not ready is never the start; when search matched one directly
|
|
68
|
+
* the reason names it, so the reader knows why the kit starts elsewhere.
|
|
69
|
+
*
|
|
70
|
+
* @param {import('./rank.mjs').RankedPage[]} ranked
|
|
71
|
+
* @param {import('./rank.mjs').IdeaKind} kind
|
|
72
|
+
* @param {SearchResultEntry[]} pages
|
|
73
|
+
* @param {boolean} directMatch
|
|
74
|
+
* @param {PageTemplate[]} catalog
|
|
75
|
+
* @param {string} idea
|
|
76
|
+
* @returns {Omit<BuildStart, 'alternatives'> | null}
|
|
77
|
+
*/
|
|
78
|
+
export function chooseStart(ranked, kind, pages, directMatch, catalog, idea) {
|
|
79
|
+
const direct = directMatch ? pages[0].name : null;
|
|
80
|
+
const unready =
|
|
81
|
+
direct && !catalog.some(t => t.name === direct) ? direct : null;
|
|
82
|
+
// The reason never denies a match the same response reports: a direct
|
|
83
|
+
// match the ranker outweighed is named, and so are the loose page matches
|
|
84
|
+
// search listed when the kit falls back to the shell.
|
|
85
|
+
const loose = pages.map(p => `\`${p.name}\``).join(', ');
|
|
86
|
+
/**
|
|
87
|
+
* A part's or an edit's reason, naming a direct match that is not the start.
|
|
88
|
+
* @param {string} place
|
|
89
|
+
* @param {string} startName
|
|
90
|
+
*/
|
|
91
|
+
const placed = (place, startName) =>
|
|
92
|
+
direct && direct !== startName
|
|
93
|
+
? `Search matched \`${direct}\` by name, but ${place}`
|
|
94
|
+
: place[0].toUpperCase() + place.slice(1);
|
|
95
|
+
const proposed = pickStart(ranked, kind);
|
|
96
|
+
// The checked-in word weights (weights.mjs), blended with the ranker's
|
|
97
|
+
// scores, decide the start of a whole page. A part or an edit starts where
|
|
98
|
+
// the ranker's placement rules put it (spec:AST-048/FR3).
|
|
99
|
+
const weighed =
|
|
100
|
+
kind === 'page'
|
|
101
|
+
? weighStart(idea, ranked, proposed, catalog, {
|
|
102
|
+
weights: loadWeights(),
|
|
103
|
+
newPage: asksForNewPage(idea, catalog),
|
|
104
|
+
})
|
|
105
|
+
: undefined;
|
|
106
|
+
// A shell start keeps the shell the ranker named, if any, and never replaces
|
|
107
|
+
// the template the ranker chose for a page search matched directly.
|
|
108
|
+
const pick =
|
|
109
|
+
weighed === undefined
|
|
110
|
+
? proposed
|
|
111
|
+
: weighed === null
|
|
112
|
+
? proposed?.family === 'Shell' || (proposed && direct && !unready)
|
|
113
|
+
? proposed
|
|
114
|
+
: null
|
|
115
|
+
: (ranked.find(r => r.name === weighed) ?? proposed);
|
|
116
|
+
const closest = pick && catalog.find(t => t.name === pick.name);
|
|
117
|
+
if (pick && closest) {
|
|
118
|
+
const agrees = closest.name === direct;
|
|
119
|
+
const place = placement(kind, pick.base && pick.familyNamed);
|
|
120
|
+
return {
|
|
121
|
+
...asTemplate(closest),
|
|
122
|
+
basis: agrees ? 'direct' : 'closest',
|
|
123
|
+
reason: unready
|
|
124
|
+
? `\`${unready}\` matches but is not ready yet; this is the closest ready template.`
|
|
125
|
+
: place
|
|
126
|
+
? placed(place, closest.name)
|
|
127
|
+
: agrees
|
|
128
|
+
? 'Matches the idea.'
|
|
129
|
+
: direct
|
|
130
|
+
? `Search matched \`${direct}\` by name, but this template fits more of the idea.`
|
|
131
|
+
: 'The closest template; none is exactly this page.',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
for (const id of FALLBACK_STARTS) {
|
|
135
|
+
const shell = catalog.find(t => t.name === id);
|
|
136
|
+
if (shell) {
|
|
137
|
+
// The shell can also be the ranker's best guess without the evidence to
|
|
138
|
+
// lead ("horizontal site navigation"); say so rather than "no match".
|
|
139
|
+
const nearest =
|
|
140
|
+
(ranked[0]?.name === shell.name && ranked[0].hits > 0) ||
|
|
141
|
+
(weighed === null && !!proposed && proposed.family !== 'Shell');
|
|
142
|
+
const place = placement(kind, false);
|
|
143
|
+
return {
|
|
144
|
+
...asTemplate(shell),
|
|
145
|
+
basis: 'fallback',
|
|
146
|
+
reason: unready
|
|
147
|
+
? `\`${unready}\` matches but is not ready yet, so start from the app shell.`
|
|
148
|
+
: place
|
|
149
|
+
? placed(place, shell.name)
|
|
150
|
+
: direct
|
|
151
|
+
? weighed === null
|
|
152
|
+
? `Search matched \`${direct}\` by name, but the app shell is the closer start.`
|
|
153
|
+
: `Search matched \`${direct}\` by name, but too little of the idea fits it, so start from the app shell.`
|
|
154
|
+
: nearest
|
|
155
|
+
? 'No template is a clear match; the app shell is the closest.'
|
|
156
|
+
: loose
|
|
157
|
+
? `Search matched ${loose} only loosely, so start from the app shell.`
|
|
158
|
+
: 'No template matched, so start from the app shell.',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Content terms of a text: lowercase alphanumeric words, stopwords removed,
|
|
6
|
+
* stemmed.
|
|
7
|
+
* @param {string} text
|
|
8
|
+
* @returns {string[]}
|
|
9
|
+
*/
|
|
10
|
+
export function terms(text: string): string[];
|
|
11
|
+
/**
|
|
12
|
+
* The terms of an idea that only ever modify another word, each with the words
|
|
13
|
+
* it modifies: in each phrase the last content word is its head, and the ones
|
|
14
|
+
* before it describe the next.
|
|
15
|
+
* @param {string} query
|
|
16
|
+
* @returns {Map<string, Set<string>>}
|
|
17
|
+
*/
|
|
18
|
+
export function modifiersOf(query: string): Map<string, Set<string>>;
|
|
19
|
+
/**
|
|
20
|
+
* Adjacent term pairs a text names, "a b", for telling a template that names
|
|
21
|
+
* a whole compound ("executive summary") from one that names only its parts.
|
|
22
|
+
* @param {string[]} texts
|
|
23
|
+
* @returns {Set<string>}
|
|
24
|
+
*/
|
|
25
|
+
export function pairsOf(texts: string[]): Set<string>;
|
|
26
|
+
/**
|
|
27
|
+
* The terms of an idea that name its frame: "modal" in "in a modal". A card, a
|
|
28
|
+
* table or an existing page after "in a" is where a part goes, not a frame.
|
|
29
|
+
* @param {string} query
|
|
30
|
+
* @returns {Set<string>}
|
|
31
|
+
*/
|
|
32
|
+
export function containersOf(query: string): Set<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Family words, from the templates' own ids: a word in the ids of two or more
|
|
35
|
+
* templates of one family, plus the family's name when a template carries it.
|
|
36
|
+
* Each word maps to its family.
|
|
37
|
+
* @param {PageTemplate[]} pages
|
|
38
|
+
* @returns {Map<string, string>}
|
|
39
|
+
*/
|
|
40
|
+
export function familyWordsOf(pages: PageTemplate[]): Map<string, string>;
|
|
41
|
+
/**
|
|
42
|
+
* Where an idea's head ends: its first clause, before the parts it lists.
|
|
43
|
+
*/
|
|
44
|
+
export const HEAD_END: RegExp;
|
|
45
|
+
/**
|
|
46
|
+
* The family whose templates are app chrome rather than page content: the
|
|
47
|
+
* explicit `Shell -` category (architecture:template-authoring/INV7).
|
|
48
|
+
*/
|
|
49
|
+
export const SHELL_FAMILY: "Shell";
|
|
50
|
+
/** A container phrase: "in a modal", "inside the side panel". */
|
|
51
|
+
export const CONTAINER_PHRASE: RegExp;
|
|
52
|
+
/**
|
|
53
|
+
* Where a phrase ends, for finding the word that heads it: punctuation and
|
|
54
|
+
* the words that join phrases.
|
|
55
|
+
*/
|
|
56
|
+
export const PHRASE_END: RegExp;
|
|
57
|
+
export function normalized(text: string): string;
|
|
58
|
+
/** Stemmed synonym lookup built from search's vocabulary, both directions. */
|
|
59
|
+
export const SYNONYMS_OF: Map<string, Set<string>>;
|
|
60
|
+
export type PageTemplate = import("../_adapter.mjs").PageTemplate;
|
|
61
|
+
export type ComponentWords = import("../_adapter.mjs").ComponentWords;
|
|
62
|
+
export type RankedPage = {
|
|
63
|
+
name: string;
|
|
64
|
+
score: number;
|
|
65
|
+
hits: number;
|
|
66
|
+
familyNamed: boolean;
|
|
67
|
+
containerMatched: boolean;
|
|
68
|
+
family: string;
|
|
69
|
+
base: boolean;
|
|
70
|
+
matched: Set<string>;
|
|
71
|
+
};
|
|
72
|
+
export type IdeaKind = "page" | "part" | "edit";
|