@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.68.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:${userId}`;
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
- try {
2879
- await deletePassword(SERVICE, account(userId));
2880
- } catch (err) {
2881
- const message = err.message;
2882
- if (/not found/i.test(message)) return;
2883
- logger.warn(
2884
- { err: message, userId },
2885
- "could not delete the API keys from the keychain"
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 createHash("sha256").update(content).digest("hex");
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 location to add search UI functionality.",
4427
- "Prefer a shared, always-rendered layout location (e.g. a header or navigation component) so search is reachable across the app.",
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
- 'Use as few tools as possible, but do not guess. If you cannot find a clear location, say "unknown".',
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
- return { ingestionAnalysis: entities, confirmedEntities: confirmed2 };
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 entity = input.findings.confirmedEntities ? input.findings.confirmedEntities[0].name : null;
5134
- const attributes = input.findings.confirmedEntities ? input.findings.confirmedEntities[0].attributes : null;
5135
- const entitySchemaMessage = entity && attributes ? `The following entity schema should be used to build the UI: ${JSON.stringify({ entity, attributes })}` : null;
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
- entitySchemaMessage ?? "",
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
- "Meet WCAG AA contrast (4.5:1 body text, 3:1 large text/icons) between the panel's text and its own background, and give the input and the active result a focus indicator visible against whatever sits behind it. Style the panel through the widget's class and CSS-variable overrides (or, when hand-built, the app's existing theme tokens) \u2014 never assume a light surface or reuse the surrounding page's colors unchanged inside the panel.",
5144
- "The panel takes its width from the input by default, so a small input makes it unreadably narrow: give it a min-width of 320px independent of the input, anchored to the input edge it opens from so widening does not push it off-screen.",
5145
- "Match the styles of the application as closely as possible.",
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 'Search...', 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".`
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
- `Confirm that the search bar is mounted in ${input.searchLocation ? `"${input.searchLocation}"` : "the best always-rendered shared layout location"}.`,
5185
- "Confirm that the search component is reachable across the application and fits the surrounding layout.",
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=true only when the search bar is unique, correctly placed, and structurally complete.",
5194
- "Set sufficient=false for an implementation issue. Include concrete additionalInstructions for the next pass.",
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 normalized = normalizeFindingPaths(findings);
5496
- const confirmed2 = normalized.confirmedEntities;
5497
- const searchLocation = normalized.searchImplementationAnalysis;
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: normalized,
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
- })) : void 0;
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 ?? searchTools : searchTools
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
- extraInstructions
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: searchTools
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 ?? searchTools;
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.68.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 min-width
143
- and anchor it to the edge it opens from, or it grows off-screen:
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 min-width
135
- and anchor it to the edge it opens from, or it grows off-screen:
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="search-panel"
46
+ :aria-controls="panelId"
45
47
  :aria-expanded="isOpen(currentRefinement)"
46
48
  :aria-activedescendant="
47
- active >= 0 ? `search-option-${active}` : undefined
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="search-panel"
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="`search-option-${i}`"
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
- The panel is your own markup with no widget theme to inherit: give `.search-panel` a
151
- background from the app's theme tokens (never transparent over page content), text
152
- clearing 4.5:1 against it, `position: absolute` with `min-width: 320px` anchored to the
153
- input edge it opens from, and a `max-height` with `overflow-y: auto`.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.68.0",
3
+ "version": "0.70.0",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {