@algolia/wizard 0.69.0 → 0.71.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.69.0",
1972
+ version: "0.71.0",
1973
1973
  description: "Magically implement Algolia functionality in your codebase",
1974
1974
  type: "module",
1975
1975
  engines: {
@@ -3440,6 +3440,12 @@ var DEFAULT_TOOL_LIMITS = {
3440
3440
  match: 100,
3441
3441
  shell: 50
3442
3442
  };
3443
+ var EMPTY_TOOL_COUNTS = {
3444
+ list: 0,
3445
+ search: 0,
3446
+ read: 0,
3447
+ shell: 0
3448
+ };
3443
3449
  var DEFAULT_SHELL_TIMEOUT_MS = 10 * 60 * 1e3;
3444
3450
  async function refuseByDefault() {
3445
3451
  return "reject";
@@ -3458,11 +3464,14 @@ function createToolContext(limits = DEFAULT_TOOL_LIMITS, cwd = process.cwd(), sh
3458
3464
  root: cwd,
3459
3465
  cwd,
3460
3466
  limits: { ...limits },
3461
- counts: { list: 0, search: 0, read: 0, shell: 0 },
3467
+ counts: { ...EMPTY_TOOL_COUNTS },
3462
3468
  shell: shell2,
3463
3469
  reviewed: []
3464
3470
  };
3465
3471
  }
3472
+ function resetToolCounts(ctx) {
3473
+ Object.assign(ctx.counts, EMPTY_TOOL_COUNTS);
3474
+ }
3466
3475
 
3467
3476
  // src/lib/tools/utils/runShell.ts
3468
3477
  var SIGKILL_DELAY_MS = 5e3;
@@ -4128,10 +4137,21 @@ var MISSING_REPORT_STATUS_ERROR_MESSAGE = "Agent finished without calling report
4128
4137
  var MISSING_REPORT_USER_MESSAGE = "This step ran into a problem finishing. Run the wizard again to retry it.";
4129
4138
  var REPORT_STATUS_RETRIES = 2;
4130
4139
  var ATTEMPT_NUMBER_OFFSET = 1;
4140
+ var INITIAL_ATTEMPT = 0;
4131
4141
  var PROVIDER_ERROR_USER_MESSAGE = "The AI service had trouble responding. Run the wizard again to retry this step.";
4142
+ var MissingReportError = class extends Error {
4143
+ atStepLimit;
4144
+ constructor(atStepLimit) {
4145
+ super(MISSING_REPORT_STATUS_ERROR_MESSAGE);
4146
+ this.atStepLimit = atStepLimit;
4147
+ }
4148
+ };
4149
+ function isMissingReportRetry(kind) {
4150
+ return kind === "missingReport" || kind === "missingReportAtStepLimit";
4151
+ }
4132
4152
  function retryKind(err) {
4133
- if (err instanceof Error && err.message === MISSING_REPORT_STATUS_ERROR_MESSAGE) {
4134
- return "missingReport";
4153
+ if (err instanceof MissingReportError) {
4154
+ return err.atStepLimit ? "missingReportAtStepLimit" : "missingReport";
4135
4155
  }
4136
4156
  if (NoOutputGeneratedError.isInstance(err)) return "transientProvider";
4137
4157
  if (APICallError.isInstance(err) && err.isRetryable) {
@@ -4140,37 +4160,42 @@ function retryKind(err) {
4140
4160
  return null;
4141
4161
  }
4142
4162
  function exhaustedError(kind, err) {
4143
- return kind === "missingReport" ? new Error(MISSING_REPORT_USER_MESSAGE) : new Error(PROVIDER_ERROR_USER_MESSAGE, { cause: err });
4163
+ return isMissingReportRetry(kind) ? new Error(MISSING_REPORT_USER_MESSAGE) : new Error(PROVIDER_ERROR_USER_MESSAGE, { cause: err });
4144
4164
  }
4145
4165
  async function runAgent(req) {
4146
- for (let attempt = 0; attempt <= REPORT_STATUS_RETRIES; attempt++) {
4166
+ let retryReason = null;
4167
+ for (let attempt = INITIAL_ATTEMPT; attempt <= REPORT_STATUS_RETRIES; attempt++) {
4147
4168
  try {
4148
- return await runAgentAttempt(req, attempt);
4169
+ return await runAgentAttempt(req, attempt, retryReason);
4149
4170
  } catch (err) {
4150
4171
  const kind = retryKind(err);
4151
4172
  if (!kind) throw err;
4152
4173
  if (attempt === REPORT_STATUS_RETRIES) throw exhaustedError(kind, err);
4174
+ retryReason = kind === "transientProvider" && isMissingReportRetry(retryReason) ? retryReason : kind;
4153
4175
  logger.warn(
4154
4176
  { attempt: attempt + 1, err },
4155
- kind === "missingReport" ? "retrying runAgent after missing reportStatus" : "retrying runAgent after a transient provider error"
4177
+ isMissingReportRetry(kind) ? "retrying runAgent after missing reportStatus" : "retrying runAgent after a transient provider error"
4156
4178
  );
4157
4179
  }
4158
4180
  }
4159
4181
  throw new Error("unreachable");
4160
4182
  }
4161
- async function runAgentAttempt(req, attempt) {
4183
+ async function runAgentAttempt(req, attempt, retryReason) {
4162
4184
  const start = Date.now();
4163
4185
  const profileName = req.modelProfile ?? "implementation" /* implementation */;
4164
4186
  const profile = getModelProfile(profileName);
4165
- const modelOptions = providerOptionsForProfile(profileName);
4187
+ const profileOptions = providerOptionsForProfile(profileName);
4188
+ const forceReport = retryReason === "missingReport";
4189
+ const modelOptions = forceReport ? { thinking: { type: "disabled" } } : profileOptions;
4166
4190
  logger.info(
4167
4191
  {
4168
4192
  startedAt: new Date(start).toISOString(),
4169
4193
  operation: req.operation,
4170
4194
  profile: profileName,
4171
4195
  model: profile.model,
4172
- effort: profile.effort,
4173
- thinking: profile.thinking
4196
+ effort: forceReport ? void 0 : profile.effort,
4197
+ thinking: forceReport ? "disabled" : profile.thinking,
4198
+ retryReason
4174
4199
  },
4175
4200
  "runAgent started"
4176
4201
  );
@@ -4184,6 +4209,7 @@ async function runAgentAttempt(req, attempt) {
4184
4209
  fetch: proxyFetch
4185
4210
  });
4186
4211
  const toolContext = req.toolContext ?? createToolContext();
4212
+ if (attempt > INITIAL_ATTEMPT) resetToolCounts(toolContext);
4187
4213
  const readTools = ["readFile", "searchFiles", "listFiles"];
4188
4214
  const hasReadTools = !req.tools || req.tools.some((t) => readTools.includes(t));
4189
4215
  const instructions = [
@@ -4195,8 +4221,14 @@ async function runAgentAttempt(req, attempt) {
4195
4221
  // Last so a retry does not bust the cached tools+system prefix from
4196
4222
  // the first attempt. Inspect-and-continue, not start over: implement
4197
4223
  // shares the worktree and toolContext across attempts.
4198
- ...attempt > 0 ? [
4199
- "This run is a retry after a previous attempt that did not finish. Files, shell commands, or ingestion may already have been applied in this workspace \u2014 inspect what is already there and continue from it rather than repeating that work."
4224
+ ...retryReason === "missingReport" ? hasReadTools ? [
4225
+ "The previous attempt ended without reportStatus. Do not repeat side effects. Use the available read tools to inspect existing work if needed, then call reportStatus."
4226
+ ] : [
4227
+ "The previous attempt ended without reportStatus. Do not repeat side effects. Call reportStatus now."
4228
+ ] : retryReason === "missingReportAtStepLimit" ? [
4229
+ "The previous attempt reached its step limit without reportStatus and may be incomplete. Inspect existing work, finish only the remaining work, avoid repeated side effects, and call reportStatus."
4230
+ ] : attempt > 0 ? [
4231
+ "This run is a retry after a transient AI service error. Files, shell commands, or ingestion may already have been applied in this workspace \u2014 inspect what is already there and continue from it rather than repeating that work."
4200
4232
  ] : []
4201
4233
  ];
4202
4234
  const agent = new ToolLoopAgent({
@@ -4304,7 +4336,8 @@ async function runAgentAttempt(req, attempt) {
4304
4336
  },
4305
4337
  "Agent finished without calling reportStatus"
4306
4338
  );
4307
- throw new Error(MISSING_REPORT_STATUS_ERROR_MESSAGE);
4339
+ const incomplete = steps.length >= profile.maxSteps || lastStep?.finishReason !== "stop";
4340
+ throw new MissingReportError(incomplete);
4308
4341
  }
4309
4342
  const result = report.output;
4310
4343
  if (result.status !== "success") {
@@ -4426,10 +4459,14 @@ var MODE_CONFIG = {
4426
4459
  },
4427
4460
  searchImplementation: {
4428
4461
  instructions: [
4429
- "Analyze the codebase to determine the single best location to add search UI functionality.",
4430
- "Prefer a shared, always-rendered layout location (e.g. a header or navigation component) so search is reachable across the app.",
4462
+ "Analyze the codebase to determine the single best existing file in which to mount search UI.",
4463
+ "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.",
4464
+ "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.",
4465
+ "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.",
4466
+ "Account for available space, alignment, and behavior at the app's existing responsive breakpoints so the control will not crowd or displace primary navigation.",
4467
+ "Prefer an existing search control with the same content scope when one can be replaced without removing unrelated filters or navigation.",
4431
4468
  "Return one file path as searchImplementationAnalysis (e.g. /layouts/header.tsx).",
4432
- 'Use as few tools as possible, but do not guess. If you cannot find a clear location, say "unknown".',
4469
+ '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".',
4433
4470
  "Ignore directories that may be related to testing, like `/fixtures`, `/tests`, etc",
4434
4471
  "When done, call reportStatus"
4435
4472
  ],
@@ -4684,7 +4721,8 @@ import z26 from "zod";
4684
4721
  var confirmEntitiesSchema = z26.object({
4685
4722
  // Final detection — the focused re-run may supersede project-scan's.
4686
4723
  ingestionAnalysis: ingestionAnalysisSchema.shape.ingestionAnalysis.optional(),
4687
- confirmedEntities: confirmedEntitiesFieldSchema
4724
+ confirmedEntities: confirmedEntitiesFieldSchema,
4725
+ searchImplementationAnalysis: searchImplementationAnalysisSchema.shape.searchImplementationAnalysis.optional()
4688
4726
  });
4689
4727
  var SUGGEST_ENTITY = "Suggest a different entity\u2026";
4690
4728
  function usedDetectedBackend(detected, confirmed2) {
@@ -4807,7 +4845,26 @@ async function confirmEntities(ctx) {
4807
4845
  attribute_count: summary[0]?.attributes.length ?? 0,
4808
4846
  used_detected_back_end: usedDetectedBackend(scan.languages, languages)
4809
4847
  });
4810
- return { ingestionAnalysis: entities, confirmedEntities: confirmed2 };
4848
+ let searchImplementationAnalysis = scan.searchImplementationAnalysis;
4849
+ try {
4850
+ const placement = await runAnalysis("searchImplementation", [
4851
+ `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.`
4852
+ ]);
4853
+ searchImplementationAnalysis = placement.searchImplementationAnalysis;
4854
+ } catch (err) {
4855
+ logger.warn({ err }, "confirmEntities: placement refresh failed");
4856
+ }
4857
+ if (searchImplementationAnalysis) {
4858
+ ctx.setUserInput(
4859
+ "searchImplementationAnalysis",
4860
+ searchImplementationAnalysis
4861
+ );
4862
+ }
4863
+ return {
4864
+ ingestionAnalysis: entities,
4865
+ confirmedEntities: confirmed2,
4866
+ searchImplementationAnalysis
4867
+ };
4811
4868
  }
4812
4869
  }
4813
4870
 
@@ -5133,19 +5190,37 @@ function ingestionInstructions(input) {
5133
5190
  ];
5134
5191
  }
5135
5192
  function searchInstructions(input) {
5136
- const entity = input.findings.confirmedEntities ? input.findings.confirmedEntities[0].name : null;
5137
- const attributes = input.findings.confirmedEntities ? input.findings.confirmedEntities[0].attributes : null;
5138
- const entitySchemaMessage = entity && attributes ? `The following entity schema should be used to build the UI: ${JSON.stringify({ entity, attributes })}` : null;
5193
+ const indexedEntity = input.findings.confirmedEntities?.[0];
5194
+ const indexedContentMessage = indexedEntity ? `Indexed content context: ${JSON.stringify({
5195
+ entity: indexedEntity.name,
5196
+ attributes: indexedEntity.attributes,
5197
+ sourcePaths: indexedEntity.paths
5198
+ })}. Use the source and the existing UI that displays this entity to understand the search task, result content, and appropriate scope.` : null;
5199
+ 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.";
5200
+ 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.";
5201
+ 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.";
5202
+ const accessibilityInstructions = [
5203
+ "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.",
5204
+ "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.",
5205
+ "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.",
5206
+ "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.",
5207
+ "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."
5208
+ ];
5139
5209
  const packageManagedInstructions = input.frontendHasPackageJson ? packageSearchInstructions(input) : cdnSearchInstructions(input);
5140
5210
  return [
5141
5211
  "Implement an in-app Algolia search experience.",
5142
- entitySchemaMessage ?? "",
5212
+ indexedContentMessage ?? "",
5143
5213
  `Build the search UI for ${input.searchUiTarget}.`,
5144
5214
  "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.",
5215
+ mountCandidateMessage,
5216
+ placementMessage,
5217
+ replacementMessage,
5218
+ "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.",
5145
5219
  ...packageManagedInstructions,
5146
- "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.",
5147
- "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.",
5148
- "Match the styles of the application as closely as possible.",
5220
+ ...accessibilityInstructions,
5221
+ "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.",
5222
+ "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.",
5223
+ "Match the application styles closely, including light, dark, and high-contrast themes the project already supports.",
5149
5224
  "The summary should be extremely concise; do not mention manual testing steps."
5150
5225
  ];
5151
5226
  }
@@ -5159,8 +5234,6 @@ function packageSearchInstructions(input) {
5159
5234
  "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."
5160
5235
  ],
5161
5236
  `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.`,
5162
- `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.`,
5163
- "If a search box already exists, replace its usage with an import and render of your new component; remove the old implementation.",
5164
5237
  "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.",
5165
5238
  "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.",
5166
5239
  `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).`,
@@ -5171,21 +5244,29 @@ function packageSearchInstructions(input) {
5171
5244
  ];
5172
5245
  }
5173
5246
  function cdnSearchInstructions(input) {
5247
+ const entityName = input.findings.confirmedEntities?.[0]?.name;
5248
+ const placeholder = entityName ? `Search ${entityName} records...` : "Search indexed content...";
5174
5249
  return [
5175
5250
  `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.`,
5176
5251
  `Set window.${SEARCH_CONFIG_APP_ID} to "${input.appId}" and window.${SEARCH_CONFIG_INDEX_NAME} to "${input.targetIndex}".`,
5177
5252
  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.`,
5178
5253
  '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>.',
5179
5254
  '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.',
5180
- `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".`
5255
+ `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".`
5181
5256
  ];
5182
5257
  }
5183
5258
  function validationInstructions(input) {
5184
5259
  return [
5185
5260
  "Validate the Algolia search implementation by inspecting the project source.",
5261
+ `Indexed content context: ${JSON.stringify(input.findings.confirmedEntities?.[0] ?? null)}.`,
5262
+ `The confirmed placement candidate was "${input.searchLocation ?? "unknown"}", but verify the actual mount rather than assuming the candidate was used.`,
5263
+ "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.",
5186
5264
  "Confirm that the application renders exactly one search bar. Check for an old search bar and duplicate mounts.",
5187
- `Confirm that the search bar is mounted in ${input.searchLocation ? `"${input.searchLocation}"` : "the best always-rendered shared layout location"}.`,
5188
- "Confirm that the search component is reachable across the application and fits the surrounding layout.",
5265
+ "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.",
5266
+ "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.",
5267
+ "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.",
5268
+ "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.",
5269
+ "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.",
5189
5270
  "Do not run the test suite or any test command.",
5190
5271
  "Do not modify files. Report each implementation issue as a concrete instruction for the next implementation pass.",
5191
5272
  `Available type-check and lint tools: ${JSON.stringify(input.findings.verification ?? [])}.`,
@@ -5193,8 +5274,10 @@ function validationInstructions(input) {
5193
5274
  "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."
5194
5275
  ] : [],
5195
5276
  "Always call reportStatus with status=success after validation, even when sufficient=false.",
5196
- "Set sufficient=true only when the search bar is unique, correctly placed, and structurally complete.",
5197
- "Set sufficient=false for an implementation issue. Include concrete additionalInstructions for the next pass.",
5277
+ "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.",
5278
+ "Set sufficient=true when the search bar is unique and structurally complete and no placement or accessibility violation was found.",
5279
+ "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.",
5280
+ "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.",
5198
5281
  "If a check fails only in untouched code, set unrelatedFailure to a concise failure description."
5199
5282
  ];
5200
5283
  }
@@ -5465,12 +5548,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
5465
5548
  const entities = ctx.getStepOutput(
5466
5549
  "confirm-entities"
5467
5550
  );
5468
- const findings = {
5469
- ingestionAnalysis: entities?.ingestionAnalysis ?? scan.ingestionAnalysis,
5470
- searchImplementationAnalysis: scan.searchImplementationAnalysis,
5471
- verification: scan.verification,
5472
- confirmedEntities: entities?.confirmedEntities
5473
- };
5474
5551
  const language = {
5475
5552
  languages: ctx.getStepOutput("confirm-language")?.languages ?? scan.languages,
5476
5553
  frameworks: ctx.getStepOutput("confirm-framework")?.frameworks ?? scan.frameworks
@@ -5495,9 +5572,14 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
5495
5572
  if (useCases.includes("ingestion")) {
5496
5573
  await mkdir5(join10(repoRoot, INGEST_DIR), { recursive: true });
5497
5574
  }
5498
- const normalized = normalizeFindingPaths(findings);
5499
- const confirmed2 = normalized.confirmedEntities;
5500
- const searchLocation = normalized.searchImplementationAnalysis;
5575
+ const findings = normalizeFindingPaths({
5576
+ ingestionAnalysis: entities?.ingestionAnalysis ?? scan.ingestionAnalysis,
5577
+ searchImplementationAnalysis: entities?.searchImplementationAnalysis ?? scan.searchImplementationAnalysis,
5578
+ verification: scan.verification,
5579
+ confirmedEntities: entities?.confirmedEntities
5580
+ });
5581
+ const confirmed2 = findings.confirmedEntities;
5582
+ const searchLocation = findings.searchImplementationAnalysis;
5501
5583
  let appId;
5502
5584
  let ingestAppId;
5503
5585
  if (useCases.includes("search")) {
@@ -5532,7 +5614,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
5532
5614
  }
5533
5615
  }
5534
5616
  const input = {
5535
- findings: normalized,
5617
+ findings,
5536
5618
  confirmed: confirmed2,
5537
5619
  searchLocation,
5538
5620
  targetIndex,
@@ -5560,8 +5642,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
5560
5642
  [APP_ID_VAR]: ingestKeyAppId,
5561
5643
  [API_KEY_VAR]: ingestWriteKey,
5562
5644
  [INDEX_NAME_VAR]: targetIndex
5563
- })) : void 0;
5564
- const searchTools = makeToolContext(repoRoot);
5645
+ })) : makeToolContext(repoRoot);
5565
5646
  async function runImplementationUseCase(currentUseCase, extraInstructions = [], isRetry = false) {
5566
5647
  if (agentRuns > 0) ctx.recordStepExecution();
5567
5648
  agentRuns += 1;
@@ -5575,23 +5656,29 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
5575
5656
  tools: toolsForUseCase(currentUseCase, input.ingestionSource),
5576
5657
  outputSchema: implementationOutputSchema,
5577
5658
  modelProfile: isRetry ? "implementationRetry" /* implementationRetry */ : "implementation" /* implementation */,
5578
- toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
5659
+ toolContext: currentUseCase === "ingestion" ? ingestionTools : makeToolContext(repoRoot)
5579
5660
  });
