@algolia/wizard 0.68.0 → 0.70.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/dist/main.js
CHANGED
|
@@ -1969,7 +1969,7 @@ function identify(traits) {
|
|
|
1969
1969
|
// package.json
|
|
1970
1970
|
var package_default = {
|
|
1971
1971
|
name: "@algolia/wizard",
|
|
1972
|
-
version: "0.
|
|
1972
|
+
version: "0.70.0",
|
|
1973
1973
|
description: "Magically implement Algolia functionality in your codebase",
|
|
1974
1974
|
type: "module",
|
|
1975
1975
|
engines: {
|
|
@@ -2810,10 +2810,11 @@ import { relative as relative3 } from "node:path";
|
|
|
2810
2810
|
import { z as z12 } from "zod";
|
|
2811
2811
|
|
|
2812
2812
|
// src/lib/keychain.ts
|
|
2813
|
+
import { createHash } from "node:crypto";
|
|
2813
2814
|
import { deletePassword, getPassword, setPassword } from "cross-keychain";
|
|
2814
2815
|
import { z as z11 } from "zod";
|
|
2815
2816
|
var SERVICE = "algolia-wizard";
|
|
2816
|
-
var account = (userId) => `api-keys
|
|
2817
|
+
var account = (userId) => `api-keys-${createHash("sha256").update(userId).digest("hex")}`;
|
|
2817
2818
|
var storedKeysSchema = z11.record(z11.string(), z11.string());
|
|
2818
2819
|
function entryId(kind, index, appId) {
|
|
2819
2820
|
return `${kind}:${appId}:${index}`;
|
|
@@ -2875,15 +2876,17 @@ function storeKey(kind, userId, index, appId, value) {
|
|
|
2875
2876
|
}
|
|
2876
2877
|
function deleteStoredKeys(userId) {
|
|
2877
2878
|
return serialized(async () => {
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2879
|
+
for (const keychainAccount of [account(userId), "api-keys"]) {
|
|
2880
|
+
try {
|
|
2881
|
+
await deletePassword(SERVICE, keychainAccount);
|
|
2882
|
+
} catch (err) {
|
|
2883
|
+
const message = err.message;
|
|
2884
|
+
if (/not found/i.test(message)) continue;
|
|
2885
|
+
logger.warn(
|
|
2886
|
+
{ err: message, userId },
|
|
2887
|
+
"could not delete the API keys from the keychain"
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2887
2890
|
}
|
|
2888
2891
|
});
|
|
2889
2892
|
}
|
|
@@ -3303,7 +3306,7 @@ import { relative as relative5 } from "node:path";
|
|
|
3303
3306
|
// src/lib/tools/reviewScript.ts
|
|
3304
3307
|
import { tool as tool8 } from "ai";
|
|
3305
3308
|
import z15 from "zod";
|
|
3306
|
-
import { createHash } from "node:crypto";
|
|
3309
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3307
3310
|
import { readFile as readFile7, stat as stat2 } from "node:fs/promises";
|
|
3308
3311
|
import { relative as relative4 } from "node:path";
|
|
3309
3312
|
|
|
@@ -3320,7 +3323,7 @@ function serializePrompt(work) {
|
|
|
3320
3323
|
var reviewedCommandApprovals = /* @__PURE__ */ new WeakMap();
|
|
3321
3324
|
async function fileDigest(filePath) {
|
|
3322
3325
|
const content = await readFile7(filePath);
|
|
3323
|
-
return
|
|
3326
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
3324
3327
|
}
|
|
3325
3328
|
function grantReviewedCommandApproval(ctx, approval) {
|
|
3326
3329
|
reviewedCommandApprovals.set(ctx, approval);
|
|
@@ -4423,10 +4426,14 @@ var MODE_CONFIG = {
|
|
|
4423
4426
|
},
|
|
4424
4427
|
searchImplementation: {
|
|
4425
4428
|
instructions: [
|
|
4426
|
-
"Analyze the codebase to determine the single best
|
|
4427
|
-
"
|
|
4429
|
+
"Analyze the codebase to determine the single best existing file in which to mount search UI.",
|
|
4430
|
+
"Inspect the layout hierarchy, routes, and the candidate file together with its parent and sibling components; do not choose a location from its filename alone.",
|
|
4431
|
+
"Identify the pages and components that display or browse the indexed domain content, plus any existing search, filter, or navigation controls. Details of the confirmed entity are appended below when the wizard already knows them; otherwise infer the likely indexed domain content from the codebase.",
|
|
4432
|
+
"Match placement to search scope: put content-specific search near the corresponding browse/list controls; use a shared header or navigation component only when search is genuinely app-wide and fits its visual hierarchy.",
|
|
4433
|
+
"Account for available space, alignment, and behavior at the app's existing responsive breakpoints so the control will not crowd or displace primary navigation.",
|
|
4434
|
+
"Prefer an existing search control with the same content scope when one can be replaced without removing unrelated filters or navigation.",
|
|
4428
4435
|
"Return one file path as searchImplementationAnalysis (e.g. /layouts/header.tsx).",
|
|
4429
|
-
'
|
|
4436
|
+
'Spend your reads on the layout entry point, the candidate file with its parent and siblings, and the UI that lists the indexed content \u2014 around a dozen targeted reads or searches, not a repo-wide crawl. Stop once one location is clearly best, and do not guess: if none is, say "unknown".',
|
|
4430
4437
|
"Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
|
|
4431
4438
|
"When done, call reportStatus"
|
|
4432
4439
|
],
|
|
@@ -4681,7 +4688,8 @@ import z26 from "zod";
|
|
|
4681
4688
|
var confirmEntitiesSchema = z26.object({
|
|
4682
4689
|
// Final detection — the focused re-run may supersede project-scan's.
|
|
4683
4690
|
ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
|
|
4684
|
-
confirmedEntities: confirmedEntitiesFieldSchema
|
|
4691
|
+
confirmedEntities: confirmedEntitiesFieldSchema,
|
|
4692
|
+
searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional()
|
|
4685
4693
|
});
|
|
4686
4694
|
var SUGGEST_ENTITY = "Suggest a different entity\u2026";
|
|
4687
4695
|
function usedDetectedBackend(detected, confirmed2) {
|
|
@@ -4804,7 +4812,26 @@ async function confirmEntities(ctx) {
|
|
|
4804
4812
|
attribute_count: summary[0]?.attributes.length ?? 0,
|
|
4805
4813
|
used_detected_back_end: usedDetectedBackend(scan.languages, languages)
|
|
4806
4814
|
});
|
|
4807
|
-
|
|
4815
|
+
let searchImplementationAnalysis = scan.searchImplementationAnalysis;
|
|
4816
|
+
try {
|
|
4817
|
+
const placement = await runAnalysis("searchImplementation", [
|
|
4818
|
+
`The user confirmed this indexed entity: ${JSON.stringify(confirmed2[0])}. Determine placement specifically for this content and its existing user task; do not substitute another domain found in the repository.`
|
|
4819
|
+
]);
|
|
4820
|
+
searchImplementationAnalysis = placement.searchImplementationAnalysis;
|
|
4821
|
+
} catch (err) {
|
|
4822
|
+
logger.warn({ err }, "confirmEntities: placement refresh failed");
|
|
4823
|
+
}
|
|
4824
|
+
if (searchImplementationAnalysis) {
|
|
4825
|
+
ctx.setUserInput(
|
|
4826
|
+
"searchImplementationAnalysis",
|
|
4827
|
+
searchImplementationAnalysis
|
|
4828
|
+
);
|
|
4829
|
+
}
|
|
4830
|
+
return {
|
|
4831
|
+
ingestionAnalysis: entities,
|
|
4832
|
+
confirmedEntities: confirmed2,
|
|
4833
|
+
searchImplementationAnalysis
|
|
4834
|
+
};
|
|
4808
4835
|
}
|
|
4809
4836
|
}
|
|
4810
4837
|
|
|
@@ -5130,19 +5157,37 @@ function ingestionInstructions(input) {
|
|
|
5130
5157
|
];
|
|
5131
5158
|
}
|
|
5132
5159
|
function searchInstructions(input) {
|
|
5133
|
-
const
|
|
5134
|
-
const
|
|
5135
|
-
|
|
5160
|
+
const indexedEntity = input.findings.confirmedEntities?.[0];
|
|
5161
|
+
const indexedContentMessage = indexedEntity ? `Indexed content context: ${JSON.stringify({
|
|
5162
|
+
entity: indexedEntity.name,
|
|
5163
|
+
attributes: indexedEntity.attributes,
|
|
5164
|
+
sourcePaths: indexedEntity.paths
|
|
5165
|
+
})}. Use the source and the existing UI that displays this entity to understand the search task, result content, and appropriate scope.` : null;
|
|
5166
|
+
const mountCandidateMessage = input.searchLocation ? `The confirmed-content placement analysis suggested "${input.searchLocation}" as a mount candidate. Before editing, read that file and inspect its parent layout, sibling controls, and the relevant content UI. If the evidence confirms it, import and render the new search component there; otherwise mount it in the better existing file. In all cases, mount the component before finishing; do not leave an unrendered component.` : "The scan found no reliable mount candidate. Determine the best existing location from the layout hierarchy and the UI that displays the indexed content, then mount the new search component there; do not default to a header or navigation component or leave an unrendered component.";
|
|
5167
|
+
const placementMessage = "Place content-specific search beside the heading or controls for the corresponding browse/listing task. Use shared navigation only for genuinely app-wide search and only when it fits the available space and responsive layout without crowding primary actions.";
|
|
5168
|
+
const replacementMessage = "If a search control with the same content scope and purpose already exists, replace its usage with the new component while preserving its intentional placement, and delete the implementation it supersedes. Do not remove or replace unrelated filters, navigation, or differently scoped search.";
|
|
5169
|
+
const accessibilityInstructions = [
|
|
5170
|
+
"Target WCAG 2.2 AA. Use semantic search structure and give the input a persistent accessible name: prefer a visible associated label and use the project's visually-hidden utility where the layout has no room for one. A placeholder or a labeled search landmark does not name the input. When the input uses aria-labelledby, every referenced element must exist and contain meaningful, content-specific text. Preserve a widget's generated label when it provides one and make it content-specific through supported options; if the installed widget exposes no such option, use its documented connector or headless API to render a labeled input instead of patching generated DOM.",
|
|
5171
|
+
"Preserve widget-provided combobox, popup, and item roles, relationships, and keyboard behavior; for a headless widget, implement the documented semantics instead of inventing different ARIA. Tab must reach the input and controls, Arrow keys move through results, Enter selects, and Escape dismisses without trapping or losing focus.",
|
|
5172
|
+
"Meet 4.5:1 contrast for normal text, including input values, placeholders, and result text, and 3:1 for large text, meaningful icons, control boundaries, and focus indicators against adjacent colors. Keep focus visible and unobscured, and never communicate active, loading, empty, or error states by color alone.",
|
|
5173
|
+
"Keep loading, empty, and error status text perceivable to assistive technology. Do not assume a widget announces these states: use documented announcements when present, otherwise add a persistent polite live region driven by the current search status, results, and errors.",
|
|
5174
|
+
"Reflow at a 320 CSS-pixel viewport without horizontal page scrolling, keep the results panel readable and on-screen whatever the width of the input it hangs off, keep interactive targets at least 24 by 24 CSS pixels or sufficiently spaced, and respect prefers-reduced-motion for any animation you add."
|
|
5175
|
+
];
|
|
5136
5176
|
const packageManagedInstructions = input.frontendHasPackageJson ? packageSearchInstructions(input) : cdnSearchInstructions(input);
|
|
5137
5177
|
return [
|
|
5138
5178
|
"Implement an in-app Algolia search experience.",
|
|
5139
|
-
|
|
5179
|
+
indexedContentMessage ?? "",
|
|
5140
5180
|
`Build the search UI for ${input.searchUiTarget}.`,
|
|
5141
5181
|
"Create search UI only. Do not create or modify ingestion scripts, rake/manage/CLI tasks, migrations, seeders, or other data-loading code, even if the data looks incomplete.",
|
|
5182
|
+
mountCandidateMessage,
|
|
5183
|
+
placementMessage,
|
|
5184
|
+
replacementMessage,
|
|
5185
|
+
"Inspect the chosen mount area at the app's existing breakpoints before writing CSS. Preserve its visual hierarchy, alignment, spacing, and primary actions rather than forcing the search control into an arbitrary gap.",
|
|
5142
5186
|
...packageManagedInstructions,
|
|
5143
|
-
|
|
5144
|
-
"
|
|
5145
|
-
"
|
|
5187
|
+
...accessibilityInstructions,
|
|
5188
|
+
"Style the search surfaces through the widget's class and CSS-variable overrides (or, when hand-built, the app's existing theme tokens): give the results panel and the element that actually draws the input's border and background \u2014 for a widget, the form or wrapper it renders around the bare input \u2014 non-transparent surfaces, keep that frame distinguishable from the background behind it in every supported theme, and never assume a light surface or reuse the surrounding page's colors unchanged inside the panel.",
|
|
5189
|
+
"The panel takes its width from the input by default, so a small input makes it unreadably narrow: give it `min-width: min(320px, calc(100vw - 2rem))` independent of the input, cap its maximum width to the viewport, and anchor it to the input edge it opens from so widening does not push it off-screen.",
|
|
5190
|
+
"Match the application styles closely, including light, dark, and high-contrast themes the project already supports.",
|
|
5146
5191
|
"The summary should be extremely concise; do not mention manual testing steps."
|
|
5147
5192
|
];
|
|
5148
5193
|
}
|
|
@@ -5156,8 +5201,6 @@ function packageSearchInstructions(input) {
|
|
|
5156
5201
|
"No bundled Algolia SDK reference exists for this stack, so rely on the project's own conventions and Algolia's official client for its language. Do not invent APIs \u2014 keep to the documented search endpoint and its parameters."
|
|
5157
5202
|
],
|
|
5158
5203
|
`Create the search experience as its own component in a new file, following the project's existing component conventions (location, naming, styling approach). Do not write it inline into an existing file.`,
|
|
5159
|
-
`Import and render the new component in ${input.searchLocation ? `"${input.searchLocation}"` : "the best, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working search input and results panel against the target index.`,
|
|
5160
|
-
"If a search box already exists, replace its usage with an import and render of your new component; remove the old implementation.",
|
|
5161
5204
|
"When rendering results with an existing shared component (e.g. a card), import and reuse that component rather than inlining its markup \u2014 inlining silently drops the styles and behavior its own file provides.",
|
|
5162
5205
|
"Vendor theme CSS, and any CSS you write against vendor class names, must be global: import the theme from the component's own script module or the app's global stylesheet, and put overrides in a global block (Astro <style is:global>, an unscoped Vue block, a plain global CSS file). Never a framework-scoped style block or a CSS Module \u2014 scoping rewrites the vendor selectors and the widget's runtime DOM carries no scope attribute, so not one rule matches: styling silently does nothing and the build still passes.",
|
|
5163
5206
|
`Define ${SEARCH_CONFIG_APP_ID}, ${SEARCH_CONFIG_SEARCH_KEY}, and ${SEARCH_CONFIG_INDEX_NAME} as exported constants in a module that fits this project's existing conventions for shared client-side config \u2014 reuse an existing one if it already holds config like this, or add a small new one otherwise. These are PUBLIC values, safe to commit and expose client-side: never read them from an environment variable or a .env* file, and never hardcode them anywhere except in that one module (import them wherever the search client needs them).`,
|
|
@@ -5168,21 +5211,29 @@ function packageSearchInstructions(input) {
|
|
|
5168
5211
|
];
|
|
5169
5212
|
}
|
|
5170
5213
|
function cdnSearchInstructions(input) {
|
|
5214
|
+
const entityName = input.findings.confirmedEntities?.[0]?.name;
|
|
5215
|
+
const placeholder = entityName ? `Search ${entityName} records...` : "Search indexed content...";
|
|
5171
5216
|
return [
|
|
5172
5217
|
`The target frontend has no package.json. Add the search container to its existing HTML or template, create separate classic config and search scripts, define window.${SEARCH_CONFIG_APP_ID}, window.${SEARCH_CONFIG_SEARCH_KEY}, and window.${SEARCH_CONFIG_INDEX_NAME} in the config script, and report that script as "searchConfigFile". Never create package.json, create a component, use imports or exports, or run npm, pnpm, yarn, or bun.`,
|
|
5173
5218
|
`Set window.${SEARCH_CONFIG_APP_ID} to "${input.appId}" and window.${SEARCH_CONFIG_INDEX_NAME} to "${input.targetIndex}".`,
|
|
5174
5219
|
input.searchKey ? `Set window.${SEARCH_CONFIG_SEARCH_KEY} to "${input.searchKey}".` : `A real search-only key could not be provisioned${input.searchKeyError ? ` (${input.searchKeyError})` : ""} \u2014 set window.${SEARCH_CONFIG_SEARCH_KEY} to the placeholder "${SEARCH_KEY_PLACEHOLDER}" and add a prominent TODO for the developer to fill in a real one.`,
|
|
5175
5220
|
'Load these exact pinned tags in order before the config and search scripts: <script src="https://cdn.jsdelivr.net/npm/algoliasearch@5.59.0/dist/lite/builds/browser.umd.js" integrity="sha256-pduQHl1jn0IaN/BIpSotQm7Y5THdpKcX3Bw6xdRteRw=" crossorigin="anonymous"></script> then <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.117.0/dist/instantsearch.production.min.js" integrity="sha256-5zhKxAGeH7ThWfnmdD4pS1gsmafE2b8aWRsq8set/dc=" crossorigin="anonymous"></script>.',
|
|
5176
5221
|
'When using the InstantSearch theme, load this exact tag before the application styles: <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@8.22.1/themes/satellite-min.css" integrity="sha256-5/eHPZl63VYJSDVOKrOgGe/5+owUkX3uPAW6+XYeLNc=" crossorigin="anonymous">. Never use an unversioned or differently versioned theme URL, and never omit its integrity attribute.',
|
|
5177
|
-
`In that classic search script, use window['algoliasearch/lite'].liteClient and window.instantsearch. Add window.instantsearch.widgets.autocomplete as the only search widget. Configure it with container '#autocomplete', placeholder
|
|
5222
|
+
`In that classic search script, use window['algoliasearch/lite'].liteClient and window.instantsearch. Add window.instantsearch.widgets.autocomplete as the only search widget. Configure it with container '#autocomplete', the content-specific placeholder ${JSON.stringify(placeholder)}, and an indices array containing one object whose indexName is window.${SEARCH_CONFIG_INDEX_NAME}; put item and noResults functions in its templates option, implement getURL only for an existing route, and match the item template to the entity schema. Keep insights: true and call search.start(). Do not substitute searchBox or hits, omit indices, use imports or exports, or use type="module".`
|
|
5178
5223
|
];
|
|
5179
5224
|
}
|
|
5180
5225
|
function validationInstructions(input) {
|
|
5181
5226
|
return [
|
|
5182
5227
|
"Validate the Algolia search implementation by inspecting the project source.",
|
|
5228
|
+
`Indexed content context: ${JSON.stringify(input.findings.confirmedEntities?.[0] ?? null)}.`,
|
|
5229
|
+
`The confirmed placement candidate was "${input.searchLocation ?? "unknown"}", but verify the actual mount rather than assuming the candidate was used.`,
|
|
5230
|
+
"Inspect the generated search component, its actual mount file, that file's parent layout and sibling controls, and the relevant content UI. Do not rely only on lint, type checks, or tests.",
|
|
5183
5231
|
"Confirm that the application renders exactly one search bar. Check for an old search bar and duplicate mounts.",
|
|
5184
|
-
|
|
5185
|
-
"Confirm
|
|
5232
|
+
"Confirm content-specific search is placed with the corresponding content or task controls. Shared navigation is valid only for genuinely app-wide search that fits the visual hierarchy and responsive layout without crowding primary actions.",
|
|
5233
|
+
"Confirm the input has a persistent accessible name; widget-provided combobox, popup, and item relationships and keyboard behavior are preserved; and loading, empty, and error status text is perceivable to assistive technology.",
|
|
5234
|
+
"Inspect the CSS that actually applies to the generated DOM. Confirm the results panel and the element drawing the input frame have non-transparent surfaces, the input frame is distinguishable from its surrounding background in every supported theme, text meets 4.5:1 contrast, meaningful boundaries and focus indicators meet 3:1, and no state relies on color alone.",
|
|
5235
|
+
"Confirm the search experience reflows without horizontal page scrolling at 320 CSS pixels, keeps its panel on-screen, preserves visible focus, and respects reduced-motion preferences for any added animation.",
|
|
5236
|
+
"Confirm that the search component is reachable from and fits the corresponding content task at every supported breakpoint; require app-wide reachability only when the search scope is genuinely app-wide.",
|
|
5186
5237
|
"Do not run the test suite or any test command.",
|
|
5187
5238
|
"Do not modify files. Report each implementation issue as a concrete instruction for the next implementation pass.",
|
|
5188
5239
|
`Available type-check and lint tools: ${JSON.stringify(input.findings.verification ?? [])}.`,
|
|
@@ -5190,8 +5241,10 @@ function validationInstructions(input) {
|
|
|
5190
5241
|
"The frontend has no package.json: never create one or run npm, pnpm, yarn, or bun. Validate the CDN implementation by inspecting its HTML or template and classic scripts for the search container, Algolia \u2192 InstantSearch \u2192 config \u2192 search load order, browser globals, autocomplete widget configuration, search.start(), and exact pinned CDN URLs and integrity attributes for every Algolia script or stylesheet. Source inspection is required even when the project has no automated checks. window.instantsearch.widgets.autocomplete is a real widget in the pinned InstantSearch build; do not report its use as an error, and do not fetch a remote bundle or URL to verify it."
|
|
5191
5242
|
] : [],
|
|
5192
5243
|
"Always call reportStatus with status=success after validation, even when sufficient=false.",
|
|
5193
|
-
"Set sufficient=
|
|
5194
|
-
"Set sufficient=
|
|
5244
|
+
"Set sufficient=false if the input's computed accessible name is empty or generic, including when aria-labelledby references a missing or empty element; a placeholder does not count as the label.",
|
|
5245
|
+
"Set sufficient=true when the search bar is unique and structurally complete and no placement or accessibility violation was found.",
|
|
5246
|
+
"Set sufficient=false when a required placement or accessibility property is observed to fail, the implementation is incomplete, or checks have implementation-caused failures; include concrete additionalInstructions for the next pass.",
|
|
5247
|
+
"If any required placement or accessibility property cannot be established with the available tools\u2014including contrast, keyboard behavior, 320-pixel reflow, or live-region status\u2014set sufficient=false and request concrete verification or a fix in additionalInstructions; do not let an unverified requirement pass as a manual follow-up.",
|
|
5195
5248
|
"If a check fails only in untouched code, set unrelatedFailure to a concise failure description."
|
|
5196
5249
|
];
|
|
5197
5250
|
}
|
|
@@ -5462,12 +5515,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5462
5515
|
const entities = ctx.getStepOutput(
|
|
5463
5516
|
"confirm-entities"
|
|
5464
5517
|
);
|
|
5465
|
-
const findings = {
|
|
5466
|
-
ingestionAnalysis: entities?.ingestionAnalysis ?? scan.ingestionAnalysis,
|
|
5467
|
-
searchImplementationAnalysis: scan.searchImplementationAnalysis,
|
|
5468
|
-
verification: scan.verification,
|
|
5469
|
-
confirmedEntities: entities?.confirmedEntities
|
|
5470
|
-
};
|
|
5471
5518
|
const language = {
|
|
5472
5519
|
languages: ctx.getStepOutput("confirm-language")?.languages ?? scan.languages,
|
|
5473
5520
|
frameworks: ctx.getStepOutput("confirm-framework")?.frameworks ?? scan.frameworks
|
|
@@ -5492,9 +5539,14 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5492
5539
|
if (useCases.includes("ingestion")) {
|
|
5493
5540
|
await mkdir5(join10(repoRoot, INGEST_DIR), { recursive: true });
|
|
5494
5541
|
}
|
|
5495
|
-
const
|
|
5496
|
-
|
|
5497
|
-
|
|
5542
|
+
const findings = normalizeFindingPaths({
|
|
5543
|
+
ingestionAnalysis: entities?.ingestionAnalysis ?? scan.ingestionAnalysis,
|
|
5544
|
+
searchImplementationAnalysis: entities?.searchImplementationAnalysis ?? scan.searchImplementationAnalysis,
|
|
5545
|
+
verification: scan.verification,
|
|
5546
|
+
confirmedEntities: entities?.confirmedEntities
|
|
5547
|
+
});
|
|
5548
|
+
const confirmed2 = findings.confirmedEntities;
|
|
5549
|
+
const searchLocation = findings.searchImplementationAnalysis;
|
|
5498
5550
|
let appId;
|
|
5499
5551
|
let ingestAppId;
|
|
5500
5552
|
if (useCases.includes("search")) {
|
|
@@ -5529,7 +5581,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5529
5581
|
}
|
|
5530
5582
|
}
|
|
5531
5583
|
const input = {
|
|
5532
|
-
findings
|
|
5584
|
+
findings,
|
|
5533
5585
|
confirmed: confirmed2,
|
|
5534
5586
|
searchLocation,
|
|
5535
5587
|
targetIndex,
|
|
@@ -5557,8 +5609,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5557
5609
|
[APP_ID_VAR]: ingestKeyAppId,
|
|
5558
5610
|
[API_KEY_VAR]: ingestWriteKey,
|
|
5559
5611
|
[INDEX_NAME_VAR]: targetIndex
|
|
5560
|
-
})) :
|
|
5561
|
-
const searchTools = makeToolContext(repoRoot);
|
|
5612
|
+
})) : makeToolContext(repoRoot);
|
|
5562
5613
|
async function runImplementationUseCase(currentUseCase, extraInstructions = [], isRetry = false) {
|
|
5563
5614
|
if (agentRuns > 0) ctx.recordStepExecution();
|
|
5564
5615
|
agentRuns += 1;
|
|
@@ -5572,23 +5623,29 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5572
5623
|
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
5573
5624
|
outputSchema: implementationOutputSchema,
|
|
5574
5625
|
modelProfile: isRetry ? "implementationRetry" /* implementationRetry */ : "implementation" /* implementation */,
|
|
5575
|
-
toolContext: currentUseCase === "ingestion" ? ingestionTools
|
|
5626
|
+
toolContext: currentUseCase === "ingestion" ? ingestionTools : makeToolContext(repoRoot)
|
|
5576
5627
|
});
|
|
5577
5628
|
}
|
|
5578
5629
|
async function runValidationUseCase(extraInstructions) {
|
|
5579
5630
|
if (agentRuns > 0) ctx.recordStepExecution();
|
|
5580
5631
|
agentRuns += 1;
|
|
5632
|
+
const writtenFiles = [...new Set(useWizard.getState().writtenFiles)].map(
|
|
5633
|
+
(file) => relative6(repoRoot, file)
|
|
5634
|
+
);
|
|
5581
5635
|
return runAgent({
|
|
5582
5636
|
operation: "search-validation",
|
|
5583
5637
|
instructions: buildAgentInstructions(
|
|
5584
5638
|
"validation",
|
|
5585
5639
|
input,
|
|
5586
|
-
|
|
5640
|
+
[
|
|
5641
|
+
writtenFiles.length ? `Files written during search implementation: ${JSON.stringify(writtenFiles)}. Inspect these first, then trace the actual import and render site.` : "The search agent reported no written files. Inspect the current diff and set sufficient=false if no complete mounted search implementation exists.",
|
|
5642
|
+
...extraInstructions
|
|
5643
|
+
]
|
|
5587
5644
|
),
|
|
5588
5645
|
tools: toolsForUseCase("validation"),
|
|
5589
5646
|
outputSchema: validationOutputSchema,
|
|
5590
5647
|
modelProfile: "validation" /* validation */,
|
|
5591
|
-
toolContext:
|
|
5648
|
+
toolContext: makeToolContext(repoRoot)
|
|
5592
5649
|
});
|
|
5593
5650
|
}
|
|
5594
5651
|
if (useCases.includes("ingestion")) {
|
|
@@ -5596,7 +5653,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5596
5653
|
const result = await runImplementationUseCase("ingestion");
|
|
5597
5654
|
summaries.push(formatSummary("ingestion", result.summary));
|
|
5598
5655
|
ingestCommand = result.ingestCommand;
|
|
5599
|
-
const ingestionContext = ingestionTools
|
|
5656
|
+
const ingestionContext = ingestionTools;
|
|
5600
5657
|
const executions = ingestionContext.shell.executions;
|
|
5601
5658
|
const {
|
|
5602
5659
|
run: ingestRun,
|
|
@@ -7499,7 +7556,7 @@ function delay(ms) {
|
|
|
7499
7556
|
// package.json with { type: 'json' }
|
|
7500
7557
|
var package_default2 = {
|
|
7501
7558
|
name: "@algolia/wizard",
|
|
7502
|
-
version: "0.
|
|
7559
|
+
version: "0.70.0",
|
|
7503
7560
|
description: "Magically implement Algolia functionality in your codebase",
|
|
7504
7561
|
type: "module",
|
|
7505
7562
|
engines: {
|
|
@@ -91,6 +91,40 @@ Ensure the attributes you map into the component match the entity. 'title' above
|
|
|
91
91
|
`instantsearch.js`), then `autocomplete<ProductHit>({ ... })` types `item`, `getURL`,
|
|
92
92
|
and the templates.
|
|
93
93
|
|
|
94
|
+
### Accessibility contract
|
|
95
|
+
|
|
96
|
+
The widget owns its combobox/grid/row relationships, active-option announcements, and
|
|
97
|
+
keyboard behavior. Keep its generated roles and accessible names intact; never patch its
|
|
98
|
+
DOM or add competing ARIA.
|
|
99
|
+
|
|
100
|
+
The widget renders the input and its label itself. Keep that association intact, and
|
|
101
|
+
verify that the generated label names the indexed content (`Search products`, not just
|
|
102
|
+
`Search`). A placeholder is only a hint, and the accessible name of a surrounding
|
|
103
|
+
`<search>` landmark does not name the input. If the installed widget exposes no supported
|
|
104
|
+
way to make its generated label content-specific, use its documented connector or
|
|
105
|
+
headless API to render an associated visible or visually-hidden label while preserving
|
|
106
|
+
the widget's generated input props and keyboard behavior; never patch its DOM after
|
|
107
|
+
rendering.
|
|
108
|
+
|
|
109
|
+
Use a content-specific placeholder, put the container in the app's existing named search
|
|
110
|
+
landmark or a `<search aria-label="Product search">`, and preserve any visible search
|
|
111
|
+
label already present in the layout. `translations` covers only the detached-mode
|
|
112
|
+
controls (`detachedCancelButtonText`, `detachedSearchButtonTitle`,
|
|
113
|
+
`detachedClearButtonTitle`) — set them in a non-English app.
|
|
114
|
+
|
|
115
|
+
The widget does not provide live announcements for loading, empty, or error states. Add
|
|
116
|
+
a persistent `aria-live="polite"` region driven by the current search status, results,
|
|
117
|
+
and errors, hidden with the app's visually-hidden utility if it should not show. Target
|
|
118
|
+
WCAG 2.2 AA and verify after styling that:
|
|
119
|
+
|
|
120
|
+
- input, placeholder, and result text clear 4.5:1 contrast;
|
|
121
|
+
- meaningful icons, control boundaries, and focus indicators clear 3:1 contrast;
|
|
122
|
+
- focus is visible and unobscured, and no state relies on color alone;
|
|
123
|
+
- all controls remain keyboard-operable and have 24 by 24 CSS pixel targets or enough
|
|
124
|
+
spacing around smaller targets;
|
|
125
|
+
- the experience reflows at a 320 CSS pixel viewport without horizontal page scrolling;
|
|
126
|
+
- any added animation respects `prefers-reduced-motion`.
|
|
127
|
+
|
|
94
128
|
### Styling the panel
|
|
95
129
|
|
|
96
130
|
The widget ships no CSS. Install `instantsearch.css` and import a theme once, from the
|
|
@@ -139,12 +173,15 @@ Everything else is plain CSS against the widget's classes, or `cssClasses` /
|
|
|
139
173
|
- `.ais-AutocompleteDetached*` — detached mode (below).
|
|
140
174
|
|
|
141
175
|
The theme's panel is `position: absolute; width: 100%`, so it inherits the input
|
|
142
|
-
container's width — a small input yields an unreadably narrow panel. Give it a
|
|
143
|
-
|
|
176
|
+
container's width — a small input yields an unreadably narrow panel. Give it a 320px
|
|
177
|
+
minimum when space permits, cap it to the viewport, and anchor it to the edge it opens
|
|
178
|
+
from. Give the form and panel non-transparent surfaces from the app's tokens, with the
|
|
179
|
+
form boundary distinguishable from the surrounding background in every supported theme:
|
|
144
180
|
|
|
145
181
|
```css
|
|
146
182
|
.ais-AutocompletePanel {
|
|
147
|
-
min-width: 320px;
|
|
183
|
+
min-width: min(320px, calc(100vw - 2rem));
|
|
184
|
+
max-width: calc(100vw - 2rem);
|
|
148
185
|
left: 0; /* right: 0; left: auto for an input aligned to the right */
|
|
149
186
|
}
|
|
150
187
|
```
|
|
@@ -83,6 +83,40 @@ Ensure the attributes you map into the component match the entity. 'title' above
|
|
|
83
83
|
`instantsearch.js`), then `<Autocomplete<ProductHit> ... />` types `item`, `getURL`, and
|
|
84
84
|
`itemComponent`.
|
|
85
85
|
|
|
86
|
+
### Accessibility contract
|
|
87
|
+
|
|
88
|
+
The widget owns its combobox/grid/row relationships, active-option announcements, and
|
|
89
|
+
keyboard behavior. Keep its generated roles and accessible names intact; never patch its
|
|
90
|
+
DOM or add competing ARIA.
|
|
91
|
+
|
|
92
|
+
The widget renders the input and its label itself. Keep that association intact, and
|
|
93
|
+
verify that the generated label names the indexed content (`Search products`, not just
|
|
94
|
+
`Search`). A placeholder is only a hint, and the accessible name of a surrounding
|
|
95
|
+
`<search>` landmark does not name the input. If the installed widget exposes no supported
|
|
96
|
+
way to make its generated label content-specific, use its documented connector or
|
|
97
|
+
headless API to render an associated visible or visually-hidden label while preserving
|
|
98
|
+
the widget's generated input props and keyboard behavior; never patch its DOM after
|
|
99
|
+
rendering.
|
|
100
|
+
|
|
101
|
+
Use a content-specific placeholder, put the widget in the app's existing named search
|
|
102
|
+
landmark or a `<search aria-label="Product search">`, and preserve any visible search
|
|
103
|
+
label already present in the layout. On `react-instantsearch` 7.24.0+, `translations`
|
|
104
|
+
covers the detached-mode controls (`detachedCancelButtonText`,
|
|
105
|
+
`detachedSearchButtonTitle`, `detachedClearButtonTitle`) — set them in a non-English app.
|
|
106
|
+
|
|
107
|
+
The widget does not provide live announcements for loading, empty, or error states. Add
|
|
108
|
+
a persistent `aria-live="polite"` region driven by the current search status, results,
|
|
109
|
+
and errors, hidden with the app's visually-hidden utility if it should not show. Target
|
|
110
|
+
WCAG 2.2 AA and verify after styling that:
|
|
111
|
+
|
|
112
|
+
- input, placeholder, and result text clear 4.5:1 contrast;
|
|
113
|
+
- meaningful icons, control boundaries, and focus indicators clear 3:1 contrast;
|
|
114
|
+
- focus is visible and unobscured, and no state relies on color alone;
|
|
115
|
+
- all controls remain keyboard-operable and have 24 by 24 CSS pixel targets or enough
|
|
116
|
+
spacing around smaller targets;
|
|
117
|
+
- the experience reflows at a 320 CSS pixel viewport without horizontal page scrolling;
|
|
118
|
+
- any added animation respects `prefers-reduced-motion`.
|
|
119
|
+
|
|
86
120
|
### Styling the panel
|
|
87
121
|
|
|
88
122
|
The widget ships no CSS. Install `instantsearch.css` and import a theme once, from the
|
|
@@ -131,12 +165,15 @@ Everything else is plain CSS against the widget's classes, or the `classNames` /
|
|
|
131
165
|
- `.ais-AutocompleteDetached*` — detached mode (below).
|
|
132
166
|
|
|
133
167
|
The theme's panel is `position: absolute; width: 100%`, so it inherits the input
|
|
134
|
-
container's width — a small input yields an unreadably narrow panel. Give it a
|
|
135
|
-
|
|
168
|
+
container's width — a small input yields an unreadably narrow panel. Give it a 320px
|
|
169
|
+
minimum when space permits, cap it to the viewport, and anchor it to the edge it opens
|
|
170
|
+
from. Give the form and panel non-transparent surfaces from the app's tokens, with the
|
|
171
|
+
form boundary distinguishable from the surrounding background in every supported theme:
|
|
136
172
|
|
|
137
173
|
```css
|
|
138
174
|
.ais-AutocompletePanel {
|
|
139
|
-
min-width: 320px;
|
|
175
|
+
min-width: min(320px, calc(100vw - 2rem));
|
|
176
|
+
max-width: calc(100vw - 2rem);
|
|
140
177
|
left: 0; /* right: 0; left: auto for an input aligned to the right */
|
|
141
178
|
}
|
|
142
179
|
```
|
|
@@ -37,14 +37,16 @@ each index's `sendEvent`.
|
|
|
37
37
|
>
|
|
38
38
|
<ais-autocomplete>
|
|
39
39
|
<template v-slot="{ currentRefinement, indices, refine }">
|
|
40
|
+
<label :for="inputId">Search products</label>
|
|
40
41
|
<input
|
|
42
|
+
:id="inputId"
|
|
41
43
|
type="search"
|
|
42
44
|
role="combobox"
|
|
43
45
|
aria-autocomplete="list"
|
|
44
|
-
aria-controls="
|
|
46
|
+
:aria-controls="panelId"
|
|
45
47
|
:aria-expanded="isOpen(currentRefinement)"
|
|
46
48
|
:aria-activedescendant="
|
|
47
|
-
active >= 0 ?
|
|
49
|
+
active >= 0 ? optionId(active) : undefined
|
|
48
50
|
"
|
|
49
51
|
:value="currentRefinement"
|
|
50
52
|
@focus="open = true"
|
|
@@ -54,16 +56,19 @@ each index's `sendEvent`.
|
|
|
54
56
|
@keydown.enter.prevent="select(indices, active)"
|
|
55
57
|
@keydown.esc="close"
|
|
56
58
|
/>
|
|
59
|
+
<p class="visually-hidden" aria-live="polite">
|
|
60
|
+
{{ statusMessage(currentRefinement, indices) }}
|
|
61
|
+
</p>
|
|
57
62
|
<ul
|
|
58
63
|
v-show="isOpen(currentRefinement)"
|
|
59
|
-
id="
|
|
64
|
+
:id="panelId"
|
|
60
65
|
class="search-panel"
|
|
61
66
|
role="listbox"
|
|
62
67
|
aria-label="Search results"
|
|
63
68
|
>
|
|
64
69
|
<li
|
|
65
70
|
v-for="(hit, i) in indices[0]?.hits ?? []"
|
|
66
|
-
:id="
|
|
71
|
+
:id="optionId(i)"
|
|
67
72
|
:key="hit.objectID"
|
|
68
73
|
role="option"
|
|
69
74
|
:aria-selected="i === active"
|
|
@@ -89,15 +94,28 @@ import {
|
|
|
89
94
|
ALGOLIA_INDEX_NAME,
|
|
90
95
|
} from '<the project's shared config module>'
|
|
91
96
|
|
|
97
|
+
const props = defineProps({
|
|
98
|
+
idPrefix: { type: String, required: true },
|
|
99
|
+
})
|
|
92
100
|
const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
93
101
|
const open = ref(false)
|
|
94
102
|
const active = ref(-1)
|
|
95
103
|
const containerRef = ref(null)
|
|
96
104
|
|
|
105
|
+
const inputId = `${props.idPrefix}-input`
|
|
106
|
+
const panelId = `${props.idPrefix}-panel`
|
|
107
|
+
const optionId = (index) => `${props.idPrefix}-option-${index}`
|
|
108
|
+
|
|
97
109
|
function isOpen(currentRefinement) {
|
|
98
110
|
return open.value && Boolean(currentRefinement)
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
function statusMessage(currentRefinement, indices) {
|
|
114
|
+
if (!isOpen(currentRefinement)) return ''
|
|
115
|
+
const count = indices[0]?.hits?.length ?? 0
|
|
116
|
+
return count === 0 ? 'No results' : `${count} search result${count === 1 ? '' : 's'}`
|
|
117
|
+
}
|
|
118
|
+
|
|
101
119
|
function onInput(refine, event) {
|
|
102
120
|
open.value = true
|
|
103
121
|
active.value = -1
|
|
@@ -131,6 +149,11 @@ function onFocusOut(event) {
|
|
|
131
149
|
</script>
|
|
132
150
|
```
|
|
133
151
|
|
|
152
|
+
### Match entity schema
|
|
153
|
+
|
|
154
|
+
Adapt the label, placeholder, highlighted attributes, result content, and destination to
|
|
155
|
+
the confirmed indexed entity. `products` and `name` above are examples.
|
|
156
|
+
|
|
134
157
|
Details that break accessibility if changed:
|
|
135
158
|
|
|
136
159
|
- `role="combobox"` on the `input`, not a wrapper — with `aria-controls` at the popup's
|
|
@@ -146,11 +169,42 @@ Details that break accessibility if changed:
|
|
|
146
169
|
top — it fights `@mousedown.prevent`.
|
|
147
170
|
- `.is-active` needs a visible style, not just `aria-selected` — it is a keyboard user's
|
|
148
171
|
only focus feedback, since focus never leaves the input.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
172
|
+
- Pass a stable, unique `id-prefix` whenever this component is rendered, for example
|
|
173
|
+
`id-prefix="catalog-search"`. Use a different value for each simultaneous mount so
|
|
174
|
+
labels and `aria-controls` never point at another instance.
|
|
175
|
+
|
|
176
|
+
Keep the associated label visible when the layout permits; otherwise use the app's
|
|
177
|
+
existing visually-hidden utility rather than removing it. The persistent `aria-live`
|
|
178
|
+
status above announces result counts and no-results state without moving focus; keep it
|
|
179
|
+
in the app's visually-hidden utility class (`.visually-hidden` is a placeholder for
|
|
180
|
+
whatever the project already uses) so it never shifts the layout it sits in. Surface
|
|
181
|
+
loading and errors through the same status region if the surrounding app exposes those
|
|
182
|
+
states.
|
|
183
|
+
|
|
184
|
+
Target WCAG 2.2 AA after applying the app's design tokens:
|
|
185
|
+
|
|
186
|
+
- input, placeholder, and result text clear 4.5:1 contrast;
|
|
187
|
+
- meaningful icons, control boundaries, and focus indicators clear 3:1 contrast;
|
|
188
|
+
- focus is visible and unobscured, and no state relies on color alone;
|
|
189
|
+
- controls have 24 by 24 CSS pixel targets or enough spacing around smaller targets;
|
|
190
|
+
- the experience reflows at a 320 CSS pixel viewport without horizontal page scrolling;
|
|
191
|
+
- any added animation respects `prefers-reduced-motion`.
|
|
192
|
+
|
|
193
|
+
The panel is your own markup with no widget theme to inherit. Give the input and
|
|
194
|
+
`.search-panel` non-transparent surfaces from the app's tokens, and make the input
|
|
195
|
+
boundary distinguishable from its surrounding background in every supported theme. Use
|
|
196
|
+
`position: absolute`, a 320px minimum when space permits, a viewport cap, an input-edge
|
|
197
|
+
anchor, and a scrollable maximum height:
|
|
198
|
+
|
|
199
|
+
```css
|
|
200
|
+
.search-panel {
|
|
201
|
+
min-width: min(320px, calc(100vw - 2rem));
|
|
202
|
+
max-width: calc(100vw - 2rem);
|
|
203
|
+
max-height: min(24rem, calc(100vh - 2rem));
|
|
204
|
+
overflow-y: auto;
|
|
205
|
+
left: 0; /* right: 0; left: auto for an input aligned to the right */
|
|
206
|
+
}
|
|
207
|
+
```
|
|
154
208
|
|
|
155
209
|
`indices` supports federated search across indices; this project has one, so `indices[0]`
|
|
156
210
|
is always the one to read.
|