5580
5661
  }
5581
5662
  async function runValidationUseCase(extraInstructions) {
5582
5663
  if (agentRuns > 0) ctx.recordStepExecution();
5583
5664
  agentRuns += 1;
5665
+ const writtenFiles = [...new Set(useWizard.getState().writtenFiles)].map(
5666
+ (file) => relative6(repoRoot, file)
5667
+ );
5584
5668
  return runAgent({
5585
5669
  operation: "search-validation",
5586
5670
  instructions: buildAgentInstructions(
5587
5671
  "validation",
5588
5672
  input,
5589
- extraInstructions
5673
+ [
5674
+ 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.",
5675
+ ...extraInstructions
5676
+ ]
5590
5677
  ),
5591
5678
  tools: toolsForUseCase("validation"),
5592
5679
  outputSchema: validationOutputSchema,
5593
5680
  modelProfile: "validation" /* validation */,
5594
- toolContext: searchTools
5681
+ toolContext: makeToolContext(repoRoot)
5595
5682
  });
5596
5683
  }
5597
5684
  if (useCases.includes("ingestion")) {
@@ -5599,7 +5686,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
5599
5686
  const result = await runImplementationUseCase("ingestion");
5600
5687
  summaries.push(formatSummary("ingestion", result.summary));
5601
5688
  ingestCommand = result.ingestCommand;
5602
- const ingestionContext = ingestionTools ?? searchTools;
5689
+ const ingestionContext = ingestionTools;
5603
5690
  const executions = ingestionContext.shell.executions;
5604
5691
  const {
5605
5692
  run: ingestRun,
@@ -7502,7 +7589,7 @@ function delay(ms) {
7502
7589
  // package.json with { type: 'json' }
7503
7590
  var package_default2 = {
7504
7591
  name: "@algolia/wizard",
7505
- version: "0.69.0",
7592
+ version: "0.71.0",
7506
7593
  description: "Magically implement Algolia functionality in your codebase",
7507
7594
  type: "module",
7508
7595
  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.69.0",
3
+ "version": "0.71.0",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {