mcp-recipes 1.0.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/CHANGELOG.md +147 -0
- package/LICENSE +21 -0
- package/README.md +684 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3037 -0
- package/dist/index.js.map +1 -0
- package/dist/language-BgfmjwtF.d.ts +14 -0
- package/dist/recipe/scale.d.ts +86 -0
- package/dist/recipe/scale.js +1367 -0
- package/dist/recipe/scale.js.map +1 -0
- package/dist/sources/client.d.ts +448 -0
- package/dist/sources/client.js +697 -0
- package/dist/sources/client.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/server.ts","../src/version.ts","../src/config.ts","../src/errors.ts","../src/sources/adapter.ts","../src/sources/ids.ts","../src/sources/registry.ts","../src/sources/cookbook.ts","../src/sources/marmiton.ts","../src/sources/client.ts","../src/tools/compareRecipes.ts","../src/tools/recipeView.ts","../src/recipe/language.ts","../src/recipe/units.ts","../src/recipe/quantity.ts","../src/recipe/scale.ts","../src/tools/shared.ts","../src/tools/arguments.ts","../src/tools/getRecipe.ts","../src/tools/scaleIngredients.ts","../src/tools/searchRecipes.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point.\n *\n * stdout carries the protocol and nothing else: every diagnostic goes to\n * stderr, because a stray line on stdout corrupts the session for the client.\n */\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n const shutdown = async () => {\n await server.close().catch(() => undefined);\n process.exit(0);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`[mcp-recipes] fatal: ${message}\\n`);\n process.exit(1);\n});\n","/**\n * Wiring: one client over the registered sources, four tools, and the guidance\n * a model reads before using any of them.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { RecipesClient } from \"./sources/client.js\";\nimport {\n compareRecipesDescription,\n compareRecipesInput,\n compareRecipesOutput,\n runCompareRecipes,\n} from \"./tools/compareRecipes.js\";\nimport type { CompareRecipesArgs } from \"./tools/compareRecipes.js\";\nimport {\n getRecipeDescription,\n getRecipeInput,\n getRecipeOutput,\n runGetRecipe,\n} from \"./tools/getRecipe.js\";\nimport type { GetRecipeArgs } from \"./tools/getRecipe.js\";\nimport {\n runScaleIngredients,\n scaleIngredientsDescription,\n scaleIngredientsInput,\n scaleIngredientsOutput,\n} from \"./tools/scaleIngredients.js\";\nimport type { ScaleIngredientsArgs } from \"./tools/scaleIngredients.js\";\nimport {\n runSearchRecipes,\n searchRecipesDescription,\n searchRecipesInput,\n searchRecipesOutput,\n} from \"./tools/searchRecipes.js\";\nimport type { SearchRecipesArgs } from \"./tools/searchRecipes.js\";\nimport { PKG_VERSION } from \"./version.js\";\n\nexport interface CreateServerOptions {\n config?: Config;\n logger?: Logger;\n client?: RecipesClient;\n}\n\n/** Nothing here writes, uploads or deletes; every tool only reads. */\nconst READ_ONLY = {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n} as const;\n\n/**\n * The guidance a model reads before choosing a tool.\n *\n * The sources are named from the registry rather than written into the prose,\n * so the sentence stays true when the registry grows. What it says about them\n * holds for any corpus this server reads.\n */\nexport function buildInstructions(\n sources: Array<{ name: string; language: string }>,\n): string {\n const named = sources.map((source) => `${source.name} (${source.language})`).join(\", \");\n\n return [\n `Tools for recipes, reading ${sources.length} sources: ${named}. No API key and no account are needed.`,\n \"search_recipes asks every source at once and returns one list; every id it hands back names the source it came from, and get_recipe routes on that.\",\n \"Use compare_recipes when the question is how a dish differs between traditions: it reads each source's closest version and can rescale them all to the same number of servings so the lists stand comparison.\",\n \"Do not rescale quantities yourself. get_recipe and scale_ingredients keep an egg whole, halve what pours or cuts, move a small measurement to a smaller unit before rounding it, and flag what cannot be scaled, which is what stops answers like '2.4 eggs'.\",\n \"scale_ingredients works offline on a list from anywhere and reads French and English lines in one call, each in its own language.\",\n \"A source that fails is named as a source that failed. An answer holding rows from some sources is never evidence about what the others hold, and 'per_source' says which was which.\",\n \"The sources share no scale. Their result counts count different things and are never added; some carry reader ratings and some have no author and no rating by nature, so rows are interleaved rather than ranked.\",\n \"Nothing is converted between measuring systems, and a yield is repeated in the source's own words, because '4 à 6 personnes' and '4 personnes' are different claims. A page stating no yield cannot be rescaled and says so.\",\n \"Every result carries a url. Credit the source you took a recipe from and link it; some publish under a licence that requires it, and one that states no terms at all has not granted permission.\",\n ].join(\" \");\n}\n\n/** The guidance as it reads for the sources this build registers. */\nexport const INSTRUCTIONS = buildInstructions([\n { name: \"Marmiton\", language: \"fr\" },\n { name: \"Wikibooks Cookbook\", language: \"en\" },\n]);\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const config = options.config ?? loadConfig();\n const logger = options.logger ?? createLogger(config.logLevel);\n const client = options.client ?? new RecipesClient({ config, logger });\n\n const server = new McpServer(\n { name: \"mcp-recipes\", version: PKG_VERSION },\n { instructions: buildInstructions(client.profiles) },\n );\n\n server.registerTool(\n \"search_recipes\",\n {\n title: \"Search every recipe source at once\",\n description: searchRecipesDescription,\n inputSchema: searchRecipesInput,\n outputSchema: searchRecipesOutput,\n annotations: READ_ONLY,\n },\n async (args) => runSearchRecipes(client, args as SearchRecipesArgs),\n );\n\n server.registerTool(\n \"get_recipe\",\n {\n title: \"Read one recipe, optionally rescaled\",\n description: getRecipeDescription,\n inputSchema: getRecipeInput,\n outputSchema: getRecipeOutput,\n annotations: READ_ONLY,\n },\n async (args) => runGetRecipe(client, args as GetRecipeArgs),\n );\n\n server.registerTool(\n \"scale_ingredients\",\n {\n title: \"Scale an ingredient list, in either language\",\n description: scaleIngredientsDescription,\n inputSchema: scaleIngredientsInput,\n outputSchema: scaleIngredientsOutput,\n annotations: {\n ...READ_ONLY,\n // Nothing is fetched, so this tool reaches nothing outside itself.\n openWorldHint: false,\n },\n },\n async (args) => runScaleIngredients(args as ScaleIngredientsArgs),\n );\n\n server.registerTool(\n \"compare_recipes\",\n {\n title: \"One dish, as each source writes it\",\n description: compareRecipesDescription,\n inputSchema: compareRecipesInput,\n outputSchema: compareRecipesOutput,\n annotations: READ_ONLY,\n },\n async (args) => runCompareRecipes(client, args as CompareRecipesArgs),\n );\n\n logger.info(\n `ready: ${client.profiles.length} sources (${client.profiles.map((source) => source.id).join(\", \")}), user-agent=\"${client.userAgent}\", ${client.intervalMs}ms between requests to one source`,\n );\n\n return server;\n}\n","/**\n * Kept in step with the version in package.json. The User-Agent embeds it, so\n * the sites this server reads can tell which build is calling from their own\n * logs.\n */\nexport const PKG_VERSION = \"1.0.0\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-recipes\";\nexport const ISSUES_URL = \"https://github.com/smeet666/mcp-recipes/issues\";\n","/**\n * Settings, read from the environment.\n *\n * A value that cannot be read warns and falls back rather than stopping the\n * server: a typo in one variable should not take away every tool. Warnings go\n * to stderr, because stdout carries the protocol and anything written there\n * corrupts the session.\n *\n * One setting governs every source. Each is read through its own client, and\n * each of those keeps a pacing floor of its own that nothing here can lower, so\n * a value set here can only make this server more patient than it already is.\n */\n\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport const LOG_LEVELS = [\"silent\", \"error\", \"info\", \"debug\"] as const;\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\n/**\n * Spacing between two requests to the same site.\n *\n * No source publishes a ceiling for a client like this one, so the number is\n * self-imposed politeness: a second between requests is far below what a person\n * browsing one of these sites generates, and the answers are cached, so a\n * conversation that circles back to a recipe costs no traffic at all.\n */\nexport const DEFAULT_INTERVAL_MS = 1000;\n/**\n * Configuration can slow the server down. It cannot take the spacing below this\n * floor, whether the setting arrives through the environment or through a\n * configuration object handed to the published client entry point.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n/** Beyond this a request would look hung rather than paced. */\nexport const MAX_ALLOWED_INTERVAL_MS = 60_000;\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\nexport const DEFAULT_USER_AGENT = `mcp-recipes/${PKG_VERSION} (+${REPO_URL})`;\n\nexport interface Logger {\n debug(message: string): void;\n info(message: string): void;\n warn(message: string): void;\n error(message: string): void;\n}\n\nexport function createLogger(level: LogLevel): Logger {\n const rank = LOG_LEVELS.indexOf(level);\n const write = (at: LogLevel, message: string) => {\n if (rank === 0 || LOG_LEVELS.indexOf(at) > rank) return;\n process.stderr.write(`[mcp-recipes] ${at}: ${message}\\n`);\n };\n return {\n debug: (m) => write(\"debug\", m),\n info: (m) => write(\"info\", m),\n // A warning goes out at the error level so it survives the default\n // setting: a caller has to know that a site was left out of an answer.\n warn: (m) => write(\"error\", m),\n error: (m) => write(\"error\", m),\n };\n}\n\nfunction readInteger(\n env: NodeJS.ProcessEnv,\n name: string,\n fallback: number,\n min: number,\n max: number,\n): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") return fallback;\n\n const value = Number(raw);\n if (!Number.isFinite(value) || !Number.isInteger(value)) {\n process.stderr.write(\n `[mcp-recipes] error: ${name}=\"${raw}\" is not a whole number; using ${fallback}.\\n`,\n );\n return fallback;\n }\n if (value < min || value > max) {\n // Clamping silently would let a caller believe a setting took effect when\n // the opposite is true, so the refusal is stated and the default stands.\n process.stderr.write(\n `[mcp-recipes] error: ${name}=${value} is outside ${min}..${max}; using ${fallback}.\\n`,\n );\n return fallback;\n }\n return value;\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const level = env.RECIPES_LOG_LEVEL as LogLevel | undefined;\n const logLevel = level && LOG_LEVELS.includes(level) ? level : \"error\";\n if (level && !LOG_LEVELS.includes(level)) {\n process.stderr.write(\n `[mcp-recipes] error: RECIPES_LOG_LEVEL=\"${level}\" is not one of ${LOG_LEVELS.join(\", \")}; using error.\\n`,\n );\n }\n\n const custom = env.RECIPES_USER_AGENT?.trim();\n\n return {\n // A caller who wants to be recognised may say who they are, and the\n // contact address stays attached: a site has to be able to reach a human\n // about traffic it did not expect.\n userAgent: custom ? `${custom} ${DEFAULT_USER_AGENT}` : DEFAULT_USER_AGENT,\n minIntervalMs: readInteger(\n env,\n \"RECIPES_MIN_INTERVAL_MS\",\n DEFAULT_INTERVAL_MS,\n MIN_ALLOWED_INTERVAL_MS,\n MAX_ALLOWED_INTERVAL_MS,\n ),\n timeoutMs: readInteger(env, \"RECIPES_TIMEOUT_MS\", 20_000, 1000, 120_000),\n maxRetries: readInteger(env, \"RECIPES_MAX_RETRIES\", 3, 0, 8),\n cacheTtlMs: readInteger(env, \"RECIPES_CACHE_TTL_MS\", 900_000, 0, 86_400_000),\n cacheMaxEntries: readInteger(env, \"RECIPES_CACHE_MAX_ENTRIES\", 200, 1, 5000),\n logLevel,\n };\n}\n","/**\n * One error type, carrying a code the caller can branch on.\n *\n * The distinction that matters most is between \"no site holds this\" and \"the\n * question could not be asked\". Collapsing the two lets a model report an\n * absence it never established, which is a false statement about the world\n * rather than a missing feature.\n */\n\nimport { ISSUES_URL } from \"./version.js\";\n\nexport type ErrorCode =\n /** A site answered, and holds no such recipe. */\n | \"not_found\"\n /** The arguments cannot produce a request. */\n | \"invalid_input\"\n /** A site asked this client to slow down. */\n | \"rate_limited\"\n /** A response arrived in a shape this server cannot read. */\n | \"parse_failure\"\n /** The request could not be completed. */\n | \"network_error\"\n /** The request was abandoned before an answer arrived. */\n | \"timeout\";\n\nexport const ERROR_CODES: readonly ErrorCode[] = [\n \"not_found\",\n \"invalid_input\",\n \"rate_limited\",\n \"parse_failure\",\n \"network_error\",\n \"timeout\",\n];\n\nexport interface ErrorDetails {\n /** What the caller can do about it, when there is something. */\n hint?: string;\n /** The address that produced the failure, for a bug report. */\n url?: string;\n status?: number;\n}\n\nexport class RecipesError extends Error {\n readonly code: ErrorCode;\n readonly details: ErrorDetails;\n\n constructor(code: ErrorCode, message: string, details: ErrorDetails = {}) {\n super(message);\n this.name = \"RecipesError\";\n this.code = code;\n this.details = details;\n }\n}\n\nexport const notFound = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"not_found\", message, details ?? {});\n\nexport const invalidInput = (message: string, hint?: string) =>\n new RecipesError(\"invalid_input\", message, hint ? { hint } : {});\n\nexport const rateLimited = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"rate_limited\", message, {\n hint: \"Wait a moment and ask again. This says nothing about whether the recipe exists.\",\n ...details,\n });\n\nexport const parseFailure = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"parse_failure\", message, {\n hint: `A site may have changed how it answers. Please report this at ${ISSUES_URL} with the arguments you used.`,\n ...details,\n });\n\nexport const networkError = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"network_error\", message, details ?? {});\n\nexport const timeout = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"timeout\", message, details ?? {});\n\n/**\n * Read the code off a failure raised by the library that reads a source.\n *\n * Those libraries throw their own error classes carrying the six codes above,\n * and the classes are not part of the interface this server imports. Reading\n * the field keeps the taxonomy intact without depending on a class identity a\n * library is free to change, and anything unrecognisable is reported as a\n * network failure, which is the reading that claims least.\n */\nexport function toRecipesError(error: unknown): RecipesError {\n if (error instanceof RecipesError) return error;\n\n const message = error instanceof Error ? error.message : String(error);\n const raw = error as { code?: unknown; details?: unknown } | null;\n const code = typeof raw?.code === \"string\" ? raw.code : \"\";\n const known = ERROR_CODES.find((candidate) => candidate === code);\n\n const details =\n raw && typeof raw.details === \"object\" && raw.details !== null\n ? (raw.details as ErrorDetails)\n : {};\n\n return new RecipesError(known ?? \"network_error\", message, details);\n}\n","/**\n * What a source has to provide to be one.\n *\n * Everything above this file is written for however many sources the registry\n * holds. A source knows four things nothing else does: what it is called, how\n * to recognise one of its own identifiers, how to search it, and how to read one\n * recipe off it. Adding a corpus is writing those four and registering it.\n *\n * The helpers below are the ones every adapter needs, because no source answers\n * with a contract: a field can arrive missing, null, or holding something other\n * than the type it usually holds.\n */\n\nimport { parseFailure } from \"../errors.js\";\nimport type { Language } from \"../recipe/language.js\";\nimport type { RecipeDetail, RecipeRow, SourceId, SourceProfile } from \"../types.js\";\n\n/** Rows that could be read, and how many could not. */\nexport interface ReadRows {\n rows: RecipeRow[];\n /** Rows the source sent in a shape this server could not read. */\n skipped: number;\n /** Rows the source said it saw, in the terms that source counts in. */\n reportedTotal: number | null;\n /** What `reportedTotal` counts on this source, in words. Null when it states none. */\n reportedTotalMeans: string | null;\n cached: boolean;\n}\n\nexport interface ReadRecipe {\n recipe: RecipeDetail;\n cached: boolean;\n}\n\n/** How a source recognised a raw identifier as one of its own. */\nexport interface Claim {\n /** The string this source's own reader takes. */\n reference: string;\n /** Why the shape was read this way, for an answer that has to say so. */\n why: string;\n /**\n * Whether reading it this way is a guess. A shape only this source mints is\n * not; a shape it would merely accept is, and the answer says so.\n */\n guess: boolean;\n}\n\nexport interface SourceAdapter extends SourceProfile {\n /**\n * Recognise a raw identifier, or decline it.\n *\n * A source claims a shape it mints and declines everything else, so a string\n * no source claims is refused, instead of being sent somewhere that would\n * answer it with a confident absence.\n */\n claims(raw: string): Claim | null;\n search(query: string, limit: number): Promise<ReadRows>;\n getRecipe(reference: string): Promise<ReadRecipe>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Reading a value a source sent, without trusting its shape */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The distinction these draw is between a row and a record. One row this server\n * cannot read is dropped and counted, because the rest of the list is still a\n * good answer. A record it cannot read is `parse_failure`, because there is\n * nothing left to return and a caller told \"network_error\" would retry a source\n * that has actually changed.\n */\nexport function text(value: unknown): string | null {\n return typeof value === \"string\" && value.trim() !== \"\" ? value : null;\n}\n\nexport function textList(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === \"string\")\n : [];\n}\n\nexport function count(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nexport function required(value: unknown, field: string, source: SourceProfile): string {\n const found = text(value);\n if (found === null) {\n throw parseFailure(\n `${source.name} answered without a readable \"${field}\", so there is no recipe to return.`,\n );\n }\n return found;\n}\n\nexport function rowsOf<T>(value: unknown, source: SourceProfile): T[] {\n if (!Array.isArray(value)) {\n throw parseFailure(\n `${source.name} answered in a shape this server could not read: the list of results was missing.`,\n );\n }\n return value as T[];\n}\n\n/** Build the identifier this server hands out for a row from a source. */\nexport function namespacedId(source: SourceId, reference: string): string {\n return `${source}:${reference}`;\n}\n\n/**\n * Keep the entries a page actually published.\n *\n * A nutrition panel arrives with a slot for every figure the source knows how\n * to print, and the slots a page left empty are null. Passing those through\n * would put \"protein: null\" in front of a reader as though the page had said\n * something about protein.\n *\n * The panel is walked rather than typed, because each source names its slots\n * differently and a figure is repeated as the page wrote it either way.\n */\nexport function publishedFigures(panel: unknown): Record<string, string> | null {\n if (typeof panel !== \"object\" || panel === null) return null;\n const kept: Record<string, string> = {};\n for (const [key, value] of Object.entries(panel)) {\n if (typeof value === \"string\" && value.trim() !== \"\") kept[key] = value.trim();\n }\n return Object.keys(kept).length > 0 ? kept : null;\n}\n\n/**\n * Read a yield that names a span rather than a number.\n *\n * \"4 à 6 personnes\" and \"4 personnes\" are different claims, and a source that\n * reports the first as a plain count of four hands this server a factor that is\n * too large for anyone at the upper end. The span is read back off the published\n * wording so the answer can say which end it scaled from.\n */\nexport function readYieldSpan(published: string | null): {\n count: number | null;\n max: number | null;\n unit: string | null;\n} {\n if (published === null) return { count: null, max: null, unit: null };\n\n const span = /^\\s*(\\d+(?:[.,]\\d+)?)\\s*(?:à|a|to|-|–|—|ou|or)\\s*(\\d+(?:[.,]\\d+)?)\\s*(.*)$/i.exec(\n published,\n );\n if (span) {\n const low = Number(span[1]!.replace(\",\", \".\"));\n const high = Number(span[2]!.replace(\",\", \".\"));\n if (Number.isFinite(low) && Number.isFinite(high) && high > low) {\n return { count: low, max: high, unit: text(span[3]) };\n }\n }\n\n const single = /^\\s*(\\d+(?:[.,]\\d+)?)\\s*(.*)$/.exec(published);\n if (single) {\n const value = Number(single[1]!.replace(\",\", \".\"));\n if (Number.isFinite(value)) return { count: value, max: null, unit: text(single[2]) };\n }\n\n return { count: null, max: null, unit: null };\n}\n\n/** Make the language a source publishes in available to whatever reads its lines. */\nexport function detailBase(source: SourceProfile): {\n source: SourceId;\n sourceName: string;\n language: Language;\n attribution: string;\n} {\n return {\n source: source.id,\n sourceName: source.name,\n language: source.language,\n attribution: source.attribution,\n };\n}\n","/**\n * Identifiers that name the source they came from.\n *\n * Every source hands back an opaque string, and a caller holding a merged list\n * has no way to tell one source's string from another's. Every identifier this\n * server returns therefore carries a prefix, and `get_recipe` routes on it\n * rather than trying each source in turn.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { SourceId } from \"../types.js\";\nimport type { SourceAdapter } from \"./adapter.js\";\n\nexport { namespacedId } from \"./adapter.js\";\n\nconst SEPARATOR = \":\";\n\nexport interface ResolvedId {\n source: SourceAdapter;\n /** The string that source's own reader takes. */\n reference: string;\n /** Set when the shape was read rather than stated, so the answer can say so. */\n inferred: string | null;\n}\n\n/**\n * Work out which source an identifier belongs to.\n *\n * A prefixed identifier is routed on what it says. A raw one is offered to each\n * source in turn, and a source claims only the shapes it mints: digits for one,\n * a namespaced page key for another. Exactly one claim routes the read and the\n * answer says which reading it used.\n *\n * More than one claim is refused rather than resolved, because picking a winner\n * would send the read somewhere a caller did not intend and answer with a\n * confident recipe for the wrong dish. No claim at all is refused too: a bare\n * dish name is a search, not an identifier, and guessing a page name from it\n * returns whatever that guess happens to hit.\n */\nexport function resolveId(rawId: string, sources: readonly SourceAdapter[]): ResolvedId {\n const trimmed = rawId.trim();\n if (trimmed === \"\") {\n throw invalidInput(\n \"A recipe identifier is required.\",\n `Use one of the ids search_recipes returned, such as ${example(sources)}.`,\n );\n }\n\n for (const source of sources) {\n const prefix = `${source.id}${SEPARATOR}`;\n if (!trimmed.toLowerCase().startsWith(prefix.toLowerCase())) continue;\n const reference = trimmed.slice(prefix.length).trim();\n if (reference === \"\") {\n throw invalidInput(\n `\"${trimmed}\" names a source and no recipe.`,\n \"Put the source's own identifier after the colon.\",\n );\n }\n return { source, reference, inferred: null };\n }\n\n const claimed = sources\n .map((source) => ({ source, claim: source.claims(trimmed) }))\n .filter((entry): entry is { source: SourceAdapter; claim: NonNullable<typeof entry.claim> } =>\n Boolean(entry.claim),\n );\n\n if (claimed.length > 1) {\n throw invalidInput(\n `\"${trimmed}\" could be an identifier on ${names(claimed.map((entry) => entry.source))}, so it names no one recipe.`,\n `Spell it with its source, as in ${claimed.map((entry) => `${entry.source.id}:${trimmed}`).join(\" or \")}.`,\n );\n }\n\n const only = claimed[0];\n if (!only) {\n throw invalidInput(\n `\"${trimmed}\" is not an identifier any of the sources this server reads would mint.`,\n `Call search_recipes with \"${trimmed}\" as the query and pass an id from a row, ` +\n `which reads like ${example(sources)}.`,\n );\n }\n\n return {\n source: only.source,\n reference: only.claim.reference,\n inferred: only.claim.guess\n ? `${only.claim.why}, which is a guess: check the recipe that comes back is the one you meant`\n : only.claim.why,\n };\n}\n\n/** Sources named the way a sentence names them. */\nfunction names(sources: readonly { name: string }[]): string {\n const all = sources.map((source) => source.name);\n if (all.length <= 1) return all.join(\"\");\n return `${all.slice(0, -1).join(\", \")} and ${all[all.length - 1]}`;\n}\n\n/** An identifier of the shape this server hands out, for a hint. */\nfunction example(sources: readonly SourceAdapter[]): string {\n const first = sources[0];\n return first ? `${first.id}${SEPARATOR}<the source's own id>` : \"<source>:<id>\";\n}\n\n/** Which source an identifier names, without resolving what it points at. */\nexport function sourceOf(rawId: string, sources: readonly SourceAdapter[]): SourceId | null {\n const prefix = rawId.split(SEPARATOR)[0]?.toLowerCase();\n return sources.find((source) => source.id.toLowerCase() === prefix)?.id ?? null;\n}\n","/**\n * The sources this server reads.\n *\n * One list, and everything above it is written for however long the list is.\n * Adding a corpus means writing an adapter and adding it here; no tool, no\n * merge and no error path has a branch per source.\n */\n\nimport { MarmitonClient } from \"mcp-marmiton/client\";\nimport type { MarmitonClientOptions } from \"mcp-marmiton/client\";\nimport { CookbookClient } from \"mcp-wikibooks-cookbook/client\";\nimport type { ClientOptions as CookbookClientOptions } from \"mcp-wikibooks-cookbook/client\";\n\nimport type { Config } from \"../config.js\";\nimport { invalidInput } from \"../errors.js\";\nimport type { SourceId } from \"../types.js\";\nimport type { SourceAdapter } from \"./adapter.js\";\nimport { COOKBOOK_PROFILE, cookbookAdapter } from \"./cookbook.js\";\nimport type { CookbookReader } from \"./cookbook.js\";\nimport { MARMITON_PROFILE, marmitonAdapter } from \"./marmiton.js\";\nimport type { MarmitonReader } from \"./marmiton.js\";\n\n/**\n * A reader may be supplied in place of the one that talks to a site, so a\n * program embedding this server can put its own cache in front of a corpus, and\n * a test can drive it from fixed answers.\n */\nexport interface Readers {\n marmiton?: MarmitonReader;\n cookbook?: CookbookReader;\n}\n\n/** The sources a caller can name, in the order an answer takes them. */\nexport const SOURCE_IDS: readonly SourceId[] = [MARMITON_PROFILE.id, COOKBOOK_PROFILE.id];\n\nexport function buildSources(config: Config, readers: Readers = {}): SourceAdapter[] {\n const shared = {\n userAgent: config.userAgent,\n minIntervalMs: config.minIntervalMs,\n timeoutMs: config.timeoutMs,\n maxRetries: config.maxRetries,\n cacheTtlMs: config.cacheTtlMs,\n cacheMaxEntries: config.cacheMaxEntries,\n logLevel: config.logLevel,\n };\n\n const marmitonOptions: MarmitonClientOptions = { config: shared };\n const cookbookOptions: CookbookClientOptions = { config: shared };\n\n return [\n marmitonAdapter(readers.marmiton ?? new MarmitonClient(marmitonOptions)),\n cookbookAdapter(readers.cookbook ?? new CookbookClient(cookbookOptions)),\n ];\n}\n\n/** The sources a caller asked for, in the registry's own order. */\nexport function selectSources(\n sources: SourceAdapter[],\n wanted: readonly SourceId[] | undefined,\n): SourceAdapter[] {\n if (!wanted) return sources;\n\n const unknown = wanted.filter((id) => !sources.some((source) => source.id === id));\n if (unknown.length > 0) {\n throw invalidInput(\n `This server reads no source called ${unknown.map((id) => `\"${id}\"`).join(\", \")}.`,\n `It reads ${sources.map((source) => source.id).join(\", \")}.`,\n );\n }\n\n const chosen = sources.filter((source) => wanted.includes(source.id));\n if (chosen.length === 0) {\n throw invalidInput(\n \"A search needs at least one source.\",\n `Name one of ${sources.map((source) => source.id).join(\", \")}, or leave the argument out to ask them all.`,\n );\n }\n return chosen;\n}\n","/**\n * The Cookbook on the English Wikibooks, as a source.\n *\n * A wiki written by whoever edits it, published under a licence that requires\n * attribution, with no author and no reader rating by nature. Its identifiers\n * are page keys carrying the namespace every Cookbook page lives in.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { RecipeDetail } from \"../types.js\";\nimport type { Claim, ReadRecipe, ReadRows, SourceAdapter } from \"./adapter.js\";\nimport {\n count,\n detailBase,\n namespacedId,\n publishedFigures,\n readYieldSpan,\n required,\n rowsOf,\n text,\n textList,\n} from \"./adapter.js\";\n\n/** A search row, as the Cookbook's own reader publishes one. */\nexport interface CookbookSummary {\n key: string;\n title: string;\n description: string | null;\n excerpt: string | null;\n imageUrl: string | null;\n sourceUrl: string;\n}\n\n/** A page, as the Cookbook's own reader publishes one. */\nexport interface CookbookRecipe {\n key: string;\n title: string;\n sourceUrl: string;\n license: { title: string; url: string } | null;\n description: string | null;\n category: string | null;\n servings: number | null;\n yieldText: string | null;\n yieldUnit: string | null;\n totalMinutes: number | null;\n ingredients: string[];\n equipment: string[];\n steps: string[];\n tips: string[];\n nutrition: unknown;\n}\n\n/** The part of the Cookbook's client this server uses. */\nexport interface CookbookReader {\n search(\n query: string,\n limit: number,\n ): Promise<{ data: { results: CookbookSummary[] }; cached: boolean }>;\n getRecipe(reference: string): Promise<{ data: CookbookRecipe; cached: boolean }>;\n}\n\nexport const COOKBOOK_PROFILE = {\n id: \"cookbook\",\n name: \"Wikibooks Cookbook\",\n homeUrl: \"https://en.wikibooks.org/wiki/Cookbook:Table_of_Contents\",\n language: \"en\" as const,\n attribution: \"Source: Wikibooks Cookbook\",\n // The namespace holds pages about salt and about a dish made with it alike.\n mixesReferencePages: true,\n};\n\nconst SITE_URL = /^https?:\\/\\/en\\.wikibooks\\.org\\/wiki\\//i;\n/** The namespace every page in the Cookbook lives under. */\nconst NAMESPACE = /^cookbook\\s*:/i;\n\n/**\n * Every page in the Cookbook is filed under one namespace, so its title arrives\n * as \"Cookbook:Spaghetti alla Carbonara\". The prefix is where the page lives on\n * the wiki rather than what the dish is called, and repeating it on every row of\n * a list says nothing and crowds out the names. The key keeps it, because that\n * is the string the site resolves.\n */\nfunction dishName(title: string): string {\n return title.replace(NAMESPACE, \"\").trim() || title;\n}\n\nexport function cookbookAdapter(reader: CookbookReader): SourceAdapter {\n return {\n ...COOKBOOK_PROFILE,\n\n claims(raw: string): Claim | null {\n if (SITE_URL.test(raw)) {\n let key: string;\n try {\n key = decodeURIComponent(raw.replace(SITE_URL, \"\")).replace(/_/g, \" \");\n } catch {\n // A percent sign that opens no escape is common in a wiki title, and\n // it is the caller's string rather than a site that failed.\n throw invalidInput(\n `\"${raw}\" carries a percent sign that is not a valid escape, so the page name cannot be read.`,\n \"Pass the id search_recipes returned instead of the address.\",\n );\n }\n return {\n reference: key,\n why: \"the address is a page on the English Wikibooks\",\n guess: false,\n };\n }\n\n if (NAMESPACE.test(raw)) {\n return {\n reference: raw,\n why: \"the name opens with the namespace every Cookbook page lives under\",\n guess: false,\n };\n }\n\n return null;\n },\n\n async search(query: string, limit: number): Promise<ReadRows> {\n const outcome = await reader.search(query, limit);\n const list = rowsOf<Partial<CookbookSummary>>(\n (outcome.data as { results?: unknown })?.results,\n COOKBOOK_PROFILE,\n );\n const rows = [];\n let skipped = 0;\n\n for (const summary of list) {\n const key = text(summary?.key);\n const title = text(summary?.title) ?? key;\n const url = text(summary?.sourceUrl);\n if (!key || !title || !url) {\n skipped += 1;\n continue;\n }\n rows.push({\n id: namespacedId(COOKBOOK_PROFILE.id, key),\n source: COOKBOOK_PROFILE.id,\n sourceName: COOKBOOK_PROFILE.name,\n title: dishName(title),\n url,\n imageUrl: text(summary?.imageUrl),\n excerpt: text(summary?.excerpt) ?? text(summary?.description),\n });\n }\n\n return {\n rows,\n skipped,\n // The gateway states no total and offers no second page, so there is no\n // number to report. Counting the rows returned and calling it a total\n // would invent one.\n reportedTotal: null,\n reportedTotalMeans: null,\n cached: outcome.cached,\n };\n },\n\n async getRecipe(reference: string): Promise<ReadRecipe> {\n const outcome = await reader.getRecipe(reference);\n return { recipe: cookbookDetail(outcome.data), cached: outcome.cached };\n },\n };\n}\n\nexport function cookbookDetail(payload: unknown): RecipeDetail {\n const recipe = (payload ?? {}) as Partial<CookbookRecipe>;\n const license = recipe.license as Partial<NonNullable<CookbookRecipe[\"license\"]>> | null;\n const licenseTitle = text(license?.title);\n const licenseUrl = text(license?.url);\n\n const published = text(recipe.yieldText);\n const span = readYieldSpan(published);\n\n return {\n ...detailBase(COOKBOOK_PROFILE),\n id: namespacedId(COOKBOOK_PROFILE.id, required(recipe.key, \"key\", COOKBOOK_PROFILE)),\n title: dishName(required(recipe.title ?? recipe.key, \"title\", COOKBOOK_PROFILE)),\n url: required(recipe.sourceUrl, \"url\", COOKBOOK_PROFILE),\n // The page source carries no picture this server can address.\n imageUrl: null,\n yieldCount: count(recipe.servings) ?? span.count,\n yieldMax: span.max,\n yieldText: published,\n yieldUnit: text(recipe.yieldUnit) ?? span.unit,\n ingredients: textList(recipe.ingredients),\n steps: textList(recipe.steps),\n // The Cookbook publishes one time for a recipe rather than splitting it.\n prepMinutes: null,\n cookMinutes: null,\n totalMinutes: count(recipe.totalMinutes),\n category: text(recipe.category),\n // The page is written by everyone who edited it, so it credits nobody in\n // particular and carries no reader rating.\n author: null,\n rating: null,\n nutrition: publishedFigures(recipe.nutrition),\n equipment: textList(recipe.equipment),\n tips: textList(recipe.tips),\n // A licence is only stated when both halves of it arrived: a name with no\n // address, or an address with no name, is not terms anyone can follow.\n license: licenseTitle && licenseUrl ? { title: licenseTitle, url: licenseUrl } : null,\n };\n}\n","/**\n * Marmiton, as a source.\n *\n * A French recipe site with reader ratings, whose identifiers are digits and\n * whose search serves a single page it disallows paging past.\n */\n\nimport type { RecipeDetail } from \"../types.js\";\nimport type { Claim, ReadRecipe, ReadRows, SourceAdapter } from \"./adapter.js\";\nimport {\n count,\n detailBase,\n namespacedId,\n publishedFigures,\n readYieldSpan,\n required,\n rowsOf,\n text,\n textList,\n} from \"./adapter.js\";\n\n/** A search row, as Marmiton's own reader publishes one. */\nexport interface MarmitonSummary {\n id: string;\n title: string;\n url: string;\n imageUrl: string | null;\n}\n\n/** A recipe, as Marmiton's own reader publishes one. */\nexport interface MarmitonRecipe {\n id: string;\n title: string;\n url: string;\n imageUrl: string | null;\n ingredients: string[];\n steps: string[];\n recipeYield: { count: number | null; unit: string | null; text: string };\n prepMinutes: number | null;\n cookMinutes: number | null;\n totalMinutes: number | null;\n category: string | null;\n author: string | null;\n rating: { value: number; count: number | null; best: number | null } | null;\n nutrition: unknown;\n}\n\n/** The part of Marmiton's client this server uses. */\nexport interface MarmitonReader {\n search(query: string): Promise<{ data: MarmitonSummary[]; cached: boolean }>;\n getRecipe(ref: { id?: string; url?: string }): Promise<{ data: MarmitonRecipe; cached: boolean }>;\n}\n\nexport const MARMITON_PROFILE = {\n id: \"marmiton\",\n name: \"Marmiton\",\n homeUrl: \"https://www.marmiton.org\",\n language: \"fr\" as const,\n attribution: \"Source: Marmiton\",\n // Every page the search returns is a recipe.\n mixesReferencePages: false,\n};\n\n/** Recipe pages, whose address ends in the numeric identifier. */\nconst SITE_URL = /^https?:\\/\\/(?:www\\.)?marmiton\\.org\\//i;\n/** The shape Marmiton mints: digits and nothing else. */\nconst REFERENCE = /^\\d+$/;\n\nexport function marmitonAdapter(reader: MarmitonReader): SourceAdapter {\n return {\n ...MARMITON_PROFILE,\n\n claims(raw: string): Claim | null {\n if (SITE_URL.test(raw)) {\n return { reference: raw, why: \"the address is a Marmiton page\", guess: false };\n }\n if (REFERENCE.test(raw)) {\n return {\n reference: raw,\n why: \"a bare number is the shape Marmiton mints\",\n guess: false,\n };\n }\n return null;\n },\n\n async search(query: string): Promise<ReadRows> {\n const outcome = await reader.search(query);\n const list = rowsOf<Partial<MarmitonSummary>>(outcome.data, MARMITON_PROFILE);\n const rows = [];\n let skipped = 0;\n\n for (const summary of list) {\n // A row needs something to call it, somewhere to send a reader, and an\n // identifier that reads back. A row missing any of them is dropped\n // rather than returned with a hole in it.\n const id = text(summary?.id);\n const title = text(summary?.title);\n const url = text(summary?.url);\n if (!id || !title || !url) {\n skipped += 1;\n continue;\n }\n rows.push({\n id: namespacedId(MARMITON_PROFILE.id, id),\n source: MARMITON_PROFILE.id,\n sourceName: MARMITON_PROFILE.name,\n title,\n url,\n imageUrl: text(summary?.imageUrl),\n // The result list carries a title and a picture and no prose, so\n // there is nothing to quote here.\n excerpt: null,\n });\n }\n\n return {\n rows,\n skipped,\n // Rows on the page, including the ones this server could not read: the\n // site saw them, and reporting only the readable ones would understate\n // what it holds.\n reportedTotal: rows.length + skipped,\n reportedTotalMeans:\n \"rows on the single page of results Marmiton serves; the site disallows paging, so this is not a catalogue count\",\n cached: outcome.cached,\n };\n },\n\n async getRecipe(reference: string): Promise<ReadRecipe> {\n // The reader takes an identifier or an address and tells them apart.\n const ref = /^https?:\\/\\//i.test(reference) ? { url: reference } : { id: reference };\n const outcome = await reader.getRecipe(ref);\n return { recipe: marmitonDetail(outcome.data), cached: outcome.cached };\n },\n };\n}\n\nexport function marmitonDetail(payload: unknown): RecipeDetail {\n const recipe = (payload ?? {}) as Partial<MarmitonRecipe>;\n const yields = (recipe.recipeYield ?? {}) as Partial<MarmitonRecipe[\"recipeYield\"]>;\n const rating = recipe.rating as Partial<NonNullable<MarmitonRecipe[\"rating\"]>> | null | undefined;\n const value = count(rating?.value);\n\n // The reader splits the published yield at the first number, which leaves the\n // rest of a span sitting where the word being counted belongs: \"4 à 6\n // personnes\" arrives as a count of four and a unit of \"à 6 personnes\". The\n // wording is read again here so both ends of the span are known.\n const published = text(yields.text);\n const span = readYieldSpan(published);\n\n return {\n ...detailBase(MARMITON_PROFILE),\n id: namespacedId(MARMITON_PROFILE.id, required(recipe.id, \"id\", MARMITON_PROFILE)),\n title: required(recipe.title, \"title\", MARMITON_PROFILE),\n url: required(recipe.url, \"url\", MARMITON_PROFILE),\n imageUrl: text(recipe.imageUrl),\n yieldCount: span.count ?? count(yields.count),\n yieldMax: span.max,\n yieldText: published,\n yieldUnit: span.unit ?? text(yields.unit),\n ingredients: textList(recipe.ingredients),\n steps: textList(recipe.steps),\n prepMinutes: count(recipe.prepMinutes),\n cookMinutes: count(recipe.cookMinutes),\n totalMinutes: count(recipe.totalMinutes),\n category: text(recipe.category),\n author: text(recipe.author),\n rating: value === null ? null : { value, count: count(rating?.count), max: count(rating?.best) },\n nutrition: publishedFigures(recipe.nutrition),\n equipment: [],\n tips: [],\n // Marmiton states no licence on a recipe page, and silence is not a grant.\n license: null,\n };\n}\n","/**\n * The one place that talks to the sources.\n *\n * It asks them all at once and merges what comes back, so a question is\n * answered by whichever of them holds something. It imports nothing from the\n * MCP layer and is published on its own, so the same code serves a plain\n * script.\n *\n * A source that fails is reported as a source that failed. It is never folded\n * into the answer as an absence: \"Marmiton was unreachable\" and \"Marmiton holds\n * no such recipe\" are different statements about the world, and a caller that\n * cannot tell them apart will say the second when only the first is true.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n MAX_ALLOWED_INTERVAL_MS,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport { invalidInput, timeout as timedOut, toRecipesError } from \"../errors.js\";\nimport type { MergedSearch, RecipeDetail, RecipeRow, SourceId, SourceReport } from \"../types.js\";\nimport type { SourceAdapter } from \"./adapter.js\";\nimport { resolveId } from \"./ids.js\";\nimport type { ResolvedId } from \"./ids.js\";\nimport { buildSources, selectSources } from \"./registry.js\";\nimport type { Readers } from \"./registry.js\";\n\nexport type { SourceAdapter, ReadRecipe, ReadRows } from \"./adapter.js\";\nexport type { CookbookReader } from \"./cookbook.js\";\nexport type { MarmitonReader } from \"./marmiton.js\";\nexport type { Readers } from \"./registry.js\";\nexport type {\n MergedSearch,\n RecipeDetail,\n RecipeRow,\n SourceId,\n SourceProfile,\n SourceReport,\n} from \"../types.js\";\nexport { SOURCE_IDS } from \"./registry.js\";\nexport { namespacedId, resolveId } from \"./ids.js\";\n\nexport interface RecipesClientOptions {\n config?: Partial<Config>;\n logger?: Logger;\n /** Stands in for one or more of the site readers. */\n readers?: Readers;\n /** Replaces the registry outright, for a program bringing its own corpora. */\n sources?: SourceAdapter[];\n}\n\n/** The largest number of rows any one source is asked for in a single call. */\nexport const MAX_LIMIT_PER_SOURCE = 25;\n\n/**\n * The pacing this server owes each source, applied to whatever it is handed.\n *\n * A configuration object assembled by a caller has not been through\n * `loadConfig`, so it can carry a missing value, a value of the wrong shape, or\n * a User-Agent that names somebody else. Every setting is held to the same range\n * the environment parser enforces: `timeoutMs: 0` is the usual way of writing\n * \"no deadline\", and a retry count of a hundred thousand aimed at a site paced\n * in milliseconds is hours of traffic from a single call.\n */\nfunction withGuarantees(config: Config): Config {\n const defaults = loadConfig({});\n\n const bounded = (value: unknown, fallback: number, min: number, max: number): number => {\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) return fallback;\n return Math.min(max, Math.max(min, parsed));\n };\n\n const claimed = typeof config.userAgent === \"string\" ? config.userAgent.trim() : \"\";\n const identifier = defaults.userAgent;\n\n return {\n ...config,\n userAgent:\n claimed === \"\" || claimed.includes(identifier) ? identifier : `${claimed} ${identifier}`,\n minIntervalMs: bounded(\n config.minIntervalMs,\n defaults.minIntervalMs,\n MIN_ALLOWED_INTERVAL_MS,\n MAX_ALLOWED_INTERVAL_MS,\n ),\n timeoutMs: bounded(config.timeoutMs, defaults.timeoutMs, 1000, 120_000),\n maxRetries: bounded(config.maxRetries, defaults.maxRetries, 0, 8),\n cacheTtlMs: bounded(config.cacheTtlMs, defaults.cacheTtlMs, 0, 86_400_000),\n cacheMaxEntries: bounded(config.cacheMaxEntries, defaults.cacheMaxEntries, 1, 5000),\n logLevel: config.logLevel ?? defaults.logLevel,\n };\n}\n\n/**\n * A wall clock over one source, as a backstop under the deadline that source's\n * own reader keeps.\n *\n * That reader times out its own requests, so this only matters when it cannot:\n * a socket that stalls without erroring, a stand-in reader, a bug in a\n * dependency free to change under a caret range. Without it, one source that\n * never answers holds the whole call open and the rows the others had ready in\n * milliseconds are never returned.\n *\n * The allowance covers every attempt a reader is entitled to make, plus the\n * pacing between them, so this never fires before the reader's own deadline has.\n */\nfunction withDeadline<T>(work: Promise<T>, ms: number, source: SourceAdapter): Promise<T> {\n let timer: ReturnType<typeof setTimeout>;\n const alarm = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(\n () =>\n reject(\n timedOut(\n `${source.name} did not answer within ${ms}ms and was left out of this answer.`,\n ),\n ),\n ms,\n );\n // A pending timer must not hold the process open once the answer is out.\n timer.unref?.();\n });\n return Promise.race([work, alarm]).finally(() => clearTimeout(timer));\n}\n\n/** What one source produced for one question, whether or not it produced anything. */\ninterface Attempt {\n source: SourceAdapter;\n rows: RecipeRow[];\n cached: boolean;\n reportedTotal: number | null;\n reportedTotalMeans: string | null;\n skipped: number;\n error: { code: string; message: string; hint?: string } | null;\n}\n\nfunction reportOf(attempt: Attempt, count: number): SourceReport {\n return {\n source: attempt.source.id,\n name: attempt.source.name,\n status: attempt.error ? \"failed\" : \"answered\",\n count,\n reportedTotal: attempt.reportedTotal,\n reportedTotalMeans: attempt.reportedTotalMeans,\n skipped: attempt.skipped,\n mixesReferencePages: attempt.source.mixesReferencePages,\n cached: attempt.cached,\n error: attempt.error,\n };\n}\n\n/**\n * Interleave rows so no source opens the list twice before another has opened\n * it once.\n *\n * Ranking them against each other would need a score every source carries, and\n * there is none: one has reader ratings, another has no author and no rating by\n * nature, and a single ordering would rank on a field most rows cannot have.\n * Taking one from each in turn is an order a caller can describe exactly, which\n * is what the answer does.\n */\nexport function interleave(groups: RecipeRow[][]): RecipeRow[] {\n const merged: RecipeRow[] = [];\n const longest = Math.max(0, ...groups.map((group) => group.length));\n for (let index = 0; index < longest; index += 1) {\n for (const group of groups) {\n const row = group[index];\n if (row) merged.push(row);\n }\n }\n return merged;\n}\n\nexport class RecipesClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly sources: SourceAdapter[];\n\n constructor(options: RecipesClientOptions = {}) {\n this.config = withGuarantees({ ...loadConfig(), ...options.config });\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.sources = options.sources ?? buildSources(this.config, options.readers ?? {});\n }\n\n /** What every source sees this client call itself. */\n get userAgent(): string {\n return this.config.userAgent;\n }\n\n get intervalMs(): number {\n return this.config.minIntervalMs;\n }\n\n get timeoutMs(): number {\n return this.config.timeoutMs;\n }\n\n get maxRetries(): number {\n return this.config.maxRetries;\n }\n\n /** The sources this server reads, in the order an answer takes them. */\n get profiles(): Array<{ id: SourceId; name: string; language: string; homeUrl: string }> {\n return this.sources.map((source) => ({\n id: source.id,\n name: source.name,\n language: source.language,\n homeUrl: source.homeUrl,\n }));\n }\n\n /**\n * The backstop deadline over one source, covering every attempt it is\n * entitled to make and the pacing between them.\n */\n private get deadlineMs(): number {\n const attempts = this.config.maxRetries + 1;\n return this.config.timeoutMs * attempts + this.config.minIntervalMs * this.config.maxRetries;\n }\n\n /**\n * Ask every source the same question and merge the rows.\n *\n * The calls go out together, so the answer takes as long as the slowest\n * source rather than as long as all of them. Whichever of them fail, the rest\n * still come back, and the report says which was which.\n */\n async searchRecipes(\n query: string,\n limitPerSource: number,\n wanted?: readonly SourceId[],\n ): Promise<MergedSearch> {\n const trimmed = query.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"A search needs something to look for.\", \"Name a dish or an ingredient.\");\n }\n\n const chosen = selectSources(this.sources, wanted);\n const limit = Math.max(1, Math.min(MAX_LIMIT_PER_SOURCE, Math.trunc(limitPerSource)));\n\n const attempts = await Promise.all(\n chosen.map((source) => this.searchOne(source, trimmed, limit)),\n );\n\n const groups = attempts.map((attempt) => attempt.rows.slice(0, limit));\n return {\n rows: interleave(groups),\n reports: attempts.map((attempt, index) => reportOf(attempt, groups[index]!.length)),\n };\n }\n\n private async searchOne(\n source: SourceAdapter,\n query: string,\n limit: number,\n ): Promise<Attempt> {\n try {\n const read = await withDeadline(source.search(query, limit), this.deadlineMs, source);\n if (read.skipped > 0) {\n this.logger.warn(\n `${source.name} sent ${read.skipped} row(s) this server could not read; they were left out.`,\n );\n }\n return {\n source,\n rows: read.rows,\n cached: read.cached,\n reportedTotal: read.reportedTotal,\n reportedTotalMeans: read.reportedTotalMeans,\n skipped: read.skipped,\n error: null,\n };\n } catch (error) {\n const known = toRecipesError(error);\n this.logger.warn(`${source.name} did not answer: [${known.code}] ${known.message}`);\n return {\n source,\n rows: [],\n cached: false,\n reportedTotal: null,\n reportedTotalMeans: null,\n skipped: 0,\n error: {\n code: known.code,\n message: known.message,\n ...(known.details.hint ? { hint: known.details.hint } : {}),\n },\n };\n }\n }\n\n /**\n * Read one recipe, from whichever source the identifier names.\n *\n * Only that source is called. Trying another after a miss would turn \"this\n * source has no page by this name\" into somebody else's recipe with a\n * different title, which is a worse answer than the absence it replaced.\n */\n async getRecipe(id: string): Promise<{ recipe: RecipeDetail; cached: boolean; read: ResolvedId }> {\n const read = resolveId(id, this.sources);\n try {\n const outcome = await withDeadline(\n read.source.getRecipe(read.reference),\n this.deadlineMs,\n read.source,\n );\n return { recipe: outcome.recipe, cached: outcome.cached, read };\n } catch (error) {\n throw toRecipesError(error);\n }\n }\n}\n","/**\n * compare_recipes: the same dish, as each source writes it.\n *\n * This is the one tool that exists because there is more than one source. A\n * French kitchen and an English one write the same dish differently, and the\n * difference is the answer: what each asks for, what each measures in, how many\n * it serves, and what each source publishes about a recipe at all.\n *\n * Nothing is ranked. The sources share no score, and declaring a winner would\n * mean inventing one.\n */\n\nimport { z } from \"zod\";\nimport type { RecipesClient } from \"../sources/client.js\";\nimport type { RecipeDetail, SourceId, SourceReport } from \"../types.js\";\nimport { buildRecipeView, recipeSchema, SECTIONS } from \"./recipeView.js\";\nimport type { RecipePayload, Section } from \"./recipeView.js\";\nimport {\n creditLine,\n ok,\n quoteForeign,\n roomForBody,\n reportNotes,\n reportSchema,\n toReportPayload,\n toToolError,\n} from \"./shared.js\";\nimport { strictInput } from \"./arguments.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const compareRecipesDescription = [\n \"Take a dish and show how each source writes it, side by side.\",\n \"Each source's closest match is read in full, and all of them can be rescaled to the same number of servings so the ingredient lists stand comparison.\",\n \"The answer states what differs and leaves it there: the quantities each asks for, what each measures in, what each yields, and which fields each source publishes at all.\",\n \"No version is ranked above another. Some sources carry reader ratings and some have no author and no rating by nature, so there is no score they share.\",\n \"When only one source answers, the answer says so and shows that one rather than presenting part of a comparison as the whole of it.\",\n].join(\" \");\n\nexport const compareRecipesInput = strictInput({\n dish: z\n .string()\n .min(1)\n .max(200)\n .describe(\"The dish to compare, in any of the languages the sources publish in.\"),\n servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Rescale every version to this many, which is what makes the lists comparable.\"),\n sections: z\n .array(z.enum(SECTIONS))\n .default([\"ingredients\"])\n .describe(\"Which parts of each version to return. Several full recipes is a lot of text.\"),\n max_steps: z.number().int().min(1).max(100).default(10).describe(\"Steps to return per version.\"),\n max_step_chars: z\n .number()\n .int()\n .min(80)\n .max(4000)\n .default(600)\n .describe(\"Characters kept per step. Raise it only if a step was cut mid-sentence.\"),\n});\n\nexport const compareRecipesOutput = z.object({\n dish: z.string(),\n versions: z.array(recipeSchema),\n differences: z\n .array(z.string())\n .describe(\"What differs between the versions, stated as fact. No version is ranked.\"),\n per_source: z.array(reportSchema),\n notes: z.array(z.string()),\n});\n\nexport type CompareRecipesArgs = z.infer<typeof compareRecipesInput>;\n\n/** Ingredient lines each version shows in the text block. */\nconst TEXT_INGREDIENT_LINES = 8;\n\n/** Differences the text block states before pointing at the rest. */\nconst TEXT_DIFFERENCE_LINES = 4;\n\n/** Sources named the way a sentence names them. */\nfunction listNames(names: string[]): string {\n if (names.length <= 1) return names.join(\"\");\n return `${names.slice(0, -1).join(\", \")} and ${names[names.length - 1]}`;\n}\n\n/**\n * Say what the versions do differently, and say nothing where they agree.\n *\n * A field one source has and another does not is the most useful thing a\n * comparison can report, and the least tempting to turn into a ranking: a wiki\n * has no author because everyone edited the page, which says nothing about the\n * recipe. Where every version says the same thing there is no difference to\n * report, and reporting one anyway costs a reader's trust in the rest.\n */\nfunction describeDifferences(recipes: RecipeDetail[], payloads: RecipePayload[]): string[] {\n if (recipes.length < 2) return [];\n const differences: string[] = [];\n const nameOf = (recipe: RecipeDetail) => recipe.sourceName;\n\n const yields = payloads.map((payload) => payload.yield.original_text ?? \"no stated amount\");\n if (new Set(yields).size > 1) {\n differences.push(\n `${payloads\n .map((payload, index) => `${payload.source_name} yields ${quoteForeign(yields[index]!)}`)\n .join(\"; \")}.`,\n );\n }\n\n const counts = payloads.map((payload) => payload.ingredients.length);\n if (new Set(counts).size > 1 && counts.every((value) => value > 0)) {\n differences.push(\n `${payloads\n .map(\n (payload) =>\n `${payload.source_name} lists ${payload.ingredients.length} ingredient lines`,\n )\n .join(\"; \")}.`,\n );\n }\n\n const timed = recipes.filter((recipe) => recipe.totalMinutes !== null);\n const untimed = recipes.filter((recipe) => recipe.totalMinutes === null);\n if (timed.length > 0 && new Set(timed.map((recipe) => recipe.totalMinutes)).size > 1) {\n differences.push(\n `${timed.map((recipe) => `${nameOf(recipe)} states ${recipe.totalMinutes} minutes`).join(\"; \")}.`,\n );\n }\n if (timed.length > 0 && untimed.length > 0) {\n differences.push(\n `${listNames(untimed.map(nameOf))} states no time for this recipe, so there is nothing there to compare against.`,\n );\n }\n\n const rated = recipes.filter((recipe) => recipe.rating !== null);\n if (rated.length > 0 && rated.length < recipes.length) {\n differences.push(\n `Only ${listNames(rated.map(nameOf))} carries a reader rating, so the versions cannot be compared on one.`,\n );\n }\n\n const credited = recipes.filter((recipe) => recipe.author !== null);\n if (credited.length > 0 && credited.length < recipes.length) {\n differences.push(\n `Only ${listNames(credited.map(nameOf))} credits an author; the rest are written by whoever edited the page.`,\n );\n }\n\n const licensed = recipes.filter((recipe) => recipe.license !== null);\n for (const recipe of licensed) {\n differences.push(\n `${nameOf(recipe)} publishes under ${quoteForeign(recipe.license!.title)}, which asks for attribution.`,\n );\n }\n const silent = recipes.filter((recipe) => recipe.license === null);\n if (silent.length > 0 && licensed.length > 0) {\n differences.push(\n `${listNames(silent.map(nameOf))} states no terms of use. Silence is not permission.`,\n );\n }\n\n return differences;\n}\n\nexport async function runCompareRecipes(\n client: RecipesClient,\n args: CompareRecipesArgs,\n): Promise<ToolResult> {\n try {\n // A few rows from each source is enough: the point is the traditions side\n // by side rather than a long list from any of them.\n const merged = await client.searchRecipes(args.dish, 3);\n\n const best = new Map<SourceId, string>();\n for (const row of merged.rows) {\n if (!best.has(row.source)) best.set(row.source, row.id);\n }\n\n const offered = [...best.entries()];\n const reads = await Promise.allSettled(offered.map(([, id]) => client.getRecipe(id)));\n\n const recipes: RecipeDetail[] = [];\n /** Sources whose row was found and whose page then could not be read. */\n const unread = new Map<SourceId, string>();\n\n reads.forEach((read, index) => {\n const source = offered[index]![0];\n if (read.status === \"fulfilled\") {\n recipes.push(read.value.recipe);\n return;\n }\n const reason = read.reason as { code?: string; message?: string } | undefined;\n unread.set(source, `${reason?.code ?? \"network_error\"}: ${reason?.message ?? \"unknown\"}`);\n });\n\n const views = recipes.map((recipe) =>\n buildRecipeView(recipe, {\n servings: args.servings ?? null,\n sections: args.sections as readonly Section[],\n maxSteps: args.max_steps,\n maxStepChars: args.max_step_chars,\n label: recipe.sourceName,\n // The choice of sections belongs to the call rather than to a version,\n // so it is said once below instead of once per recipe.\n announceSections: false,\n }),\n );\n const payloads = views.map((view) => view.payload);\n\n const notes = reportNotes(merged.reports);\n for (const view of views) notes.push(...view.notes);\n\n const omitted = SECTIONS.filter((section) => !args.sections.includes(section));\n if (omitted.length > 0) {\n notes.push(\n `Not requested, so not returned on any version: ${omitted.join(\", \")}. Name them in 'sections' to see them.`,\n );\n }\n\n // Three reasons a source can be missing from a comparison, and they are\n // three different statements about the world. Reading a fetch failure as\n // \"that source offered nothing\" is the one that turns a bad minute into a\n // claim about what a corpus holds.\n for (const report of merged.reports) {\n if (recipes.some((recipe) => recipe.source === report.source)) continue;\n const failedRead = unread.get(report.source);\n if (report.status === \"failed\") {\n notes.push(\n `${report.name} is missing from this comparison because its search did not answer, not because it holds nothing.`,\n );\n } else if (failedRead) {\n notes.push(\n `${report.name} offered a recipe and it could not be read (${quoteForeign(failedRead)}), so its version is missing rather than absent.`,\n );\n } else {\n notes.push(\n `${report.name} answered and offered nothing close enough to \"${quoteForeign(args.dish)}\" to compare.`,\n );\n }\n }\n\n if (payloads.length === 1) {\n notes.push(\n \"This is one version rather than a comparison. Read it as what that one source publishes.\",\n );\n }\n\n const differences = describeDifferences(recipes, payloads);\n if (payloads.length > 1) {\n notes.push(\n \"Quantities are shown in the units each source published. Nothing was converted between \" +\n \"measuring systems, so a gram in one version and a cup in another are two ways of \" +\n \"writing a recipe rather than one quantity restated.\",\n );\n }\n\n const credit = creditLine(\n payloads.map((payload) => ({ attribution: payload.attribution, url: payload.url })),\n );\n // The differences a reader most needs are the ones about the recipes\n // themselves, and they come first. The rest stay in the structured output\n // rather than taking the room the versions need.\n const shownDifferences = differences.slice(0, TEXT_DIFFERENCE_LINES);\n const moreDifferences = differences.length - shownDifferences.length;\n const differencesBlock =\n differences.length > 0\n ? `What differs:\\n${shownDifferences.map((line) => `- ${line}`).join(\"\\n\")}${\n moreDifferences > 0\n ? `\\n- … and ${moreDifferences} more, in full in the structured output`\n : \"\"\n }\\n\\n`\n : \"\";\n\n // Every version has to fit, so the room left after the differences and the\n // trailer is shared out equally: each version shows an opening rather than\n // the first one filling the block and the rest being cut away.\n const share =\n payloads.length === 0\n ? 0\n : (roomForBody({ notes, credit }) - differencesBlock.length) / payloads.length;\n\n const versionBlocks = payloads.map((payload) => {\n const head = [\n `${quoteForeign(payload.source_name)}: ${quoteForeign(payload.title)}`,\n ` ${quoteForeign(payload.url)}`,\n ` yields ${quoteForeign(payload.yield.original_text ?? \"an unstated amount\")}${\n payload.yield.factor === null ? \"\" : `, scaled by ${payload.yield.factor}`\n }`,\n ];\n const lines = [...head];\n let used = head.join(\"\\n\").length;\n\n let shown = 0;\n for (const entry of payload.ingredients.slice(0, TEXT_INGREDIENT_LINES)) {\n const line = ` - ${quoteForeign(entry.text)}`;\n if (used + line.length > share) break;\n lines.push(line);\n used += line.length + 1;\n shown += 1;\n }\n\n const hidden = payload.ingredients.length - shown;\n if (hidden > 0) lines.push(` … and ${hidden} more, in full in the structured output`);\n return lines.join(\"\\n\");\n });\n\n const body =\n payloads.length === 0\n ? unread.size > 0\n ? `Every version of \"${quoteForeign(args.dish)}\" that was offered could not be read, so nothing was compared.`\n : `No source offered a recipe for \"${quoteForeign(args.dish)}\".`\n : versionBlocks.join(\"\\n\\n\");\n\n // What differs comes first. It is the answer to the question that was\n // asked, and it is short, so it survives a text block cut to fit while\n // several full ingredient lists never could.\n const summary = `${differencesBlock}${body}`;\n\n return ok(\n {\n dish: args.dish,\n versions: payloads,\n differences,\n per_source: merged.reports.map(toReportPayload),\n notes,\n },\n summary,\n { notes, credit },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n\nexport type { SourceReport };\n","/**\n * Turning one recipe into the payload a tool returns.\n *\n * Both `get_recipe` and `compare_recipes` need the same thing: a yield stated\n * as published and as asked for, an ingredient list scaled to match, and the\n * sentences that qualify both. Sharing it means the two tools cannot drift into\n * describing the same recipe differently.\n */\n\nimport { z } from \"zod\";\nimport { passthroughIngredients, scaleIngredients } from \"../recipe/scale.js\";\nimport type { ScaledIngredient } from \"../recipe/scale.js\";\nimport type { RecipeDetail } from \"../types.js\";\nimport { ingredientSchema, quoteForeign, toIngredientPayload, truncate } from \"./shared.js\";\n\nexport const yieldSchema = z.object({\n original_count: z\n .number()\n .nullable()\n .describe(\"Servings the page states as a number. Null when it states none.\"),\n original_max: z\n .number()\n .nullable()\n .describe(\"The upper end when the page states a span, as in '4 à 6 personnes'.\"),\n original_text: z\n .string()\n .nullable()\n .describe(\n \"The yield in the page's own words. '4 à 6 personnes' and '4 personnes' are different claims, so the wording is kept.\",\n ),\n requested: z.number().int().nullable().describe(\"Servings asked for, when any were.\"),\n unit: z\n .string()\n .nullable()\n .describe(\"What the yield counts when it counts something other than people, such as 'balls'.\"),\n factor: z\n .number()\n .nullable()\n .describe(\"What the quantities were multiplied by. Null when nothing was rescaled.\"),\n});\n\nexport const ratingSchema = z.object({\n value: z.number(),\n count: z.number().int().nullable().describe(\"How many readers rated it, when the source says.\"),\n max: z\n .number()\n .nullable()\n .describe(\"The top of the scale. A rating read without its scale means nothing.\"),\n});\n\nexport const recipeSchema = z.object({\n id: z.string(),\n source: z.string(),\n source_name: z.string(),\n language: z.enum([\"fr\", \"en\"]).describe(\"The language this recipe is written in.\"),\n title: z.string(),\n url: z.string(),\n image_url: z.string().nullable(),\n yield: yieldSchema,\n ingredients: z.array(ingredientSchema),\n steps: z.array(z.string()),\n prep_minutes: z.number().int().nullable(),\n cook_minutes: z.number().int().nullable(),\n total_minutes: z.number().int().nullable(),\n category: z.string().nullable(),\n author: z\n .string()\n .nullable()\n .describe(\"Null on a source written by everyone who edited the page.\"),\n rating: ratingSchema.nullable().describe(\"Null on a source that carries no reader rating.\"),\n nutrition: z\n .record(z.string(), z.string())\n .nullable()\n .describe(\"The panel as published, key by key. Figures the page omits are absent, never zero.\"),\n equipment: z.array(z.string()),\n tips: z.array(z.string()),\n license: z\n .object({ title: z.string(), url: z.string() })\n .nullable()\n .describe(\"Terms the page is published under, when the source states them.\"),\n attribution: z.string().describe(\"Show this, with the url, when repeating anything from here.\"),\n sections_returned: z\n .array(z.string())\n .describe(\"The sections this answer carries. Anything outside this list was not looked at.\"),\n sections_omitted: z\n .array(z.string())\n .describe(\n \"Sections nobody asked for. A field belonging to one of these is empty because it was not requested, never because the page states nothing.\",\n ),\n scaling_summary: z.object({\n scaled_count: z.number().int(),\n rounded_count: z.number().int(),\n unscaled_count: z.number().int(),\n }),\n});\n\nexport type RecipePayload = z.infer<typeof recipeSchema>;\n\n/** The sections a caller can ask for, since a full recipe is a lot of text. */\nexport const SECTIONS = [\n \"ingredients\",\n \"steps\",\n \"times\",\n \"nutrition\",\n \"tips\",\n \"equipment\",\n] as const;\nexport type Section = (typeof SECTIONS)[number];\n\nexport interface RecipeView {\n payload: RecipePayload;\n notes: string[];\n ingredients: ScaledIngredient[];\n}\n\n/**\n * Work out what to multiply by, and say when nothing can be.\n *\n * A page that states no number of servings cannot be rescaled: dividing by a\n * yield nobody wrote would answer with quantities for a number of people the\n * page never claimed. Such a recipe comes back as published, and the answer\n * says why.\n */\nfunction resolveFactor(\n recipe: RecipeDetail,\n servings: number | null,\n): { factor: number | null; notes: string[] } {\n if (servings === null) return { factor: null, notes: [] };\n\n if (recipe.yieldCount === null || recipe.yieldCount <= 0) {\n return {\n factor: null,\n notes: [\n `${recipe.sourceName} states no number of servings for this recipe, so the quantities are ` +\n \"as published. Use scale_ingredients with a factor you choose if you know what it serves.\",\n ],\n };\n }\n\n const factor = servings / recipe.yieldCount;\n const notes: string[] = [];\n\n if (factor === 1) {\n return {\n factor: 1,\n notes: [`The page already yields ${recipe.yieldCount}, so nothing was multiplied.`],\n };\n }\n\n // A page that says \"4 à 6 personnes\" has named a span, and there is no one\n // factor that serves both ends of it. Scaling from the lower end is the\n // choice that leaves nobody short, and saying so is what stops the number\n // being read as the page's own arithmetic.\n if (recipe.yieldMax !== null && recipe.yieldMax > recipe.yieldCount) {\n notes.push(\n `The page states a span rather than a number: ${quoteForeign(recipe.yieldText ?? \"\")}. ` +\n `The factor was taken from the lower end, so ${servings} is what this makes at that end. ` +\n `Taken from ${recipe.yieldMax} it would be ${round(servings / recipe.yieldMax)} instead.`,\n );\n }\n\n const unit = recipe.yieldUnit;\n if (unit && !/personne|people|serving|portion|part\\b/i.test(unit)) {\n notes.push(\n `This page states its yield in ${quoteForeign(unit)} rather than in servings, so asking for ` +\n `${servings} multiplied the quantities by ${round(factor)} to give ${servings} ${quoteForeign(unit)}.`,\n );\n }\n\n return { factor, notes };\n}\n\nfunction round(value: number): number {\n return Math.round(value * 1000) / 1000;\n}\n\nexport interface BuildOptions {\n servings: number | null;\n sections: readonly Section[];\n /** Steps to return, since a long method is the bulk of a recipe. */\n maxSteps?: number;\n /** Characters kept per step. */\n maxStepChars?: number;\n /**\n * Names this recipe at the front of each of its notes. An answer carrying\n * several recipes needs to say which one a note is about; an answer carrying\n * one does not, and the name would only repeat what the answer already says.\n */\n label?: string;\n /**\n * Whether the choice of sections is said here. An answer built from several\n * recipes says it once for the call rather than once per recipe.\n */\n announceSections?: boolean;\n}\n\nexport function buildRecipeView(recipe: RecipeDetail, options: BuildOptions): RecipeView {\n const { factor, notes: yieldNotes } = resolveFactor(recipe, options.servings);\n const wants = (section: Section) => options.sections.includes(section);\n\n const ingredients =\n factor === null || factor === 1\n ? passthroughIngredients(recipe.ingredients, recipe.language)\n : scaleIngredients(recipe.ingredients, { factor, language: recipe.language });\n\n const notes: string[] = [...yieldNotes];\n\n // A page can carry no ingredient list at all, and only reading it tells that\n // apart from a recipe. Returning an empty list in silence reads as a recipe\n // that needs no ingredients.\n if (recipe.ingredients.length === 0) {\n notes.push(\n `${recipe.sourceName} publishes no ingredient list on this page. It is likely a page about ` +\n \"the dish rather than a recipe for it, so there is nothing here to scale. Search again \" +\n \"and pick another row.\",\n );\n }\n\n const rounded = ingredients.filter((entry) => entry.scaling === \"rounded\");\n const unscaled = ingredients.filter((entry) => entry.scaling === \"unscaled\");\n\n if (rounded.length > 0) {\n notes.push(\n `${rounded.length} line(s) did not land on the exact product; each says in its own note ` +\n \"what happened to it.\",\n );\n }\n if (unscaled.length > 0 && factor !== null && factor !== 1) {\n notes.push(\n `${unscaled.length} line(s) carry no quantity to multiply and are repeated as published.`,\n );\n }\n if (recipe.license) {\n notes.push(\n `Published under ${quoteForeign(recipe.license.title)}: ${quoteForeign(recipe.license.url)}`,\n );\n }\n\n const omitted = SECTIONS.filter((section) => !wants(section));\n if (options.announceSections !== false && omitted.length > 0) {\n notes.push(\n `Not requested, so not returned: ${omitted.join(\", \")}. Name them in 'sections' to see them.`,\n );\n }\n\n const steps = wants(\"steps\") ? recipe.steps : [];\n const shownSteps = steps\n .slice(0, options.maxSteps ?? steps.length)\n .map((step) => truncate(step, options.maxStepChars ?? step.length));\n if (steps.length > shownSteps.length) {\n notes.push(\n `The method runs to ${steps.length} steps and ${shownSteps.length} are here. Raise ` +\n \"'max_steps' for the rest.\",\n );\n }\n\n const payload: RecipePayload = {\n id: recipe.id,\n source: recipe.source,\n source_name: recipe.sourceName,\n language: recipe.language,\n title: recipe.title,\n url: recipe.url,\n image_url: recipe.imageUrl,\n yield: {\n original_count: recipe.yieldCount,\n original_max: recipe.yieldMax,\n original_text: recipe.yieldText,\n requested: options.servings,\n unit: recipe.yieldUnit,\n factor: factor === null ? null : round(factor),\n },\n ingredients: wants(\"ingredients\") ? ingredients.map(toIngredientPayload) : [],\n steps: shownSteps,\n prep_minutes: wants(\"times\") ? recipe.prepMinutes : null,\n cook_minutes: wants(\"times\") ? recipe.cookMinutes : null,\n total_minutes: wants(\"times\") ? recipe.totalMinutes : null,\n category: recipe.category,\n author: recipe.author,\n rating: recipe.rating,\n nutrition: wants(\"nutrition\") ? recipe.nutrition : null,\n equipment: wants(\"equipment\") ? recipe.equipment : [],\n tips: wants(\"tips\") ? recipe.tips : [],\n license: recipe.license,\n attribution: recipe.attribution,\n sections_returned: [...options.sections],\n sections_omitted: omitted,\n scaling_summary: {\n scaled_count: ingredients.filter((entry) => entry.scaling === \"scaled\").length,\n rounded_count: rounded.length,\n unscaled_count: unscaled.length,\n },\n };\n\n if (options.label) {\n const label = quoteForeign(options.label);\n return { payload, notes: notes.map((line) => `${label}: ${line}`), ingredients };\n }\n return { payload, notes, ingredients };\n}\n\n/** The yield as a reader would say it, with what was asked for beside it. */\nexport function renderYield(payload: RecipePayload): string {\n const published = payload.yield.original_text ?? \"an amount the page does not state\";\n if (payload.yield.factor === null) return `Yields ${quoteForeign(published)} (as published).`;\n return `Yields ${quoteForeign(published)} as published, scaled by ${payload.yield.factor} for ${payload.yield.requested}.`;\n}\n","/**\n * Which language an ingredient line is written in.\n *\n * A list can hold French lines and English lines at once, because a cook\n * comparing two versions of a dish keeps both in front of them. The language\n * decides how a number is written, how a noun agrees with it, and which\n * vocabulary of measures applies, so it is settled per line rather than once\n * for a whole list.\n */\n\nexport type Language = \"fr\" | \"en\";\n\n/** What a caller may ask for: a language, or a reading taken per line. */\nexport type LanguageChoice = Language | \"auto\";\n\n/**\n * Structural words, which are the reliable signal.\n *\n * A line names its ingredient in whatever language the page is written in, and\n * an ingredient name travels: a French recipe asks for \"Mountain Dew\" and an\n * English one for \"crème fraîche\". The small words that hold a line together do\n * not travel, so \"de\", \"à\" and \"of\" say more about the line than the name of\n * the food does.\n */\nconst FRENCH_WORDS =\n /(?:^|[\\s(])(?:de|du|des|à|au|aux|la|le|les|un|une|quelques|dans|avec|sans|pour|selon|environ)(?=[\\s),.]|$)|\\bd'|\\bl'|\\bqu'/iu;\n\n/**\n * The bare article \"a\" is left out on purpose. French recipes abbreviate\n * \"cuillère à soupe\" as \"c a s\", dropping the accent, and counting that \"a\" as\n * English evidence would send a French line through the English vocabulary.\n */\nconst ENGLISH_WORDS =\n /(?:^|[\\s(])(?:of|an|the|and|with|without|for|into|about|each|per|plus|freshly|finely|roughly|coarsely|optional)(?=[\\s),.]|$)/iu;\n\n/** Letters English does not write, which no ingredient name borrows either. */\nconst FRENCH_LETTERS = /[àâäçéèêëîïôöùûüœ]/iu;\n\n/**\n * Foods each language names its own way.\n *\n * Weak evidence, and deliberately so: a name is the part of a line most likely\n * to be borrowed, and a structural word outranks any number of these. It is\n * enough to settle a line that carries nothing else, such as \"6 oeufs\", where\n * every other signal is silent.\n */\nconst FRENCH_FOODS =\n /\\b(?:oeufs?|œufs?|sel|poivre|sucres?|farine|beurre|lait|crème|creme|huile|ail|oignons?|fromage|pain|poulet|boeuf|bœuf|riz|pâtes|pates|jambon|citrons?|pommes?|vin|eau|persil|thym|miel|jaunes?|blancs?)\\b/iu;\n\nconst ENGLISH_FOODS =\n /\\b(?:eggs?|salt|pepper|sugar|flour|butter|milk|cream|oil|garlic|onions?|cheese|bread|chicken|beef|rice|pasta|ham|lemons?|apples?|wine|water|parsley|thyme|honey|yolks?|whites?)\\b/iu;\n\n/** Weight of one structural signal, which outranks a vocabulary match. */\nconst STRUCTURAL = 3;\n/** Weight of a measure only one of the two vocabularies carries. */\nconst VOCABULARY = 2;\n/** Weight of a food name, which is the signal a line borrows most readily. */\nconst NAME = 1;\n\nexport interface LanguageEvidence {\n language: Language;\n french: number;\n english: number;\n}\n\n/**\n * Read the language off one line.\n *\n * `frenchUnit` and `englishUnit` say whether each vocabulary recognised a\n * measure in the line. They count for less than a structural word, because a\n * French line can name an English measure in passing: \"1 dose (cup) de Mountain\n * Dew\" is French with an English gloss inside it, and the \"de\" is what settles\n * it.\n *\n * English is the answer when nothing points either way, which is what an\n * ingredient with no small words and no measure gets.\n */\nexport function readLanguage(\n line: string,\n evidence: { frenchUnit: boolean; englishUnit: boolean } = { frenchUnit: false, englishUnit: false },\n): LanguageEvidence {\n let french = 0;\n let english = 0;\n\n if (FRENCH_WORDS.test(line)) french += STRUCTURAL;\n if (FRENCH_LETTERS.test(line)) french += STRUCTURAL;\n if (ENGLISH_WORDS.test(line)) english += STRUCTURAL;\n if (evidence.frenchUnit) french += VOCABULARY;\n if (evidence.englishUnit) english += VOCABULARY;\n if (FRENCH_FOODS.test(line)) french += NAME;\n if (ENGLISH_FOODS.test(line)) english += NAME;\n\n return { language: french > english ? \"fr\" : \"en\", french, english };\n}\n\n/** The language a list is written in, when a caller wants one answer for all of it. */\nexport function readListLanguage(lines: string[]): Language {\n let french = 0;\n let english = 0;\n for (const line of lines) {\n const evidence = readLanguage(line);\n french += evidence.french;\n english += evidence.english;\n }\n return french > english ? \"fr\" : \"en\";\n}\n","/**\n * Cooking measures in French and in English, and what scaling means for each.\n *\n * What matters about a measure is how far its number divides before it stops\n * naming something a cook can produce.\n * Doubling \"200 g\" gives \"400 g\", to the tenth of a gram. Doubling \"1 pincée\"\n * gives \"2 pincées\", which is the whole of what a pinch can say: the count\n * carries the quantity, and the size of one pinch is the hand's business.\n */\n\nimport type { Language } from \"./language.js\";\n\nexport type UnitKind =\n /** Mass or volume: scales continuously and cleanly. */\n | \"measured\"\n /** Spoons, cups, cloves, sachets: scales, but only to sensible fractions. */\n | \"portioned\"\n /**\n * Pinches, dashes, handfuls, pincées, poignées: a real amount, held to no\n * better than the hand that produces it. The count is multiplied and lands on\n * a whole one, and the line says the measure is approximate.\n */\n | \"approximate\";\n\nexport type UnitSystem = \"metric\" | \"imperial\" | \"none\";\n\nexport interface UnitInfo {\n /** Canonical singular form, used when rewriting the ingredient line. */\n canonical: string;\n kind: UnitKind;\n system: UnitSystem;\n /** Plural form when it is not simply the singular plus an \"s\". */\n plural?: string;\n /** A symbol such as \"g\" or \"ml\", which never takes a plural mark. */\n symbol?: true;\n}\n\n/**\n * Measures written the same way in both languages: the metric symbols.\n *\n * \"g\", \"kg\", \"ml\" and \"l\" are read off a French line and an English line alike,\n * and they take no plural mark in either.\n */\nconst SHARED_UNITS: Record<string, UnitInfo> = {\n mg: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n g: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gr: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kg: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilo: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilos: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n ml: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n cl: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n dl: { canonical: \"dl\", kind: \"measured\", system: \"metric\", symbol: true },\n l: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litre: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litres: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n};\n\n/**\n * Keys are matched lowercased and accent-stripped, so a single entry covers\n * \"cuillere\", \"cuillère\" and \"Cuillères\".\n */\nconst FRENCH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogramme: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogrammes: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitre: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitres: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n \"cuillere a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuilleres a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a s\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n cas: {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuillere a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"cuilleres a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a c\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n cac: {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n verre: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n verres: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n bol: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n bols: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n tasse: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n tasses: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n gousse: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n gousses: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n tranche: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n tranches: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n botte: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n bottes: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n boite: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n boites: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n pot: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n pots: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n brique: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n briques: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n feuille: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n feuilles: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n branche: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n branches: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readPartitiveMeasure` explains what else lands here.\n bouchon: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n bouchons: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n larme: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n larmes: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n doigt: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n doigts: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n nuage: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n nuages: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n louche: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n louches: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n lichette: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n lichettes: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n pointe: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pointes: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pincee: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n pincees: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n trait: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n traits: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n filet: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n filets: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n goutte: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n gouttes: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n poignee: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n poignees: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n // \"noix\" carries its own plural mark already.\n noix: { canonical: \"noix\", kind: \"approximate\", system: \"none\", plural: \"noix\" },\n soupcon: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n soupcons: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n};\n\n/**\n * Keys are matched lowercased with dots dropped, so a single entry covers\n * \"Tbsp\", \"tbsp.\" and \"TBSP\".\n */\nconst ENGLISH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n milligram: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n milligrams: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n gram: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grams: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogram: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilograms: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliter: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliters: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n liter: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n liters: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n\n // Imperial mass\n oz: { canonical: \"oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n ounce: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n ounces: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n lb: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n pound: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n pounds: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n\n // Imperial volume\n \"fl oz\": { canonical: \"fl oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n \"fluid ounce\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n \"fluid ounces\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n pint: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n pints: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n quart: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n quarts: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n gallon: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n gallons: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n tsp: { canonical: \"tsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n teaspoon: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n teaspoons: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n tbsp: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tbs: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tablespoon: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n tablespoons: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n cup: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n cups: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n can: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n cans: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n jar: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n jars: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n packet: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n packets: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n package: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n packages: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n clove: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n cloves: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n slice: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n slices: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n stick: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n sticks: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n stalk: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n stalks: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n sprig: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n sprigs: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n bunch: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n bunches: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n head: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n heads: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n sheet: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n sheets: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n leaf: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n leaves: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n ea: { canonical: \"ea\", kind: \"portioned\", system: \"none\", symbol: true },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readContainerLoad` explains what else lands here.\n capful: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n capfuls: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n glug: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n glugs: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n dollop: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n dollops: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n squeeze: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n squeezes: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n sprinkle: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkles: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkling: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n sprinklings: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n grating: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n gratings: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n twist: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n twists: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n smidgen: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n smidgens: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n pinch: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n pinches: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n dash: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n dashes: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n splash: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n splashes: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n drizzle: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n drizzles: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n handful: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n handfuls: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n drop: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n drops: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n knob: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n knobs: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n};\n\nconst VOCABULARY: Record<Language, Record<string, UnitInfo>> = {\n fr: FRENCH_UNITS,\n en: ENGLISH_UNITS,\n};\n\n/**\n * Lowercase, strip accents and drop abbreviation dots, so a lookup survives the\n * spellings a recipe actually uses. Recipes write \"c. à soupe\" and \"Tbsp.\" as\n * readily as the full words, and an unrecognised measure is worse than a wrong\n * one: the amount falls through to the countable branch and gets rounded as\n * though a spoonful were an indivisible object.\n */\nexport function normalizeUnitKey(text: string): string {\n return text\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/\\./g, \" \")\n // A recipe that does not know how many it will be writes the plural mark in\n // brackets: \"4 cuillère(s) à soupe\". The measure is the word without it.\n .replace(/\\((?:s|x|es)\\)/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/** Longest keys first, so \"cuillère à soupe\" wins over \"cuillère\". */\nfunction orderedKeys(vocabulary: Record<string, UnitInfo>): string[] {\n return Object.keys(vocabulary).sort(\n (a, b) => b.split(\" \").length - a.split(\" \").length || b.length - a.length,\n );\n}\n\nconst UNIT_KEYS: Record<Language, string[]> = {\n fr: orderedKeys(FRENCH_UNITS),\n en: orderedKeys(ENGLISH_UNITS),\n};\n\nexport function unitKeys(language: Language): string[] {\n return UNIT_KEYS[language];\n}\n\nexport function lookupUnit(text: string, language: Language): UnitInfo | null {\n return VOCABULARY[language][normalizeUnitKey(text)] ?? null;\n}\n\n/**\n * Matches a number followed by a measure anywhere in a piece of text.\n *\n * Used to spot a quantity the parser did not take, such as the second half of\n * \"1 Tbsp vanilla sugar OR 1 tsp vanilla extract\", which would otherwise sit in\n * a scaled line still saying what the original said.\n */\nconst EMBEDDED: Record<Language, RegExp> = {\n fr: buildEmbedded(\"fr\"),\n en: buildEmbedded(\"en\"),\n};\n\nfunction buildEmbedded(language: Language): RegExp {\n const keys = UNIT_KEYS[language].map((key) => key.replace(/ /g, \"\\\\s+\"));\n // The digits and the whitespace are kept in separate pieces that cannot both\n // match a space. Letting them overlap makes the engine try every way of\n // splitting a run of spaces between them, which turns a long line into\n // seconds of work for an answer that was always going to be no.\n return new RegExp(`\\\\d[\\\\d.,/]*\\\\s*(?:${keys.join(\"|\")})\\\\b`, \"i\");\n}\n\nexport function hasEmbeddedMeasure(text: string, language: Language): boolean {\n return EMBEDDED[language].test(text);\n}\n\n/**\n * Words that stand where a measure would and name no container: \"un peu de\n * sel\" states that there is some salt, and multiplying it says nothing.\n */\nconst NOT_A_MEASURE = new Set([\n \"peu\",\n \"beaucoup\",\n \"plus\",\n \"moins\",\n \"assez\",\n \"trop\",\n \"autant\",\n \"tant\",\n \"moitie\",\n \"quart\",\n \"tiers\",\n \"reste\",\n \"melange\",\n \"ensemble\",\n]);\n\n/**\n * Read a French measure named with a container or a gesture the vocabulary has\n * no entry for.\n *\n * What makes a measure approximate is that its size belongs to whoever pours\n * it: a bouchon, a poignée, a ramequin hold what they hold, and the recipe's\n * proportion lives in how many are asked for. French marks that grammatically,\n * by placing the noun between the amount and the partitive that introduces the\n * thing measured: \"un bouchon de rhum\", \"2 bouquets de persil\". A noun in that\n * position measures whatever follows it, so a container nobody thought to list\n * is read by the same rule as the ones that are, and the vocabulary only has to\n * carry the words whose plural or spelling the rule would get wrong.\n *\n * The amount has to come first. A line opening on the noun, as in \"beurre\n * pommade\", carries no quantity, and inventing one from the grammar would put a\n * number where the recipe wrote none.\n */\nexport function readPartitiveMeasure(text: string): { unit: UnitInfo; rest: string } | null {\n const match = /^\\s*(\\p{L}+)\\s+(?=(?:de|du|des)\\s|d')/u.exec(text);\n if (!match) return null;\n\n const word = match[1]!;\n if (word.length < 3) return null;\n if (NOT_A_MEASURE.has(normalizeUnitKey(word))) return null;\n if (lookupUnit(word, \"fr\")) return null;\n\n const canonical = frenchSingular(word);\n return {\n unit: {\n canonical,\n kind: \"approximate\",\n system: \"none\",\n plural: frenchPlural(canonical),\n },\n rest: text.slice(match[0].length),\n };\n}\n\n/**\n * Read an English measure named after the thing that holds it: a capful, a\n * spoonful, a jarful.\n *\n * What makes a measure approximate is that its size belongs to whoever pours\n * it rather than to a standard, and English marks that in the word itself. The\n * suffix -ful means \"as much as one of these holds\", and what a cap, a bowl or\n * a handful holds is whatever the one in the kitchen holds. Any noun built that\n * way is therefore a measure of the same family as a pinch, which is why the\n * suffix is read as a rule: a container nobody thought to list is understood\n * the first time it appears, and the vocabulary only has to carry the gestures\n * whose name says nothing about their size either.\n */\nexport function readContainerLoad(word: string): UnitInfo | null {\n const key = normalizeUnitKey(word);\n // At least three letters name the container, which keeps \"awful\" out of the\n // kitchen.\n if (!/^[a-z]{3,}fuls?$/.test(key)) return null;\n\n const canonical = key.endsWith(\"fuls\") ? key.slice(0, -1) : key;\n return { canonical, kind: \"approximate\", system: \"none\", plural: `${canonical}s` };\n}\n\n/**\n * The singular of a French noun a line wrote in the plural, so the rewrite can\n * put it back in either number.\n *\n * \"ananas\", \"jus\" and \"anis\" carry their -s in the singular, and \"morceaux\"\n * comes from \"morceau\", so the ending decides rather than the last letter\n * alone.\n */\nfunction frenchSingular(word: string): string {\n if (/eaux$/i.test(word)) return word.slice(0, -1);\n if (/aux$/i.test(word)) return `${word.slice(0, -3)}al`;\n if (/[aiou]s$/i.test(word)) return word;\n if (/s$/i.test(word) && word.length > 3) return word.slice(0, -1);\n return word;\n}\n\n/** The plural French writes for a noun, or the noun itself when it takes no mark. */\nfunction frenchPlural(word: string): string {\n if (/[sxz]$/i.test(word)) return word;\n if (/eau$/i.test(word)) return `${word}x`;\n if (/al$/i.test(word)) return `${word.slice(0, -2)}aux`;\n return `${word}s`;\n}\n\n/**\n * What a kitchen usually takes each approximate measure to be.\n *\n * Offered as words for a note, never as the quantity: writing \"2 teaspoons\"\n * where the page wrote \"4 pinches\" puts a figure on the page it never claimed,\n * and the cook is the one holding the pinch.\n */\nconst APPROXIMATE_EQUIVALENT: Record<string, string> = {\n capful: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n glug: \"commonly taken as about two tablespoons poured free-hand\",\n dollop: \"commonly taken as about a heaped tablespoon\",\n pinch: \"commonly taken as about half a teaspoon\",\n dash: \"commonly taken as about an eighth of a teaspoon\",\n splash: \"commonly taken as about a tablespoon\",\n handful: \"commonly taken as about a quarter of a cup\",\n knob: \"commonly taken as about a tablespoon of butter\",\n drizzle: \"commonly taken as about a teaspoon poured in a thin line\",\n pincee: \"commonly taken as about half a teaspoon\",\n poignee: \"commonly taken as about a quarter of a cup\",\n bouchon: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n goutte: \"commonly taken as a single drop\",\n filet: \"commonly taken as about a teaspoon poured in a thin line\",\n noix: \"commonly taken as about a tablespoon of butter\",\n louche: \"commonly taken as about half a cup\",\n soupcon: \"commonly taken as the smallest amount a spoon tip carries\",\n};\n\n/** The everyday equivalence for an approximate measure, when there is a settled one. */\nexport function approximateEquivalent(unit: UnitInfo): string | null {\n return APPROXIMATE_EQUIVALENT[normalizeUnitKey(unit.canonical)] ?? null;\n}\n\n/**\n * Ladders, used to keep a scaled amount at a human size.\n *\n * Multiplying a recipe by thirty is arithmetically fine and practically poor:\n * \"8335 g of sugar\" is correct, and nobody weighs eight thousand grams. Each\n * measured unit therefore knows the unit above and below it, so a large amount\n * climbs the ladder and a small one comes back down. Each system keeps its own\n * ladder, because converting between them changes what the recipe said.\n */\ninterface UnitStep {\n /** Unit to switch to, and how many of the current unit it holds. */\n to: string;\n per: number;\n /** Which vocabulary the step's spelling comes from. */\n language: Language;\n}\n\nconst PROMOTIONS: Record<string, UnitStep> = {\n mg: { to: \"g\", per: 1000, language: \"en\" },\n g: { to: \"kg\", per: 1000, language: \"en\" },\n ml: { to: \"l\", per: 1000, language: \"en\" },\n cl: { to: \"l\", per: 100, language: \"en\" },\n dl: { to: \"l\", per: 10, language: \"en\" },\n oz: { to: \"lb\", per: 16, language: \"en\" },\n ounce: { to: \"pound\", per: 16, language: \"en\" },\n \"fl oz\": { to: \"pint\", per: 20, language: \"en\" },\n pint: { to: \"quart\", per: 2, language: \"en\" },\n quart: { to: \"gallon\", per: 4, language: \"en\" },\n};\n\nconst DEMOTIONS: Record<string, UnitStep> = {\n // Spoons and cups hold a fixed volume, so a share of one is stated in the\n // smaller spoon rather than as a fraction no measuring set carries. The\n // spelling of the step matches the spelling of the unit it comes from, so a\n // line written in abbreviations stays in abbreviations, and a French line\n // stays in French.\n cup: { to: \"tablespoon\", per: 16, language: \"en\" },\n tablespoon: { to: \"teaspoon\", per: 3, language: \"en\" },\n tbsp: { to: \"tsp\", per: 3, language: \"en\" },\n \"cuillere a soupe\": { to: \"cuillere a cafe\", per: 3, language: \"fr\" },\n tasse: { to: \"cuillere a soupe\", per: 16, language: \"fr\" },\n\n kg: { to: \"g\", per: 1000, language: \"en\" },\n g: { to: \"mg\", per: 1000, language: \"en\" },\n l: { to: \"cl\", per: 100, language: \"en\" },\n dl: { to: \"cl\", per: 10, language: \"en\" },\n cl: { to: \"ml\", per: 10, language: \"en\" },\n lb: { to: \"oz\", per: 16, language: \"en\" },\n pound: { to: \"ounce\", per: 16, language: \"en\" },\n gallon: { to: \"quart\", per: 4, language: \"en\" },\n quart: { to: \"pint\", per: 2, language: \"en\" },\n pint: { to: \"fl oz\", per: 20, language: \"en\" },\n};\n\n/**\n * The unit one step down the ladder, with how many of it fit in one of the\n * current unit. Null at the bottom of a ladder, where there is nothing smaller\n * to express the amount in.\n */\nexport function demoteUnit(unit: UnitInfo): { unit: UnitInfo; per: number } | null {\n const step = DEMOTIONS[normalizeUnitKey(unit.canonical)];\n if (!step) return null;\n const target = lookupUnit(step.to, step.language);\n return target ? { unit: target, per: step.per } : null;\n}\n\n/**\n * Spoons and cups: a portion, and at the same time a fixed volume. The volume\n * is what lets a share of one be restated in a smaller spoon.\n */\nexport function isSpoonMeasure(unit: UnitInfo): boolean {\n return /^(cup|tablespoon|Tbsp|teaspoon|tsp|cuillère à soupe|cuillère à café|tasse)$/.test(\n unit.canonical,\n );\n}\n\n/** How finely a kitchen can divide one of a counted thing. */\nexport type Divisibility =\n /** An egg: half of one is not something a cook takes out of the shell. */\n | \"whole\"\n /** A clove, a tin, a sachet: half of it is a quantity a kitchen can take. */\n | \"half\"\n /** An onion, an apple: a knife takes it to quarters. */\n | \"quarter\";\n\n/**\n * How far one of a measure divides.\n *\n * A measure divides as far as half of what it holds stays a quantity a kitchen\n * can take out. Almost always it does: what a tin, a jar, a sachet, a bottle or\n * a carton holds is\n * poured, weighed or spooned, so half a tin of tomatoes is half a tin of\n * tomatoes and the rest keeps in the fridge. A leaf of gelatine and a sprig of\n * thyme are cut with a knife. A handful is halved by taking less.\n *\n * What does not divide is what has no half a cook can measure out. An egg is\n * the case: half of one means beating it and weighing the result, which no\n * recipe asks for, and the same holds for a yolk and a white on their own. That\n * is a fact about the contents, so it is decided where the item is named rather\n * than here.\n *\n * A doubtful word takes the half. Half an approximate measure is nearer the\n * truth than a whole one rounded up, and a cook reading \"1/2 sachet\" knows what\n * to do with it. A measure therefore answers `half`, apart from the few named\n * below, and the one thing that does not divide is decided from the item's own\n * name.\n */\nexport function unitDivisibility(unit: UnitInfo): Divisibility {\n return QUARTERED_MEASURE.test(unit.canonical) ? \"quarter\" : \"half\";\n}\n\n/**\n * Measures a cook takes a quarter of.\n *\n * The half is as far as the criterion goes on its own, because that is the\n * share most measures give up by eye. These answer the size question\n * differently, in two ways. A pot de crème fraîche, a bouteille, a jar and a\n * block hold enough that a quarter is still a portion someone serves and the\n * rest still keeps: a quarter of a pot is a couple of spoonfuls, a quarter of a\n * bouteille is a glass, a quarter of a block of tofu is a piece cut on a board.\n * A gousse and a tranche are already cut off something larger, and the board\n * that produced one takes a corner off it in the same gesture: a quarter of a\n * gousse d'ail is what a knife scrapes into a pan, a quarter of a tranche de\n * pain is a crouton.\n *\n * Each measure is listed in either language, because how far one of them\n * divides has nothing to do with the words a page uses for it.\n *\n * The pattern is exported because any of these words can stand where the\n * measure goes or inside the name of what is counted, and both readings answer\n * to the same list.\n */\nexport const QUARTERED_MEASURE =\n /\\b(pots?|bouteilles?|bottles?|jars?|blocs?|blocks?|gousses?|tranches?|slices?)\\b/i;\n\n/**\n * True for a measure that counts pieces without saying anything about them.\n *\n * \"ea\" is short for \"each\": it announces that the figure is a count of objects\n * and names no measure of any one of them. How far the count divides therefore\n * belongs to the thing standing beside it, and the marker itself has nothing to\n * say in a line a cook reads.\n */\nexport function countsBarePieces(unit: UnitInfo): boolean {\n return unit.canonical === \"ea\";\n}\n\nexport interface ChosenUnit {\n unit: UnitInfo;\n /** What to multiply an amount in the original unit by to express it in this one. */\n ratio: number;\n}\n\n/**\n * Choose the unit a cook would actually write a quantity in, and say how to get\n * there.\n *\n * A ratio rather than a converted number, because a range has two bounds and\n * they have to end up in the same unit: converting each on its own gives the\n * unreadable \"13 oz to 1.5 pounds\". The caller picks one bound to choose from,\n * then applies the ratio to both.\n *\n * Demotion repeats while the amount is under one, so a quantity divided a\n * thousandfold walks all the way down its ladder instead of rounding away.\n * Promotion takes one step, at a full unit of the step above, so 999 g stays\n * grams and 1000 g becomes a kilo.\n */\nexport function chooseReadableUnit(unit: UnitInfo, amount: number): ChosenUnit {\n if (unit.kind !== \"measured\" || !Number.isFinite(amount) || amount <= 0) {\n return { unit, ratio: 1 };\n }\n\n let current = unit;\n let ratio = 1;\n\n while (amount * ratio < 1) {\n const step = demoteUnit(current);\n if (!step) break;\n ratio *= step.per;\n current = step.unit;\n }\n\n const up = PROMOTIONS[normalizeUnitKey(current.canonical)];\n if (up && amount * ratio >= up.per) {\n const target = lookupUnit(up.to, up.language);\n if (target) {\n ratio /= up.per;\n current = target;\n }\n }\n\n return { unit: current, ratio };\n}\n\n/**\n * Render a measure for a given amount, choosing singular or plural.\n *\n * The threshold differs by language. English marks the plural above one, so 1.5\n * is plural: \"1.5 cups\". French takes it from two onwards, so 1,5 stays\n * singular: \"1,5 cuillère à soupe\".\n */\nexport function formatUnit(unit: UnitInfo, amount: number, language: Language): string {\n if (unit.symbol) return unit.canonical;\n const singular = language === \"fr\" ? amount < 2 : amount <= 1;\n if (singular) return unit.canonical;\n return unit.plural ?? `${unit.canonical}s`;\n}\n","/**\n * Reading quantities out of ingredient lines, in French and in English.\n *\n * Recipes are written as prose rather than as data: \"200 g de farine\",\n * \"450 g (1 pound) spaghetti\", \"3 ¼ cups quick oats\", \"sel\". Everything\n * downstream depends on reading these correctly, so the parser is deliberate\n * about what it recognises and returns nothing rather than guessing.\n */\n\nimport type { Language, LanguageChoice } from \"./language.js\";\nimport { readLanguage } from \"./language.js\";\nimport type { UnitInfo } from \"./units.js\";\nimport {\n lookupUnit,\n normalizeUnitKey,\n readContainerLoad,\n readPartitiveMeasure,\n unitKeys,\n} from \"./units.js\";\n\nexport interface ParsedQuantity {\n amount: number;\n /** Characters consumed from the start of the line. */\n length: number;\n}\n\n/** Unicode vulgar fractions, which hand-written recipes use freely. */\nconst VULGAR_FRACTIONS: Record<string, number> = {\n \"½\": 0.5,\n \"⅓\": 1 / 3,\n \"⅔\": 2 / 3,\n \"¼\": 0.25,\n \"¾\": 0.75,\n \"⅕\": 0.2,\n \"⅖\": 0.4,\n \"⅗\": 0.6,\n \"⅘\": 0.8,\n \"⅙\": 1 / 6,\n \"⅚\": 5 / 6,\n \"⅛\": 0.125,\n \"⅜\": 0.375,\n \"⅝\": 0.625,\n \"⅞\": 0.875,\n};\n\nconst VULGAR_CLASS = Object.keys(VULGAR_FRACTIONS).join(\"\");\n\n/**\n * Read a leading amount.\n *\n * Handles, in order of precedence: a whole number followed by a fraction, in\n * either the glyph form \"3 ¼\" or the written form \"1 1/2\"; a bare fraction; a\n * bare glyph; and a decimal. French writes the decimal with a comma, so \"1,5\"\n * is one and a half there and two separate readings in English, where a comma\n * groups thousands.\n *\n * Returns null when the line does not start with a number, which is the normal\n * case for \"sel\" or \"Freshly ground pepper\".\n */\nexport function parseLeadingQuantity(text: string, language: Language): ParsedQuantity | null {\n const trimmed = text.trimStart();\n const offset = text.length - trimmed.length;\n\n // \"3 ¼\" and \"3¼\" before the bare \"3\", so the longest reading wins.\n const mixedGlyph = new RegExp(`^(\\\\d+)\\\\s*([${VULGAR_CLASS}])`).exec(trimmed);\n if (mixedGlyph) {\n const whole = Number(mixedGlyph[1]);\n const fraction = VULGAR_FRACTIONS[mixedGlyph[2]!]!;\n return { amount: whole + fraction, length: offset + mixedGlyph[0].length };\n }\n\n const mixed = /^(\\d+)\\s+(\\d+)\\s*\\/\\s*(\\d+)/.exec(trimmed);\n if (mixed) {\n const denominator = Number(mixed[3]);\n if (denominator !== 0) {\n return {\n amount: Number(mixed[1]) + Number(mixed[2]) / denominator,\n length: offset + mixed[0].length,\n };\n }\n }\n\n const fraction = /^(\\d+)\\s*\\/\\s*(\\d+)/.exec(trimmed);\n if (fraction) {\n const denominator = Number(fraction[2]);\n // A denominator of zero is not a quantity. Reading the numerator alone\n // would leave \"/0\" in the item name and scale a number nobody wrote.\n if (denominator === 0) return null;\n return { amount: Number(fraction[1]) / denominator, length: offset + fraction[0].length };\n }\n\n const glyph = trimmed[0];\n if (glyph && glyph in VULGAR_FRACTIONS) {\n return { amount: VULGAR_FRACTIONS[glyph]!, length: offset + 1 };\n }\n\n // English groups thousands with the comma it never uses as a decimal mark, so\n // \"1,500 g\" is fifteen hundred grams. Reading the digits up to the comma and\n // stopping there answers 1 for a line that said 1500, and leaves \",500 g\"\n // behind in the item name.\n const decimal = (\n language === \"fr\" ? /^(\\d+(?:[.,]\\d+)?)/ : /^(\\d{1,3}(?:,\\d{3})+(?:\\.\\d+)?|\\d+(?:\\.\\d+)?)/\n ).exec(trimmed);\n if (decimal) {\n const written = decimal[1]!;\n const amount = Number(\n language === \"fr\" ? written.replace(\",\", \".\") : written.replace(/,/g, \"\"),\n );\n if (Number.isFinite(amount)) return { amount, length: offset + decimal[0].length };\n }\n\n if (language === \"en\") {\n const written = parseWrittenFraction(trimmed);\n if (written) return { amount: written.amount, length: offset + written.length };\n }\n\n return null;\n}\n\n/** How many of the part a line names: \"two thirds\", \"a quarter\", \"half\". */\nconst WRITTEN_NUMERATORS: Record<string, number> = { a: 1, an: 1, one: 1, two: 2, three: 3 };\n\n/** What the part is a part of. */\nconst WRITTEN_DENOMINATORS: Record<string, number> = {\n half: 2,\n halves: 2,\n third: 3,\n thirds: 3,\n quarter: 4,\n quarters: 4,\n fourth: 4,\n fourths: 4,\n};\n\n/**\n * Read a fraction an English line spells out, as in \"half a bottle\" or \"two\n * thirds of a cup\".\n *\n * Recipes write the small fractions in words as readily as in figures, and a\n * line opening on one of them carries an amount like any other. The numerator\n * is optional because \"half\" alone is the common form.\n *\n * What follows decides whether the words are a quantity at all. A share of one\n * thing, \"half a lemon\" or \"two thirds of a cup\", states an amount of its own.\n * A share of a definite thing, \"half of the dough\", points back at an amount\n * stated elsewhere, and multiplying it would answer with a number that belongs\n * to another line.\n */\nfunction parseWrittenFraction(text: string): ParsedQuantity | null {\n const match =\n /^(?:(a|an|one|two|three)[\\s-]+)?(halves|half|thirds|third|quarters|quarter|fourths|fourth)\\b/i.exec(\n text,\n );\n if (!match) return null;\n\n const numerator = match[1] ? WRITTEN_NUMERATORS[match[1].toLowerCase()] : 1;\n const denominator = WRITTEN_DENOMINATORS[match[2]!.toLowerCase()];\n if (!numerator || !denominator) return null;\n\n const rest = text\n .slice(match[0].length)\n .replace(/^\\s*of\\s+/i, \"\")\n .trimStart();\n if (!/^an?\\s/i.test(rest) && !takeUnit(rest, \"en\").unit) return null;\n\n return { amount: numerator / denominator, length: match[0].length };\n}\n\nexport interface ParsedRange extends ParsedQuantity {\n /** Upper bound. `amount` carries the lower one. */\n max: number;\n /** How the range was written, so the rewrite can keep the same shape. */\n separator: string;\n}\n\n/**\n * Read a leading range such as \"225–500\", \"3-4\", \"2 to 3\" or \"2 à 3\".\n *\n * Recipes use ranges where the exact amount is the cook's call, and both bounds\n * describe the same quantity. Reading only the first one is worse than reading\n * neither: the second number survives unscaled into the answer and contradicts\n * it.\n *\n * A descending pair is not a range. \"1/2 3\" is two amounts the parser has no\n * business joining, and a dash between two numbers is a range only when the\n * second is the larger.\n */\nexport function parseLeadingRange(text: string, language: Language): ParsedRange | null {\n const low = parseLeadingQuantity(text, language);\n if (!low) return null;\n\n const after = text.slice(low.length);\n // A written separator needs whitespace around it, so \"5 tomatoes\" is not read\n // as \"5 to\" followed by an unreadable second bound.\n const written = language === \"fr\" ? /^\\s+(à|a|ou)\\s+/i : /^\\s+(to|or)\\s+/i;\n const separator = written.exec(after) ?? /^\\s*(–|—|-)\\s*/.exec(after);\n if (!separator) return null;\n\n const high = parseLeadingQuantity(after.slice(separator[0].length), language);\n if (!high || high.amount <= low.amount) return null;\n\n return {\n amount: low.amount,\n max: high.amount,\n separator: separator[1]!,\n length: low.length + separator[0].length + high.length,\n };\n}\n\n/** One amount with its measure, as the line wrote it. */\nexport interface Measure {\n amount: number;\n /** Upper bound when the measure is a range, null otherwise. */\n amountMax: number | null;\n /** The word or sign a range was written with. */\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n}\n\nexport interface ParsedIngredient {\n /** The line exactly as it was given. */\n original: string;\n /** The language the line was read in. */\n language: Language;\n amount: number | null;\n /**\n * Upper bound when the line gives a range, as in \"225–500 g\". Null for a\n * single amount. `amount` holds the lower bound, so the two must be scaled\n * together: multiplying only one turns \"225–500\" into the nonsense \"450–500\".\n */\n amountMax: number | null;\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n /**\n * The same quantity restated in another system, which recipes give in\n * brackets: \"450 g (1 pound)\". Left unscaled it would contradict the amount\n * beside it, so it is parsed and scaled with the rest.\n */\n alternates: Measure[];\n /**\n * How the line introduced its equivalents: in brackets, as in \"450 g (1\n * pound)\", or after a slash, as in \"500 g / 1.1 lb\". The rewrite puts them\n * back the way the line offered them.\n */\n alternateStyle: \"bracket\" | \"slash\" | null;\n /** What the amount and the measure apply to, such as \"farine\" or \"egg yolks\". */\n item: string;\n /**\n * The article the amount was read from, as in \"une\" in \"une pincée de sel\".\n * Null when the line wrote a figure.\n */\n articleWord: string | null;\n /**\n * How many things one of the word the line counted with stands for, as in the\n * twelve of \"2 dozen mushrooms\". Null when the line counted the things\n * themselves. `amount` already holds the product, so this is what says where\n * the figure came from.\n */\n countMultiplier: number | null;\n}\n\n/** Articles a French line writes where a digit would go, and what they count as. */\nconst FRENCH_ARTICLES: Record<string, number> = { un: 1, une: 1, quelques: 3 };\n\n/**\n * Take a measure off the front of `text`, longest spelling first, so \"cuillère\n * à soupe\" is not read as \"cuillère\" with \"à soupe\" spilling into the item\n * name, and \"fluid ounce\" is not read as \"ounce\" with \"fluid\" left dangling.\n */\nexport function takeUnit(text: string, language: Language): { unit: UnitInfo | null; rest: string } {\n const normalized = normalizeUnitKey(text);\n for (const key of unitKeys(language)) {\n if (normalized !== key && !normalized.startsWith(`${key} `)) continue;\n const unit = lookupUnit(key, language);\n if (!unit) continue;\n // Consume the same number of words from the original text, which may be\n // spelled with accents the normalized key has lost.\n const wordCount = key.split(\" \").length;\n const words = text.trim().split(/\\s+/);\n return { unit, rest: words.slice(wordCount).join(\" \") };\n }\n\n if (language === \"en\") {\n const words = text.trimStart().split(/\\s+/);\n const load = words[0] ? readContainerLoad(words[0]) : null;\n if (load) return { unit: load, rest: words.slice(1).join(\" \") };\n }\n\n return { unit: null, rest: text };\n}\n\n/**\n * A measure inside brackets can be stated in the other language's vocabulary: a\n * French page glosses grams in ounces, and an English page glosses cups in\n * millilitres. The line's own language is tried first, so a word both\n * vocabularies carry keeps the spelling and the plural of the line it sits in.\n */\nfunction takeUnitEitherLanguage(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const first = takeUnit(text, language);\n if (first.unit) return first;\n return takeUnit(text, language === \"fr\" ? \"en\" : \"fr\");\n}\n\n/**\n * Decide which language a line is written in, using the small words that hold\n * it together and whether each vocabulary recognises the measure it names.\n */\nexport function detectLanguage(line: string): Language {\n const text = line.trim();\n // Whatever a leading figure, fraction or article occupies, so the probe looks\n // at the position a measure would stand in.\n const afterFigures = text.replace(/^[\\s\\d.,/½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞\\-–—]+/u, \"\").trimStart();\n const afterArticle = afterFigures.replace(/^(?:un|une|quelques|of\\s+an?|an?)\\s+/iu, \"\");\n\n const frenchUnit = takeUnit(afterArticle, \"fr\").unit !== null;\n const englishUnit = takeUnit(afterArticle, \"en\").unit !== null;\n\n return readLanguage(text, { frenchUnit, englishUnit }).language;\n}\n\n/**\n * Split an ingredient line into amount, measure, bracketed equivalents and\n * item.\n *\n * A missing amount is normal and not an error: many lines are just \"sel\". A\n * missing measure is equally normal and means the item is counted, as in\n * \"5 egg yolks\".\n */\nexport function parseIngredient(line: string, choice: LanguageChoice = \"auto\"): ParsedIngredient {\n const original = line;\n const text = line.trim();\n const language = choice === \"auto\" ? detectLanguage(text) : choice;\n\n const empty: ParsedIngredient = {\n original,\n language,\n amount: null,\n amountMax: null,\n rangeSeparator: null,\n unit: null,\n alternates: [],\n alternateStyle: null,\n item: text,\n articleWord: null,\n countMultiplier: null,\n };\n\n const range = parseLeadingRange(text, language);\n const article = range ? null : readArticle(text, language);\n const quantity = range ?? parseLeadingQuantity(text, language) ?? article;\n if (!quantity) return empty;\n\n let rest = text.slice(quantity.length).trimStart();\n // \"two thirds of a cup\" names a share of one cup, and the measure stands\n // behind the preposition and the article that introduce it.\n if (language === \"en\") rest = rest.replace(/^(?:of\\s+)?an?\\s+/i, \"\");\n\n // \"2 dozen mushrooms\" counts mushrooms, twelve to the dozen, so the multiplier\n // is folded into the amount and the line goes on to be read as the count of a\n // thing it now is.\n const multiplier = readCountMultiplier(rest);\n if (multiplier) rest = multiplier.rest;\n const times = multiplier?.times ?? 1;\n\n const leading = takeLeadingUnit(rest, language, quantity === article);\n rest = leading.rest;\n\n const bracketed = takeAlternates(rest, language);\n rest = bracketed.rest;\n\n const slashed = bracketed.measures.length > 0 ? null : takeSlashAlternates(rest, language);\n if (slashed) rest = slashed.rest;\n\n return {\n original,\n language,\n amount: quantity.amount * times,\n amountMax: range === null ? null : range.max * times,\n rangeSeparator: range?.separator ?? null,\n unit: leading.unit,\n alternates: slashed ? slashed.measures : bracketed.measures,\n alternateStyle: slashed ? \"slash\" : bracketed.measures.length > 0 ? \"bracket\" : null,\n item: stripItemLead(rest, language),\n articleWord: quantity === article ? (article?.word ?? null) : null,\n countMultiplier: multiplier?.times ?? null,\n };\n}\n\ninterface ParsedArticle extends ParsedQuantity {\n /** The article as the line wrote it. */\n word: string;\n}\n\n/**\n * Read the article a line uses in place of the figure one, as in \"une pincée de\n * sel\" or \"a pinch of salt\".\n *\n * The article counts as a quantity only when a measure follows it, because that\n * is where it stands for a number: \"une pincée\" is one pinch and \"a pinch\" is\n * one pinch, while \"un oignon\" and \"a ripe apple\" name a vegetable and a fruit\n * and no amount at all. Reading the second as the first would multiply a number\n * the line never wrote.\n */\nfunction readArticle(text: string, language: Language): ParsedArticle | null {\n /** A measure or a multiplier is what makes the article stand for a number. */\n const counts = (rest: string) =>\n takeLeadingUnit(rest, language, true).unit !== null || readCountMultiplier(rest) !== null;\n\n if (language === \"fr\") {\n const match = /^\\s*(un|une|quelques)\\b\\s*/i.exec(text);\n if (!match) return null;\n if (!counts(text.slice(match[0].length))) return null;\n const word = match[1]!;\n return { amount: FRENCH_ARTICLES[word.toLowerCase()]!, length: match[0].length, word };\n }\n\n const article = /^an?\\s+/i.exec(text);\n if (!article) return null;\n if (!counts(text.slice(article[0].length))) return null;\n return { amount: 1, length: article[0].length, word: article[0].trim() };\n}\n\n/**\n * Words that say how many things a number stands for, rather than how much of\n * something one of them holds.\n *\n * A dozen, and a douzaine, is twelve of whatever is being counted. \"2 dozen\n * mushrooms\" therefore asks for twenty-four mushrooms, and the answer divides\n * the way a mushroom does. Reading the word as a measure gives \"1 1/2 dozen\",\n * which is not a count a kitchen works with, and it hands the question of\n * divisibility to a word that names no food.\n */\nconst COUNT_MULTIPLIERS: Record<string, number> = {\n dozen: 12,\n dozens: 12,\n douzaine: 12,\n douzaines: 12,\n};\n\n/** The multiplier a line opens with, and what stands after it. */\nfunction readCountMultiplier(text: string): { times: number; rest: string } | null {\n const match = /^\\s*(\\p{L}+)\\s+/u.exec(text);\n if (!match) return null;\n\n const times = COUNT_MULTIPLIERS[normalizeUnitKey(match[1]!)];\n if (times === undefined) return null;\n return { times, rest: text.slice(match[0].length) };\n}\n\n/**\n * Take the measure a line opens with, allowing for the ones no vocabulary can\n * list.\n *\n * French names a measure by the container followed by the partitive that\n * introduces what is measured, and that reading is offered only where the line\n * wrote an article instead of a digit: \"un bouchon de rhum\" asks for an amount,\n * while \"1 piment de Cayenne\" asks for a chilli whose variety happens to be\n * introduced the same way.\n */\nfunction takeLeadingUnit(\n text: string,\n language: Language,\n fromArticle: boolean,\n): { unit: UnitInfo | null; rest: string } {\n const taken = takeUnit(text, language);\n if (taken.unit) return taken;\n\n if (language === \"fr\" && fromArticle) {\n const measure = readPartitiveMeasure(text);\n if (measure) return { unit: measure.unit, rest: measure.rest };\n }\n\n return taken;\n}\n\n/**\n * Drop the preposition and the article that stand between a measure and what it\n * measures.\n *\n * \"200 g de farine\" reads better as item \"farine\" than \"de farine\", and\n * \"2 heads of garlic\" names the same thing as \"2 heads garlic\".\n *\n * The article goes with the preposition. \"2/3 d'un flacon\" names a share of one\n * flacon, and once the share has been multiplied the count sits where \"un\"\n * stood. Leaving the article behind produces \"4 un flacon\", which reads as\n * broken text rather than as a quantity.\n */\nfunction stripItemLead(text: string, language: Language): string {\n if (language === \"fr\") {\n return text\n .replace(/^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+)/i, \"\")\n .replace(/^(?:une|un)\\s+/i, \"\")\n .trim();\n }\n return text\n .replace(/^of\\s+/i, \"\")\n .replace(/^an?\\s+/i, \"\")\n .trim();\n}\n\n/**\n * Read a bracketed group of equivalent measures, as in \"(1 pound)\" or\n * \"(500 g / 1.1 lb)\".\n *\n * The group is only taken when every part of it reads as an amount with a\n * measure. A bracket holding a remark, as in \"(the riper the better)\" or\n * \"(cup)\", stays in the item text where it belongs, because scaling it would\n * mean scaling prose.\n */\nfunction takeAlternates(text: string, language: Language): { measures: Measure[]; rest: string } {\n if (!text.startsWith(\"(\")) return { measures: [], rest: text };\n const close = text.indexOf(\")\");\n if (close < 0) return { measures: [], rest: text };\n\n const inside = text.slice(1, close);\n const parts = inside.split(\"/\").map((part) => part.trim());\n const measures: Measure[] = [];\n\n for (const part of parts) {\n const range = parseLeadingRange(part, language);\n const quantity = range ?? parseLeadingQuantity(part, language);\n if (!quantity) return { measures: [], rest: text };\n\n const after = takeUnitEitherLanguage(part.slice(quantity.length).trimStart(), language);\n // A trailing word means the bracket is not purely a measure, as in\n // \"(1-inch pieces)\", so the whole group is left as prose.\n if (!after.unit || after.rest.trim() !== \"\") return { measures: [], rest: text };\n\n measures.push({\n amount: quantity.amount,\n amountMax: range?.max ?? null,\n rangeSeparator: range?.separator ?? null,\n unit: after.unit,\n });\n }\n\n if (measures.length === 0) return { measures: [], rest: text };\n return { measures, rest: text.slice(close + 1).trimStart() };\n}\n\n/**\n * Read equivalents a line states after a slash, as in \"500 g / 1.1 lb rolled\n * oats\", where the item follows the last of them.\n *\n * Both figures name one quantity, so both have to move together: a doubled\n * line reading \"1 kg / 1.1 lb\" gives two answers a factor of two apart for the\n * same ingredient. A slash followed by anything other than an amount and a\n * measure is prose and stays in the item text.\n */\nfunction takeSlashAlternates(\n text: string,\n language: Language,\n): { measures: Measure[]; rest: string } | null {\n const measures: Measure[] = [];\n let rest = text;\n\n while (rest.startsWith(\"/\")) {\n const after = rest.slice(1).trimStart();\n const range = parseLeadingRange(after, language);\n const quantity = range ?? parseLeadingQuantity(after, language);\n if (!quantity) break;\n\n const taken = takeUnitEitherLanguage(after.slice(quantity.length).trimStart(), language);\n if (!taken.unit) break;\n\n measures.push({\n amount: quantity.amount,\n amountMax: range?.max ?? null,\n rangeSeparator: range?.separator ?? null,\n unit: taken.unit,\n });\n rest = taken.rest.trimStart();\n }\n\n return measures.length > 0 ? { measures, rest } : null;\n}\n\nexport interface FormatAmountOptions {\n /**\n * Whether to snap near-fractions to 1/4, 1/3, 1/2, 2/3 and 3/4.\n *\n * True for things a cook counts or spoons out: \"1/3 cup\" is how a kitchen\n * expresses it, \"0.33 cup\" is not. False for mass and volume, which are\n * decimal by nature: nobody weighs \"8 1/3 kg\" of sugar, they weigh 8.33 kg.\n */\n fractions?: boolean;\n}\n\n/**\n * Render an amount the way a recipe would write it, with the decimal mark the\n * language uses.\n */\nexport function formatAmount(\n amount: number,\n language: Language,\n options: FormatAmountOptions = {},\n): string {\n const decimal = (value: number) => {\n // Two decimals is finer than any kitchen resolves, and for anything smaller\n // than that it is zero. A quantity that survived being divided a\n // thousandfold must not be handed back as none of the ingredient, so below\n // that point the significant digits are what gets written.\n const rounded =\n value !== 0 && Math.abs(value) < 0.01\n ? Number(value.toPrecision(2))\n : Math.round(value * 100) / 100;\n const rendered = String(rounded);\n return language === \"fr\" ? rendered.replace(\".\", \",\") : rendered;\n };\n\n if (!Number.isFinite(amount)) return \"\";\n if (Number.isInteger(amount)) return String(amount);\n if (options.fractions === false) return decimal(amount);\n\n const whole = Math.floor(amount);\n const rest = amount - whole;\n const known: Array<[number, string]> = [\n [0.25, \"1/4\"],\n [1 / 3, \"1/3\"],\n [0.5, \"1/2\"],\n [2 / 3, \"2/3\"],\n [0.75, \"3/4\"],\n ];\n for (const [value, label] of known) {\n if (Math.abs(rest - value) < 0.02) return whole > 0 ? `${whole} ${label}` : label;\n }\n\n return decimal(amount);\n}\n","/**\n * Scaling ingredient quantities, in French and in English.\n *\n * The guiding rule is that a scaled quantity must be something a cook can act\n * on. Multiplying every number by the factor is arithmetically correct and\n * practically useless: it produces \"2,4 oeufs\" and \"0.67 pinch of salt\" with\n * the same confidence as \"267 g flour\". Each line is therefore classified by\n * what its measure allows, and the classification travels with the result so\n * the caller can see what was computed and what was left alone.\n *\n * Leaving a line alone is a decision of the same weight. A quantity a recipe\n * states loosely still holds a share of the dish, and a leavening agent left at\n * one pinch for twenty-five servings is a recipe that does not rise.\n *\n * The two languages are read by one set of rules. A list holding French lines\n * and English lines comes back scaled by the same arithmetic, with each line\n * written the way its own language writes numbers, plurals and measures.\n */\n\nimport type { Language, LanguageChoice } from \"./language.js\";\nimport { formatAmount, parseIngredient } from \"./quantity.js\";\nimport type { Measure, ParsedIngredient } from \"./quantity.js\";\nimport type { Divisibility, UnitInfo } from \"./units.js\";\nimport {\n QUARTERED_MEASURE,\n approximateEquivalent,\n chooseReadableUnit,\n countsBarePieces,\n demoteUnit,\n formatUnit,\n hasEmbeddedMeasure,\n isSpoonMeasure,\n unitDivisibility,\n} from \"./units.js\";\n\nexport type { Language, LanguageChoice } from \"./language.js\";\n\nexport type ScalingKind =\n /** The arithmetic was exact. */\n | \"scaled\"\n /**\n * A countable item was moved to a whole or half unit, or a measurement was\n * demoted to a smaller unit to stay usable.\n */\n | \"rounded\"\n /** The line carries nothing that can be multiplied. */\n | \"unscaled\";\n\nexport interface ScaledIngredient {\n /** The line after scaling, identical to `original` when unscaled. */\n text: string;\n /** The line as published. */\n original: string;\n scaling: ScalingKind;\n /**\n * The scaled quantity, expressed in `unit`, and the lower bound when the line\n * gives a range.\n *\n * Read it together with `unit`, never on its own: a large result is moved to\n * a bigger unit, so scaling \"200 g\" by ten gives an amount of 2 with a unit\n * of \"kg\". The bare number can therefore shrink while the quantity grows.\n */\n amount: number | null;\n /** Upper bound when the line gives a range, null otherwise. */\n amountMax: number | null;\n /** The unit `amount` is in, which may differ from the one the recipe used. */\n unit: string | null;\n /** The language the line was read and rewritten in. */\n language: Language;\n /** Why the line was rounded, clamped or left alone. */\n note?: string;\n}\n\n/** Round to a step, keeping two decimals at most. */\nfunction roundTo(value: number, step: number): number {\n return Math.round(Math.round(value / step) * step * 100) / 100;\n}\n\n/**\n * Round a measured amount to something a kitchen scale can show.\n *\n * Large amounts do not need fine precision and small ones do, so the step grows\n * with the value rather than being fixed. The step stays a tenth in the single\n * digits because a unit can be a pound as easily as a gram, and rounding 2.2 lb\n * to 2 would throw away a tenth of the ingredient.\n */\nfunction roundMeasured(value: number): number {\n if (value >= 100) return roundTo(value, 5);\n if (value >= 10) return roundTo(value, 1);\n if (value >= 1) return roundTo(value, 0.1);\n return Math.round(value * 100) / 100;\n}\n\n/** Below this there is nothing a kitchen can measure out of a spoonful. */\nconst SMALLEST_USABLE_FRACTION = 0.25;\n\n/** The smallest share of one thing that is still worth putting in a bowl. */\nconst SMALLEST_USABLE: Record<Divisibility, number> = {\n whole: 1,\n half: 0.5,\n quarter: 0.25,\n};\n\n/** True when a number is a whole or a half, to the last bit of precision. */\nfunction isHalfStep(value: number): boolean {\n return Math.abs(value * 2 - Math.round(value * 2)) < 1e-9;\n}\n\n/** Two decimals, which is finer than any kitchen resolves. */\nfunction trim(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\ninterface CountableResult {\n value: number;\n /** The floor was hit, so this line no longer holds its share of the recipe. */\n clamped: boolean;\n}\n\n/**\n * Round a counted thing to an amount a kitchen produces.\n *\n * A count lands on a whole. The one exception is a share that comes out on a\n * half by itself, for a thing a knife can halve: half a clove of garlic is a\n * real amount, and rounding it up to a whole adds a fifth of the garlic to a\n * recipe that asked for five cloves.\n *\n * How finely the thing divides decides the floor. Under that floor the amount\n * is clamped up rather than shrunk towards nothing, which keeps the ingredient\n * in the recipe at the cost of its proportion, and the caller is told through\n * `clamped`. The ceiling stops a shrinking recipe from ever asking for more\n * than it started with.\n */\nfunction roundCountable(\n value: number,\n divisibility: Divisibility,\n ceiling: number,\n): CountableResult {\n if (value <= 0) return { value: 0, clamped: false };\n\n const floor = SMALLEST_USABLE[divisibility];\n\n if (divisibility !== \"whole\" && value >= floor && isHalfStep(value)) {\n return { value: trim(value), clamped: false };\n }\n\n if (divisibility === \"whole\") {\n // Below the halfway mark the nearest whole is none, and dropping the\n // ingredient is worse than overstating it, so the line keeps one and says\n // it no longer holds its share.\n if (value < 0.5) return { value: floor, clamped: true };\n return { value: Math.round(value), clamped: false };\n }\n\n if (value < floor) return { value: floor, clamped: true };\n\n if (value < 1) {\n // A knife takes a vegetable to quarters and thirds; anything else offers\n // the half it can be split on.\n const steps = divisibility === \"quarter\" ? [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 1] : [0.5, 1];\n const candidates = steps.filter(\n (candidate) => candidate >= floor && candidate <= Math.max(ceiling, floor),\n );\n let closest = candidates[0]!;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) closest = candidate;\n }\n return { value: trim(closest), clamped: false };\n }\n\n return { value: Math.round(value), clamped: false };\n}\n\n/**\n * Round a spoon or a cup, which a kitchen measures out in halves and in the\n * fractions printed on a measuring set.\n */\nfunction roundSpoon(value: number, ceiling: number): CountableResult {\n if (value <= 0) return { value: 0, clamped: false };\n\n if (value < 1) {\n const candidates = [SMALLEST_USABLE_FRACTION, 1 / 3, 0.5, 2 / 3, 0.75, 1].filter(\n (candidate) => candidate <= Math.max(ceiling, SMALLEST_USABLE_FRACTION),\n );\n let closest = candidates[0]!;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) closest = candidate;\n }\n return { value: trim(closest), clamped: value < SMALLEST_USABLE_FRACTION };\n }\n\n return { value: roundTo(value, 0.5), clamped: false };\n}\n\n/**\n * Walk a spoon or a cup down to the smaller spoon while the amount sits under\n * one, so a share is stated in a measure that exists.\n *\n * An amount already on a whole or a half stays where the line put it: half a\n * tablespoon is a spoon a kitchen owns, and there is nothing to gain by calling\n * it a teaspoon and a half.\n */\nfunction stepDownSpoon(unit: UnitInfo, reference: number): { unit: UnitInfo; ratio: number } {\n let current = unit;\n let ratio = 1;\n\n while (reference * ratio < 1 && !isHalfStep(reference * ratio)) {\n const step = demoteUnit(current);\n if (!step) break;\n ratio *= step.per;\n current = step.unit;\n }\n\n return { unit: current, ratio };\n}\n\n/**\n * How close a result has to be to the exact product to be called exact.\n *\n * Two tests, because one of them alone is wrong at some scale. An absolute gap\n * of a hundredth is beneath what a kitchen resolves at ordinary sizes, and at a\n * hundredth of a millilitre it is the whole quantity: 0.006 rounded to 0.01 sits\n * inside the absolute gap while being two thirds larger than what was asked for.\n * A share of half a percent catches that without calling ordinary rounding\n * inexact.\n */\nconst EXACT_WITHIN = 0.01;\nconst EXACT_SHARE = 0.005;\n\nfunction landedExactly(exact: number, amount: number): boolean {\n const gap = Math.abs(exact - amount);\n if (gap > EXACT_WITHIN) return false;\n return exact === 0 || gap / Math.abs(exact) <= EXACT_SHARE;\n}\n\nexport interface ScaleOptions {\n /** Multiplier applied to the quantities. */\n factor: number;\n /**\n * Which language to read the lines in. Left out, each line is read on its\n * own, so a list holding both comes back written the way each line was.\n */\n language?: LanguageChoice;\n}\n\ninterface ScaledBound {\n amount: number;\n /** The exact product, expressed in the unit that came back. */\n exact: number;\n clamped: boolean;\n /** The exact product in the unit the recipe wrote, for a readable note. */\n raw: number;\n}\n\ninterface ScaledMeasure {\n bounds: ScaledBound[];\n /** The unit every bound is expressed in, which both ends of a range share. */\n unit: UnitInfo | null;\n}\n\n/**\n * Scale one measure, both ends of a range together.\n *\n * The unit is chosen once, from the smaller end of a range, and applied to\n * every bound.\n *\n * A measurement walks down to a smaller unit before it is rounded, so a\n * quantity divided a thousandfold never rounds to zero and states that the\n * recipe needs none of it.\n */\nfunction scaleMeasure(\n low: number,\n high: number | null,\n unit: UnitInfo | null,\n factor: number,\n divisibility: Divisibility,\n): ScaledMeasure {\n const raws = high === null ? [low * factor] : [low * factor, high * factor];\n const sources = high === null ? [low] : [low, high];\n /**\n * The unit is chosen from the smaller end of a range.\n *\n * Both ends have to share one unit, or \"½ to 1 pound\" comes back as \"13 oz to\n * 1.5 pounds\", where the second number reads smaller than the first. Of the\n * two, the smaller end is the one a unit can ruin: choosing from the larger\n * turns \"450 to 1000 g\" into \"0.45 to 1 kg\", and pushed one step further it\n * rounds the small end away entirely. A large number in a small unit is\n * merely long to read.\n */\n const positive = raws.filter((raw) => raw > 0);\n const reference = positive.length > 0 ? Math.min(...positive) : raws[0]!;\n\n /** Both bounds share one unit, and each keeps the precision that unit affords. */\n const inUnit = (target: UnitInfo, ratio: number): ScaledMeasure => ({\n bounds: raws.map((raw, index) => {\n const exact = raw * ratio;\n const rounded = roundMeasured(exact);\n // At the bottom of a ladder, keep what precision is left rather than\n // deleting the ingredient.\n const usable = rounded === 0 && exact > 0 ? Number(exact.toPrecision(2)) : rounded;\n // Rounding to a step of five grams above a hundred can round upwards, and\n // a recipe being made smaller must never come out asking for more than\n // the page published.\n const ceiling = factor < 1 ? sources[index]! * ratio : Number.POSITIVE_INFINITY;\n return {\n amount: Math.min(usable, ceiling),\n exact,\n clamped: false,\n raw,\n };\n }),\n unit: target,\n });\n\n if (unit && unit.kind === \"measured\") {\n const chosen = chooseReadableUnit(unit, reference);\n return inUnit(chosen.unit, chosen.ratio);\n }\n\n if (unit && isSpoonMeasure(unit)) {\n const stepped = stepDownSpoon(unit, reference);\n // A share stated in the smaller spoon is a measurement, and keeps the\n // precision of one rather than being snapped to the fractions of a spoon\n // it no longer fills.\n if (stepped.ratio !== 1) return inUnit(stepped.unit, stepped.ratio);\n\n const bounds = raws.map((raw, index) => {\n const ceiling = factor < 1 ? sources[index]! : Number.POSITIVE_INFINITY;\n const rounded = roundSpoon(raw, ceiling);\n return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw };\n });\n return { bounds, unit };\n }\n\n const bounds = raws.map((raw, index) => {\n // Scaling down must never end up asking for more than the recipe did.\n const ceiling = factor < 1 ? sources[index]! : Number.POSITIVE_INFINITY;\n const rounded = roundCountable(raw, divisibility, ceiling);\n return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw };\n });\n return { bounds, unit };\n}\n\n/**\n * How finely a counted thing divides, decided by the size of one of them\n * against what a recipe puts in.\n *\n * `PORTION_SIZED_ITEM` and `QUARTERED_ITEM` are the two ends of that one\n * comparison, and each entry earns its place by where the food falls on it.\n *\n * A shrimp, a mussel, a hazelnut, a peppercorn, a juniper berry, a star anise\n * is already a portion on its own. A recipe counts five, twelve, twenty of\n * them, and a cook taking a share of that recipe puts one fewer in the pan;\n * cutting one in two is not a thing a kitchen does. These land on a whole\n * number.\n *\n * A leg of lamb, a baguette, a camembert, a pineapple, an onion, a watermelon,\n * a guinea fowl sits at the other end: a recipe asks for one or for two, and\n * the share it wants out of one is decided by a knife. A quarter of one is a\n * piece someone serves, and what is left keeps.\n *\n * Both lists carry each food in either language, because where a food falls on\n * that comparison has nothing to do with the words a page uses for it.\n */\nconst PORTION_SIZED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"shrimps?|prawns?|langoustines?|mussels?|hazelnuts?|peppercorns?|junipers?|grains?|anise\" +\n // The same foods, as a French line names them.\n \"|crevettes?|gambas|moules?|noisettes?|genièvres?|genievres?|anis\" +\n \")\\\\b\",\n \"iu\",\n);\n\nconst QUARTERED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"onions?|shallots?|potatoes|potato|carrots?|apples?|pears?|lemons?|limes?|oranges?\" +\n \"|tomato(?:es)?|cucumbers?|courgettes?|zucchinis?|aubergines?|eggplants?|squash(?:es)?\" +\n \"|pumpkins?|cabbages?|melons?|watermelons?|peppers?|beets?|turnips?|parsnips?\" +\n \"|leeks?|bananas?|mango(?:e?s)?|avocados?\" +\n \"|legs? of lamb|lamb legs?|baguettes?|camemberts?|cheeses?|chorizos?|pineapples?\" +\n \"|peach(?:es)?|apricots?|milk|chickens?|guinea fowls?|roasts?\" +\n // The same produce, as a French line names it.\n \"|oignons?|échalotes?|echalotes?|pommes? de terre|carottes?|pommes?|poires?|citrons?\" +\n \"|tomates?|concombres?|courgettes?|aubergines?|potirons?|choux?|chou|melons?|poivrons?\" +\n \"|betteraves?|navets?|panais|poireaux?|bananes?|mangues?|avocats?|pastèques?|pasteques?\" +\n \"|gigots?|fromages?|chèvres?|chevres?|ananas|pêches?|peches?|abricots?|laits?\" +\n \"|poulets?|pintades?|reblochons?|bûches?|buches?|rôtis?|rotis?\" +\n \")\\\\b\",\n \"iu\",\n);\n\n/**\n * A juice, the one counted thing whose division stops at the half.\n *\n * Half the juice of a lemon is taken by squeezing half the fruit, which is a\n * step a recipe writes. A quarter of one has to be poured out and measured\n * back, and no recipe asks for that.\n *\n * It reads before the fruit, which a knife divides further on its own.\n */\nconst HALVED_ITEM = /\\b(?:jus|juices?)\\b/iu;\n\n/**\n * Things a kitchen takes one of or none of.\n *\n * An egg comes out of its shell whole, and so does the yolk or the white a\n * recipe asks for on its own: half of one means beating it and weighing the\n * result, which no recipe asks for, and there is no way to keep the other half.\n * A count of them therefore lands on a whole number, whichever side of the half\n * the arithmetic fell on.\n *\n * This list is short on purpose, and a word joins it only when half of the\n * thing is genuinely not something a cook can measure out. Anything that is\n * poured, weighed or cut halves, and takes the half.\n *\n * Two more belong here for reasons the criterion cannot reach on its own:\n *\n * - a clou de girofle is a dried flower bud, dropped into the pot and fished\n * back out of it. Nothing about it is measured, so there is no half of one to\n * take;\n * - a zeste is what comes off one fruit in one go. A line asking for the zeste\n * of a citron is asking for all of it, and a share of a zeste names no amount\n * a cook stops at.\n */\nconst WHOLE_ITEM =\n /\\b(?:eggs?|yolks?|egg\\s+whites?|oeufs?|œufs?|jaunes?\\s+d['e]|clous?|zestes?|zests?)\\b/iu;\n\n/**\n * A piece carved off a bird or off a joint, which stops at the half.\n *\n * The whole animal divides by the knife that portions it, and one of these is\n * already the portion that knife produced: a blanc or a cuisse feeds one, and\n * half of one is the share a smaller recipe serves. Taking a quarter would name\n * a piece no one plates.\n *\n * It reads before the animal, and before the fruit or the vegetable a line\n * often names beside the meat, so that neither answers for the cut.\n */\nconst HALVED_CUT =\n /\\b(?:breasts?|thighs?|drumsticks?|wings?|cutlets?|cuisses?|ailes?|pilons?|escalopes?|magrets?)\\b/iu;\n\n/**\n * How far a \"clove\" divides, when a line counts one.\n *\n * The word names two foods that answer the question in opposite ways. A clove\n * of garlic is a wedge broken off a bulb, the gousse d'ail, and a quarter of\n * one is what a knife scrapes into a pan. A clove on its own is the dried\n * flower bud, the clou de girofle, dropped into the pot and fished back out of\n * it: nothing about it is measured, so there is no share of one to take.\n *\n * Garlic named in the line is what separates the two, and that is how the great\n * majority of lines writing the word say which food they mean. The French words\n * need none of this, each naming one food and one only.\n *\n * The question is asked only where the clove is the thing being counted. A head\n * of garlic that mentions its cloves is a head, and divides as one.\n *\n * Null when the line counts no clove at all.\n */\nfunction cloveDivisibility(unit: UnitInfo | null, item: string): Divisibility | null {\n const counted = unit ? unit.canonical === \"clove\" : /\\bcloves?\\b/i.test(item);\n if (!counted) return null;\n return /\\bgarlic\\b/i.test(item) ? \"quarter\" : \"whole\";\n}\n\n/**\n * How far a \"blanc\" divides, when a line names one.\n *\n * The word covers two foods that answer the question in opposite ways. The\n * white of an oeuf goes with the oeuf and the jaune: half of one would have to\n * be beaten and weighed. A blanc de poulet or de dinde is a piece of meat, and\n * half of one is a portion a knife cuts and a fridge keeps.\n *\n * Deciding the word here rather than letting the line fall through is what\n * keeps the fruit or the vegetable such a line often names beside the meat from\n * answering for it.\n *\n * Null when the line names no blanc at all.\n */\nconst BLANC_OF = /\\bblancs?\\s+d(?:e\\s|['’])/iu;\nconst BLANC_OF_EGG = /\\bblancs?\\s+d(?:e\\s|['’])\\s*(?:oeufs?|œufs?)\\b/iu;\n\nfunction blancDivisibility(item: string): Divisibility | null {\n // The noun is the one followed by what it is the blanc of. \"vin blanc\" and\n // \"oignon blanc\" use the same letters as a colour and count as neither.\n if (!BLANC_OF.test(item)) return null;\n return BLANC_OF_EGG.test(item) ? \"whole\" : \"half\";\n}\n\nfunction divisibilityOf(unit: UnitInfo | null, item: string): Divisibility {\n const clove = cloveDivisibility(unit, item);\n if (clove) return clove;\n if (unit && !countsBarePieces(unit)) return unitDivisibility(unit);\n const blanc = blancDivisibility(item);\n if (blanc) return blanc;\n if (WHOLE_ITEM.test(item)) return \"whole\";\n if (PORTION_SIZED_ITEM.test(item)) return \"whole\";\n if (HALVED_ITEM.test(item)) return \"half\";\n if (HALVED_CUT.test(item)) return \"half\";\n if (QUARTERED_MEASURE.test(item)) return \"quarter\";\n return QUARTERED_ITEM.test(item) ? \"quarter\" : \"half\";\n}\n\n/* -------------------------------------------------------------------------- */\n/* Agreement between a number and the thing it counts */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Names of food that read the same whatever the number.\n *\n * Some are mass nouns a recipe counts in spoons rather than in units, and some\n * are plurals already. An -s added to one of them names a thing no shop sells.\n */\nconst INVARIABLE_ITEM = new Set([\n \"anise\",\n \"ananas\",\n \"asparagus\",\n \"bacon\",\n \"basil\",\n \"beef\",\n \"bison\",\n \"broccoli\",\n \"butter\",\n \"celery\",\n \"cilantro\",\n \"citrus\",\n \"cinnamon\",\n \"cocoa\",\n \"cod\",\n \"coriander\",\n \"corn\",\n \"cornstarch\",\n \"couscous\",\n \"cream\",\n \"deer\",\n \"fish\",\n \"flour\",\n \"gambas\",\n \"garlic\",\n \"ginger\",\n \"ham\",\n \"honey\",\n \"hummus\",\n \"jus\",\n \"kale\",\n \"lamb\",\n \"macaroni\",\n \"milk\",\n \"miso\",\n \"musk\",\n \"moose\",\n \"mutton\",\n \"nutmeg\",\n \"oil\",\n \"okra\",\n \"oregano\",\n \"parsley\",\n \"pasta\",\n \"pork\",\n \"quinoa\",\n \"rice\",\n \"rosemary\",\n \"saffron\",\n \"salmon\",\n \"salt\",\n \"shrimp\",\n \"spinach\",\n \"squid\",\n \"sugar\",\n \"thyme\",\n \"tofu\",\n \"trout\",\n \"tuna\",\n \"vanilla\",\n \"veal\",\n \"venison\",\n \"vinegar\",\n \"water\",\n \"yeast\",\n \"yogurt\",\n]);\n\n/** Names whose plural the ordinary English rules get wrong. */\nconst IRREGULAR_PLURAL: Record<string, string> = {\n calf: \"calves\",\n chili: \"chilies\",\n chilli: \"chillies\",\n goose: \"geese\",\n half: \"halves\",\n knife: \"knives\",\n leaf: \"leaves\",\n loaf: \"loaves\",\n mango: \"mangoes\",\n potato: \"potatoes\",\n shelf: \"shelves\",\n tomato: \"tomatoes\",\n};\n\nconst IRREGULAR_SINGULAR: Record<string, string> = Object.fromEntries(\n Object.entries(IRREGULAR_PLURAL).map(([one, many]) => [many, one]),\n);\n\n/** Keep the capitalisation the line used while looking the word up in lower case. */\nfunction matchCase(source: string, replacement: string): string {\n if (source[0] === source[0]?.toUpperCase() && source.slice(1) === source.slice(1).toLowerCase()) {\n return replacement[0]!.toUpperCase() + replacement.slice(1);\n }\n return replacement;\n}\n\nfunction toEnglishPlural(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) return word;\n const irregular = IRREGULAR_PLURAL[key];\n if (irregular) return matchCase(word, irregular);\n if (/(?:ch|sh|s|x|z)$/i.test(word)) return `${word}es`;\n if (/[^aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;\n if (/(?:[^f]f|fe)$/i.test(word)) return `${word.replace(/fe?$/i, \"\")}ves`;\n return `${word}s`;\n}\n\nfunction toEnglishSingular(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) return word;\n const irregular = IRREGULAR_SINGULAR[key];\n if (irregular) return matchCase(word, irregular);\n if (/ies$/i.test(word) && word.length > 4) return `${word.slice(0, -3)}y`;\n // A -ves plural belongs to a noun ending in -f or -fe, and those are named\n // one by one in `IRREGULAR_PLURAL`: turning every -ves back into -f makes a\n // \"clof\" out of \"cloves\" and an \"olif\" out of \"olives\".\n if (/(?:ch|sh|s|x|z)es$/i.test(word)) return word.slice(0, -2);\n // \"glass\", \"molasses\": the -s belongs to the singular. Beyond the doubled -s\n // the ending settles nothing, \"couscous\" and \"kiwis\" both closing on -us, so\n // the names that carry their -s are named in `INVARIABLE_ITEM`.\n if (/ss$/i.test(word)) return word;\n if (/s$/i.test(word)) return word.slice(0, -1);\n return word;\n}\n\n/**\n * Make an English item agree with its number, in both directions.\n *\n * English marks the plural above one, so \"5 egg yolks\" divided reads \"1 egg\n * yolk\" and \"1 loaf\" tripled reads \"3 loaves\". Only the head noun is touched,\n * which is the last word before any comma: everything after a comma is\n * preparation, and \"cloves garlic, minced\" must not become \"minceds\".\n *\n * \"of\" moves the head to the left of it. What is being counted in \"bottles of\n * orange blossom water\" is the bottles, and the number says nothing about the\n * water.\n */\nfunction agreeInEnglish(item: string, amount: number): string {\n if (!item) return item;\n\n const comma = item.indexOf(\",\");\n const head = comma < 0 ? item : item.slice(0, comma);\n const tail = comma < 0 ? \"\" : item.slice(comma);\n\n const preposition = / of /i.exec(head);\n if (preposition) {\n const counted = agreeInEnglish(head.slice(0, preposition.index), amount);\n return `${counted}${head.slice(preposition.index)}${tail}`;\n }\n\n const words = head.trimEnd().split(\" \");\n const last = words[words.length - 1] ?? \"\";\n if (!/^[A-Za-z]+$/.test(last) || last.length <= 2) return item;\n\n const wantsPlural = amount > 1;\n const plural = toEnglishPlural(last);\n const singular = toEnglishSingular(last);\n const isPlural = last.toLowerCase() !== singular.toLowerCase();\n\n if (wantsPlural && !isPlural) words[words.length - 1] = plural;\n else if (!wantsPlural && isPlural) words[words.length - 1] = singular;\n else return item;\n\n return `${words.join(\" \")}${tail}`;\n}\n\n/**\n * French nouns carrying a final -s, -x or -z in the singular.\n *\n * The word is the same whatever the number, so the ending a plural would give\n * back belongs to the singular and must stay.\n */\nconst INVARIABLE_FRENCH_NOUN = new Set([\n \"ananas\",\n \"anis\",\n \"brebis\",\n \"cassis\",\n \"colis\",\n \"coulis\",\n \"couscous\",\n \"gambas\",\n \"houmous\",\n \"jus\",\n \"mais\",\n \"pastis\",\n \"pois\",\n \"radis\",\n \"ris\",\n \"souris\",\n \"tamis\",\n \"tapas\",\n]);\n\n/**\n * Adjectives a French recipe puts after the noun, and which take a plain -s.\n *\n * A French adjective agrees with the noun it qualifies, so \"1 piment entier\"\n * counted four times reads \"4 piments entiers\". Only this list is declined: an\n * unknown trailing word can be a brand (\"Golden\"), a proper noun (\"Cayenne\") or\n * a phrase whose head sits elsewhere, and a word left as the recipe wrote it\n * reads as faithful where an invented ending reads as wrong.\n */\nconst AGREEABLE_ADJECTIVES = new Set([\n \"entier\",\n \"entiere\",\n \"etoile\",\n \"etoilee\",\n \"moyen\",\n \"moyenne\",\n \"petit\",\n \"petite\",\n \"grand\",\n \"grande\",\n \"mur\",\n \"mure\",\n \"vert\",\n \"verte\",\n \"rouge\",\n \"jaune\",\n \"noir\",\n \"noire\",\n \"blanc\",\n \"blanche\",\n \"rond\",\n \"ronde\",\n \"hache\",\n \"hachee\",\n \"coupe\",\n \"coupee\",\n \"rape\",\n \"rapee\",\n \"pele\",\n \"pelee\",\n \"epluche\",\n \"epluchee\",\n \"denoyaute\",\n \"denoyautee\",\n \"emince\",\n \"emincee\",\n]);\n\n/** Lowercase and strip accents, so \"entière\" and \"entiere\" hit the same entry. */\nfunction foldWord(word: string): string {\n return word.toLowerCase().normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\");\n}\n\n/** The trailing adjective agreed with the count, or null when it is left alone. */\nfunction agreeTrailingAdjective(word: string, wantsPlural: boolean): string | null {\n const folded = foldWord(word);\n const isPlural = folded.endsWith(\"s\");\n const singular = isPlural ? word.slice(0, -1) : word;\n\n if (!AGREEABLE_ADJECTIVES.has(foldWord(singular))) return null;\n if (wantsPlural === isPlural) return null;\n\n return wantsPlural ? `${word}s` : singular;\n}\n\n/**\n * Make a French item agree with its number, in both directions.\n *\n * French takes the plural from two onwards, so \"2 oeufs\" halved reads \"1 oeuf\"\n * and \"1 brioche\" tripled reads \"3 brioches\". Only the head word is touched,\n * and only its trailing \"s\": nouns already ending in -s, -x or -z are\n * invariable in the plural (\"ananas\", \"choux\"), and forcing one would be worse\n * than leaving the word as the recipe wrote it.\n *\n * Going back down needs `INVARIABLE_FRENCH_NOUN`, because the ending settles\n * nothing on its own: \"jus\" and \"clous\" both end in -us, and the first is a\n * singular where the second is a plural of \"clou\".\n */\nfunction agreeInFrench(item: string, amount: number): string {\n if (!item) return item;\n\n const words = item.split(\" \");\n const head = words[0] ?? \"\";\n if (head.length <= 3) return item;\n\n const wantsPlural = amount >= 2;\n const isPlural = /s$|eaux$|aux$/i.test(head);\n\n if (wantsPlural && !isPlural) {\n // Words ending in -s, -x or -z do not take a plural mark.\n if (/[sxz]$/i.test(head)) {\n // The head stays as written.\n }\n // \"morceau\" and \"bocal\" take -x and -aux where the ordinary noun takes -s.\n else if (/eau$/i.test(head)) words[0] = `${head}x`;\n else if (/al$/i.test(head)) words[0] = `${head.slice(0, -2)}aux`;\n else words[0] = `${head}s`;\n } else if (!wantsPlural && isPlural) {\n if (/eaux$/i.test(head)) words[0] = head.slice(0, -1);\n else if (/aux$/i.test(head)) words[0] = `${head.slice(0, -3)}al`;\n // \"ananas\", \"anis\", \"couscous\": the -s belongs to the singular.\n else if (INVARIABLE_FRENCH_NOUN.has(foldWord(head))) {\n // The head stays as written.\n } else words[0] = head.slice(0, -1);\n }\n\n const last = words.length - 1;\n if (last > 0) {\n const adjective = agreeTrailingAdjective(words[last]!, wantsPlural);\n if (adjective) words[last] = adjective;\n }\n\n return words.join(\" \");\n}\n\nfunction agreeWithAmount(item: string, amount: number, language: Language): string {\n return language === \"fr\" ? agreeInFrench(item, amount) : agreeInEnglish(item, amount);\n}\n\n/**\n * \"de\" becomes \"d'\" before a vowel sound.\n *\n * The h is the hard case: it is silent in \"huile\" and sounded in \"haricot\", and\n * only a word list separates them. Elision is therefore limited to vowels plus\n * the handful of h-words a recipe actually uses, because \"de haricots\" merely\n * reads as careless while \"d'haricots\" reads as wrong.\n */\nconst MUTE_H_WORDS = /^(?:huile|huiles|huitre|huitres|huître|huîtres|herbe|herbes|hysope)\\b/i;\n\n/**\n * Put the item back after the measure, the way the language joins them.\n *\n * French needs the partitive: \"6 cuillères à soupe **de** beurre\". English puts\n * the two side by side: \"6 tablespoons butter\".\n */\nfunction joinItem(item: string, language: Language): string {\n if (!item) return \"\";\n if (language === \"en\") return ` ${item}`;\n const elides = /^[aeiouàâäéèêëîïôöûü]/i.test(item) || MUTE_H_WORDS.test(item);\n return elides ? ` d'${item}` : ` de ${item}`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Scaling one line */\n/* -------------------------------------------------------------------------- */\n\n/** How a line writes the choice between two quantities, in either language. */\nconst BRANCH_SEPARATORS: Record<Language, RegExp> = {\n en: /\\s+or\\s+/gi,\n fr: /\\s+ou\\s+/gi,\n};\n\ninterface Branch {\n head: string;\n /** The separator as published, so the rewrite reads the way the line did. */\n separator: string;\n tail: string;\n}\n\n/**\n * Split a line that offers one ingredient twice, at the word that offers the\n * choice.\n *\n * The search starts where the item name starts, so the \"or\" of a published\n * range such as \"2 or 3 cloves garlic\" is left to the range parser. A branch\n * counts only when it carries a quantity of its own; \"butter or margarine\"\n * names one amount and stays one line.\n */\nfunction splitBranch(text: string, parsed: ParsedIngredient): Branch | null {\n const itemStart = parsed.item ? text.indexOf(parsed.item) : text.length;\n if (itemStart < 0) return null;\n\n const separator = BRANCH_SEPARATORS[parsed.language];\n separator.lastIndex = 0;\n for (let match = separator.exec(text); match; match = separator.exec(text)) {\n if (match.index < itemStart) continue;\n const tail = text.slice(match.index + match[0].length);\n if (parseIngredient(tail, parsed.language).amount === null) continue;\n return { head: text.slice(0, match.index), separator: match[0], tail };\n }\n return null;\n}\n\n/**\n * Scale one ingredient line.\n *\n * Countable items are rounded to something a kitchen can measure, ranges are\n * scaled at both ends, equivalents and alternatives are scaled with the amount\n * they stand beside, and an approximate measure such as a pinch has its count\n * scaled with a note saying how loosely one of them is defined.\n */\nexport function scaleIngredient(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n // A factor of one changes nothing, and rewriting the line anyway would round\n // \"178 ml\" to \"180 ml\" and report a difference the caller never asked for.\n if (factor === 1) return passthroughIngredient(line, options.language);\n\n const text = line.trim();\n const parsed = parseIngredient(text, options.language ?? \"auto\");\n const branch = splitBranch(text, parsed);\n if (branch) return scaleBranchedLine(line, branch, { ...options, language: parsed.language });\n\n return scaleSingleLine(line, options);\n}\n\n/**\n * Scale a line that offers a choice, one branch at a time.\n *\n * A cook follows one branch and ignores the other, so both have to carry the\n * same share of the recipe: a doubled line whose second branch still reads as\n * published hands whoever takes it half the ingredient. The two branches name\n * different things, and how far one stands for the other is the page's claim\n * rather than arithmetic, so such a line is never reported as exact.\n */\nfunction scaleBranchedLine(line: string, branch: Branch, options: ScaleOptions): ScaledIngredient {\n const head = scaleSingleLine(branch.head, options);\n if (head.scaling === \"unscaled\") return { ...head, text: line.trim(), original: line };\n\n const tail = scaleAlternative(branch.tail, options);\n const result: ScaledIngredient = {\n ...head,\n original: line,\n text: `${head.text}${branch.separator}${tail.text}`,\n scaling: \"rounded\",\n };\n\n const branchNote = tail.rewritten\n ? \"This line offers a choice between two quantities, and each was scaled on its own. \" +\n \"How far one stands for the other is the page's own claim.\"\n : \"This line carries a further quantity after the first one, and only the first was scaled. \" +\n \"Read the rest as published.\";\n result.note = head.note ? `${head.note} ${branchNote}` : branchNote;\n\n return result;\n}\n\n/**\n * Scale the branch a line offers as an alternative, when it can be stated in\n * the measure the line offered it in.\n *\n * Under one of that measure the branch would have to be restated in another\n * one, which changes the shape of the choice the cook is being handed, so it\n * keeps its published wording and the line says that it did.\n */\nfunction scaleAlternative(\n tail: string,\n options: ScaleOptions,\n): { text: string; rewritten: boolean } {\n const parsed = parseIngredient(tail, options.language ?? \"auto\");\n const published = tail.trim();\n if (parsed.amount === null) return { text: published, rewritten: false };\n\n const largest = (parsed.amountMax ?? parsed.amount) * options.factor;\n if (largest < 1) return { text: published, rewritten: false };\n\n return { text: scaleIngredient(tail, options).text, rewritten: true };\n}\n\nfunction scaleSingleLine(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n const parsed = parseIngredient(line, options.language ?? \"auto\");\n const language = parsed.language;\n\n if (parsed.amount === null) {\n return {\n text: parsed.original,\n original: parsed.original,\n scaling: \"unscaled\",\n amount: null,\n amountMax: null,\n unit: null,\n language,\n note: \"No quantity given; adjust to taste.\",\n };\n }\n\n const divisibility = divisibilityOf(parsed.unit, parsed.item);\n const primary = scaleMeasure(parsed.amount, parsed.amountMax, parsed.unit, factor, divisibility);\n const alternates = parsed.alternates.map((measure) => renderMeasure(measure, factor, language));\n\n const primaryBounds = primary.bounds;\n const alternateBounds = alternates.flatMap((entry) => entry.bounds);\n const movedPrimary = primaryBounds.some((b) => !landedExactly(b.exact, b.amount));\n const movedAlternate = alternateBounds.some((b) => !landedExactly(b.exact, b.amount));\n const clamped = [...primaryBounds, ...alternateBounds].find((bound) => bound.clamped) ?? null;\n // Two figures beside each other agree only as closely as the page wrote\n // them, and multiplying both keeps that gap rather than closing it.\n const restated = parsed.alternateStyle === \"slash\";\n\n const low = primaryBounds[0]!;\n const high = primaryBounds[1] ?? null;\n const unit = primary.unit;\n const shown = high?.amount ?? low.amount;\n const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n // A range whose two ends land on the same amount stopped being a range. \"1 to\n // 1 clove\" is not something a cook reads, so the line states the one amount\n // both ends came to.\n const collapsed = high !== null && high.amount === low.amount;\n const amountText = renderRange(\n asText(low.amount),\n high === null || collapsed ? null : asText(high.amount),\n parsed.rangeSeparator,\n );\n // \"ea\" announces that the figure counts pieces, and names no measure of them,\n // so the line reads as the count of the thing itself and the marker has\n // nothing to say in it.\n const named = unit && !countsBarePieces(unit) ? unit : null;\n const unitLabel = named ? ` ${formatUnit(named, shown, language)}` : \"\";\n const alternateTexts = alternates.map((entry) => entry.text);\n // Equivalents go back the way the line offered them: inside brackets, or\n // after a slash beside the amount they restate.\n const altLabel =\n alternates.length === 0\n ? \"\"\n : parsed.alternateStyle === \"slash\"\n ? ` / ${alternateTexts.join(\" / \")}`\n : ` (${alternateTexts.join(\" / \")})`;\n // A measure needs the partitive that French puts between it and what it\n // measures. A counted item stands straight after its number in both\n // languages, and agrees with it: \"1 egg yolk\", \"3 brioches\".\n const counted = agreeWithAmount(parsed.item, shown, language);\n const itemLabel = named ? joinItem(parsed.item, language) : counted ? ` ${counted}` : \"\";\n\n const result: ScaledIngredient = {\n text: `${amountText}${unitLabel}${altLabel}${itemLabel}`.trim(),\n original: parsed.original,\n scaling: movedPrimary || movedAlternate || restated ? \"rounded\" : \"scaled\",\n amount: low.amount,\n amountMax: collapsed ? null : (high?.amount ?? null),\n unit: named?.canonical ?? null,\n language,\n };\n\n /**\n * The exact product, written for a note.\n *\n * Decimals rather than kitchen fractions: this number exists to be compared\n * against the one on the line, and a fraction snapped from 0.32 to \"1/3\"\n * reads as the exact product while being a different number.\n */\n const asPublished = (value: number, source: UnitInfo | null) =>\n `${formatAmount(value, language, { fractions: false })}${\n source ? ` ${formatUnit(source, value, language)}` : \"\"\n }`;\n\n const sentences: string[] = [];\n\n if (clamped) {\n sentences.push(\n `Clamped up to ${formatAmount(clamped.amount, language)} from ` +\n `${formatAmount(Math.round(clamped.raw * 1000) / 1000, language)}, the smallest amount ` +\n \"worth measuring. This line no longer holds its share of the recipe.\",\n );\n } else if (movedPrimary) {\n // Every bound that moved is named, with the direction it moved in. On a\n // range the two ends can move opposite ways, and reporting one of them as\n // though it spoke for both states the wrong direction for half the\n // quantity.\n const moved = primaryBounds.filter((bound) => !landedExactly(bound.exact, bound.amount));\n sentences.push(\n moved\n .map(\n (bound) =>\n `Rounded ${bound.amount > bound.exact ? \"up\" : \"down\"} from ` +\n `${asPublished(bound.raw, parsed.unit)}.`,\n )\n .join(\" \"),\n );\n } else if (movedAlternate) {\n // The amount itself came out exact, and only the equivalent beside it had\n // to move. Saying \"rounded from 300 g\" when 300 g is exact would send a\n // cook looking for an error that is not there.\n sentences.push(\n `The amount is exact; the equivalent ${\n restated ? \"beside it\" : \"in brackets\"\n } was rounded to stay readable.`,\n );\n } else if (restated) {\n sentences.push(\n \"This line states one quantity twice, and both readings were scaled. \" +\n \"They agree as closely as the page wrote them, and no closer.\",\n );\n }\n\n // A line can offer a substitute with its own amount, as in \"1 Tbsp vanilla\n // sugar OR 1 tsp vanilla extract\". Only the amount the line opens with is\n // scaled, and a substitute left at its published size contradicts it. This is\n // said whatever else happened to the line: a line that was also rounded is the\n // one where a stale second quantity is hardest to spot.\n if (hasEmbeddedMeasure(parsed.item, language)) {\n sentences.push(\n \"This line carries a further quantity after the first one, and only the first was scaled. \" +\n \"Read the rest as published.\",\n );\n }\n\n if (collapsed) {\n sentences.push(\"The page gave a range, and at this size both ends come to the same amount.\");\n }\n\n // Below what any scale shows, the arithmetic is right and the kitchen cannot\n // follow it. Saying so is the difference between an answer and a number.\n if (unit?.kind === \"measured\" && low.amount > 0 && low.amount < 0.05) {\n sentences.push(\n \"This is smaller than a kitchen scale resolves. Make a larger batch, or measure it by eye.\",\n );\n }\n\n if (sentences.length > 0) result.note = sentences.join(\" \");\n\n if (parsed.unit && parsed.unit.kind === \"approximate\") {\n result.note = withApproximateNote(parsed.unit, result.note);\n }\n\n // A line that wrote its amount as a word says which word it was, so a caller\n // can see the figure came from the grammar rather than from a digit.\n if (parsed.articleWord) {\n // `amount` carries the product once a word such as \"dozen\" has multiplied\n // it, and quoting that back would credit the article with a figure it never\n // gave.\n const stood = (parsed.amount ?? 0) / (parsed.countMultiplier ?? 1);\n const read = `\"${parsed.articleWord}\" read as ${formatAmount(stood, language)}.`;\n result.note = result.note ? `${read} ${result.note}` : read;\n }\n\n return result;\n}\n\n/**\n * Say that a measure is held to no better than the hand that produces it, and\n * what a kitchen usually takes one to be.\n *\n * The equivalence belongs in the note. A recipe that asks for four pinches of\n * baking soda has said nothing about teaspoons, and answering in teaspoons\n * would hand back a figure with a precision the page never claimed. The\n * quantity stays in the measure the line used, and the count is what carries\n * the scaling.\n */\nfunction withApproximateNote(unit: UnitInfo, existing: string | undefined): string {\n const equivalence = approximateEquivalent(unit);\n const sentence =\n `A ${unit.canonical} is an approximate measure${equivalence ? `, ${equivalence}` : \"\"}. ` +\n \"The count was scaled and the size of one is the cook's.\";\n return existing ? `${existing} ${sentence}` : sentence;\n}\n\n/**\n * Scale an equivalent the line states beside the amount, and render it the way\n * the line wrote it.\n */\nfunction renderMeasure(\n measure: Measure,\n factor: number,\n language: Language,\n): { text: string; bounds: ScaledBound[] } {\n const scaled = scaleMeasure(\n measure.amount,\n measure.amountMax,\n measure.unit,\n factor,\n divisibilityOf(measure.unit, \"\"),\n );\n const low = scaled.bounds[0]!;\n const high = scaled.bounds[1] ?? null;\n const unit = scaled.unit;\n const shown = high?.amount ?? low.amount;\n const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n return {\n text: `${renderRange(\n asText(low.amount),\n high === null ? null : asText(high.amount),\n measure.rangeSeparator,\n )}${unit ? ` ${formatUnit(unit, shown, language)}` : \"\"}`,\n bounds: scaled.bounds,\n };\n}\n\n/** Keep a range in the shape the recipe wrote it: \"3–4\", \"2 to 3\" or \"2 à 3\". */\nfunction renderRange(low: string, high: string | null, separator: string | null): string {\n if (high === null || separator === null) return low;\n return /^[-–—]$/.test(separator) ? `${low}${separator}${high}` : `${low} ${separator} ${high}`;\n}\n\nexport function scaleIngredients(lines: string[], options: ScaleOptions): ScaledIngredient[] {\n return lines.map((line) => scaleIngredient(line, options));\n}\n\n/**\n * A line returned as published, with whatever quantity could be read off it.\n *\n * A line that carries a readable amount is `scaled`, because leaving it alone\n * is what multiplying by one does. A line with no amount at all is `unscaled`\n * and says why.\n */\nexport function passthroughIngredient(\n line: string,\n language: LanguageChoice = \"auto\",\n): ScaledIngredient {\n const parsed = parseIngredient(line, language);\n\n const result: ScaledIngredient = {\n text: parsed.original,\n original: parsed.original,\n scaling: parsed.amount === null ? \"unscaled\" : \"scaled\",\n amount: parsed.amount,\n amountMax: parsed.amountMax,\n unit: parsed.unit?.canonical ?? null,\n language: parsed.language,\n };\n if (parsed.amount === null) result.note = \"No quantity given; adjust to taste.\";\n else if (parsed.unit?.kind === \"approximate\") {\n result.note = withApproximateNote(parsed.unit, undefined);\n }\n return result;\n}\n\n/** An ingredient list returned unchanged, for when no scaling was requested. */\nexport function passthroughIngredients(\n lines: string[],\n language: LanguageChoice = \"auto\",\n): ScaledIngredient[] {\n return lines.map((line) => passthroughIngredient(line, language));\n}\n","/** Schemas, error mapping and rendering shared by the tools. */\n\nimport { z } from \"zod\";\nimport { RecipesError } from \"../errors.js\";\nimport type { ScaledIngredient } from \"../recipe/scale.js\";\nimport type { RecipeRow, SourceReport } from \"../types.js\";\n\n/**\n * The text block is what many clients render, and some render nothing else, so\n * it has to answer on its own. This ceiling is what keeps a recipe with forty\n * ingredients from arriving as a wall of text.\n */\nexport const MAX_TEXT_CHARS = 2200;\n\nexport interface ToolResult {\n // The SDK's result type carries an index signature for protocol extensions.\n [key: string]: unknown;\n content: Array<{ type: \"text\"; text: string }>;\n structuredContent?: Record<string, unknown>;\n isError?: boolean;\n}\n\n/** One search row, carrying what it takes to pick a recipe out of a list. */\nexport const rowSchema = z.object({\n id: z\n .string()\n .describe(\"Pass this to get_recipe. It names its source, so nothing has to be guessed.\"),\n source: z.string().describe(\"Which source published this row.\"),\n source_name: z.string(),\n title: z.string(),\n url: z.string().describe(\"The recipe's own page. Show this when citing it.\"),\n image_url: z.string().nullable(),\n excerpt: z\n .string()\n .nullable()\n .describe(\"The gloss or matching passage the source offered, when it offered one.\"),\n});\n\n/**\n * What one source answered, or why it did not.\n *\n * Present for every source asked, including the ones that failed, because an\n * answer that quietly drops a source reads as a complete answer that found\n * less.\n */\nexport const reportSchema = z.object({\n source: z.string(),\n name: z.string(),\n status: z.enum([\"answered\", \"failed\"]).describe(\"Whether this source replied at all.\"),\n count: z.number().int().describe(\"Rows this source contributed to the answer.\"),\n reported_total: z\n .number()\n .int()\n .nullable()\n .describe(\"What the source said it saw. Null when it states no number at all.\"),\n reported_total_means: z\n .string()\n .nullable()\n .describe(\n \"What 'reported_total' counts on this source. Counts are never summed across sources.\",\n ),\n skipped: z\n .number()\n .int()\n .describe(\"Rows this source sent in a shape the server could not read, and left out.\"),\n cached: z.boolean().describe(\"Served from this server's short-lived in-memory cache.\"),\n error: z\n .object({\n code: z.string(),\n message: z.string(),\n hint: z.string().optional(),\n })\n .nullable()\n .describe(\"Why this source did not answer. Null when it did.\"),\n});\n\n/** One ingredient line, scaled, in the shape every source is read into. */\nexport const ingredientSchema = z.object({\n text: z.string().describe(\"The line as it now reads.\"),\n original: z.string().describe(\"The line as published.\"),\n scaling: z\n .enum([\"scaled\", \"rounded\", \"unscaled\"])\n .describe(\n \"'scaled' means the arithmetic landed on the exact product, 'rounded' that something had to move for the line to stay usable or that the line carries a second quantity whose match is the page's claim, 'unscaled' that there is nothing to multiply.\",\n ),\n amount: z\n .number()\n .nullable()\n .describe(\"The scaled quantity, in 'unit'. Read the two together, never the number alone.\"),\n amount_max: z.number().nullable().describe(\"Upper bound when the line gives a range.\"),\n unit: z\n .string()\n .nullable()\n .describe(\"The unit 'amount' is in, which may differ from the page's.\"),\n language: z.enum([\"fr\", \"en\"]).describe(\"The language the line was read and rewritten in.\"),\n note: z.string().optional().describe(\"What happened to this line, when anything did.\"),\n});\n\nexport function toIngredientPayload(entry: ScaledIngredient): z.infer<typeof ingredientSchema> {\n return {\n text: entry.text,\n original: entry.original,\n scaling: entry.scaling,\n amount: entry.amount,\n amount_max: entry.amountMax,\n unit: entry.unit,\n language: entry.language,\n ...(entry.note ? { note: entry.note } : {}),\n };\n}\n\nexport function toRowPayload(row: RecipeRow): z.infer<typeof rowSchema> {\n return {\n id: row.id,\n source: row.source,\n source_name: row.sourceName,\n title: row.title,\n url: row.url,\n image_url: row.imageUrl,\n // An excerpt is a source's own prose, and a long one crowds out the rows it\n // was meant to help choose between.\n excerpt: row.excerpt === null ? null : truncate(row.excerpt, MAX_EXCERPT_CHARS),\n };\n}\n\n/** How much of a source's own prose a search row carries. */\nconst MAX_EXCERPT_CHARS = 200;\n\nexport function toReportPayload(report: SourceReport): z.infer<typeof reportSchema> {\n return {\n source: report.source,\n name: report.name,\n status: report.status,\n count: report.count,\n reported_total: report.reportedTotal,\n reported_total_means: report.reportedTotalMeans,\n skipped: report.skipped,\n cached: report.cached,\n error: report.error ?? null,\n };\n}\n\n/**\n * Turn the per-source reports into sentences a reader can act on.\n *\n * A failed source is named with the reason, so an answer holding part of what\n * was asked for never reads as the whole of what exists.\n */\nexport function reportNotes(reports: SourceReport[]): string[] {\n const notes: string[] = [];\n\n const answered = reports.filter((report) => report.status === \"answered\");\n const failed = reports.filter((report) => report.status === \"failed\");\n\n for (const report of failed) {\n // What the rest of the answer holds depends on whether anything else\n // answered. Promising a caller that another source made up the difference\n // when none did is the failure this whole shape exists to prevent.\n const consolation =\n answered.length > 0\n ? `This answer holds what the other sources found, and says nothing about what ${report.name} holds.`\n : \"Nothing here is evidence about what it holds.\";\n notes.push(\n `${report.name} did not answer (${report.error?.code}): ${report.error?.message} ${consolation}`,\n );\n }\n\n for (const report of answered) {\n if (report.count === 0) {\n notes.push(\n `${report.name} answered and offered no row for this wording. That is a statement about ` +\n \"the wording as much as about the corpus: try the dish's name in another language, or a \" +\n \"main ingredient.\",\n );\n }\n if (report.skipped > 0) {\n notes.push(\n `${report.name} sent ${report.skipped} row(s) in a shape this server could not read, and they were left out.`,\n );\n }\n if (report.reportedTotal !== null && report.reportedTotalMeans !== null) {\n notes.push(`${report.name} reported ${report.reportedTotal}: ${report.reportedTotalMeans}.`);\n } else {\n notes.push(\n `${report.name} states no total and offers no second page, so a short list here is not evidence that little exists.`,\n );\n }\n }\n\n if (answered.some((report) => report.mixesReferencePages && report.count > 0)) {\n const mixing = answered.filter((report) => report.mixesReferencePages && report.count > 0);\n notes.push(\n `${mixing.map((report) => report.name).join(\" and \")} files pages about an ingredient beside ` +\n \"recipes using it, so a row here can be a page about an ingredient rather than a recipe. \" +\n \"get_recipe says when a page carries no ingredient list.\",\n );\n }\n\n if (answered.length > 1) {\n notes.push(\n \"Each count above measures something different, and they are never added together into one total.\",\n );\n }\n\n if (reports.some((report) => report.cached)) {\n notes.push(\"Part of this answer came from this server's short-lived in-memory cache.\");\n }\n\n return notes;\n}\n\n/**\n * Words this server writes at the start of one of its own lines.\n *\n * A caller has no way to tell one of these from the same words inside a title,\n * an ingredient or a step written by whoever published it, so a value opening\n * with one is indented before it is rendered. Spacing and case are allowed for,\n * because a forged line only has to look like one of these to a reader.\n */\nconst SERVER_MARKERS = /^(\\s*)(Note|Sources?|Hint|Ingredients|Steps|What differs)(\\s*:)/gim;\n\n/**\n * Keep text somebody else wrote out of the shape this server's own lines take.\n *\n * Applied to the foreign value rather than to the assembled block, because the\n * server writes those same words itself: indenting the whole block would indent\n * its own headings and leave the answer looking like quoted text.\n *\n * The structured output still carries the text exactly as it was published;\n * this is the rendered block only.\n */\nexport function indentMarkerLines(value: string): string {\n return value.replace(SERVER_MARKERS, \" $1$2$3\");\n}\n\n/**\n * Put a value somebody else wrote onto one of this server's own lines.\n *\n * Two things make that safe. A line terminator is turned into a space, because\n * a title or a licence carrying a newline can otherwise close the server's\n * sentence and open a whole section of its own: a forged \"Steps:\" block, a\n * forged search row with an address of its own, or a forged credit line. And an\n * image is defused, because most clients render this block as markdown and an\n * image tag fetches its address the moment it is drawn.\n *\n * Everything a source or a caller wrote passes through here on its way into the\n * text block, including into the notes and the credit, which are the lines a\n * reader trusts most.\n */\nexport function quoteForeign(value: string): string {\n return (\n value\n // Every line terminator Unicode recognises, so a value can occupy one line\n // and no more.\n .replace(/[\\r\\n\\u2028\\u2029\\u0085]+/g, \" \")\n // Control characters, which render as nothing and hide what follows them.\n .replace(/[\\u0000-\\u0008\\u000b-\\u001f\\u007f]/g, \"\")\n // An image tag fetches its address on render; escaping the mark leaves the\n // text readable and the request unmade.\n .replace(/!\\[/g, \"!\\\\[\")\n .replace(/\\s{2,}/g, \" \")\n .trim()\n // Now that it occupies one line, a value opening on one of the server's\n // own markers is the last way it could pass for a line the server wrote.\n .replace(SERVER_MARKERS, \" $1$2$3\")\n );\n}\n\n/**\n * Notes that have to survive when the block is too long for all of them.\n *\n * A note saying a source failed, a note carrying the terms a page is published\n * under, and a note saying an answer is smaller than it looks are the ones a\n * reader most needs and the ones an over-long answer is most likely to have.\n * Dropping from the end alone would drop exactly these.\n */\nconst LOAD_BEARING =\n /did not answer|could not be read|no longer holds its share|Published under|licen[cs]e|https?:\\/\\/|rather than a comparison|states no number of servings|no ingredient list|offered no row/i;\n\n/**\n * How much of the block the notes may take.\n *\n * Enough for the sentences that qualify an answer, while leaving the answer\n * they qualify room to be one. Whatever is dropped is still in the structured\n * output, where a caller reading that instead loses nothing.\n */\nconst NOTE_BUDGET = Math.round(MAX_TEXT_CHARS * 0.55);\n\nexport interface OkOptions {\n notes?: string[];\n /** The line that credits whoever published what the answer holds. */\n credit?: string;\n}\n\n/**\n * Build a result whose text block ends with its notes and its credit.\n *\n * The body is cut to fit around the trailer rather than the whole block being\n * cut afterwards. Appending the credit and then truncating loses exactly the\n * credit, which is the one line that must survive.\n *\n * Notes qualify an answer: that a source failed, that a quantity was rounded,\n * that a count means one thing here and another there. A client that shows only\n * the text would otherwise present an unqualified answer, so they travel with\n * the credit.\n */\n/**\n * The notes the block will carry, and the trailer they make with the credit.\n *\n * A long run of notes must not crowd out the answer it qualifies. What goes\n * first is the last note that qualifies the answer least, so the ones a reader\n * cannot do without are still there when the room runs out. Whatever is dropped\n * stays in the structured output.\n */\nfunction buildTrailer(options: OkOptions): string {\n const credit = options.credit ?? \"Source: this server called none.\";\n const kept = [...new Set(options.notes ?? [])];\n\n while (kept.length > 0 && kept.join(\"\\n\").length > NOTE_BUDGET) {\n let victim = kept.length - 1;\n for (let index = kept.length - 1; index >= 0; index -= 1) {\n if (!LOAD_BEARING.test(kept[index]!)) {\n victim = index;\n break;\n }\n }\n kept.splice(victim, 1);\n }\n\n return [...kept.map((note) => `Note: ${note}`), credit].join(\"\\n\");\n}\n\n/**\n * Build a result whose text block ends with its notes and its credit.\n *\n * The body is cut to fit around the trailer rather than the whole block being\n * cut afterwards. Appending the credit and then truncating loses exactly the\n * credit, which is the one line that must survive.\n *\n * Notes qualify an answer: that a source failed, that a quantity was rounded,\n * that a count means one thing here and another there. A client that shows only\n * the text would otherwise present an unqualified answer, so they travel with\n * the credit.\n */\nexport function ok(\n structured: Record<string, unknown>,\n body: string,\n options: OkOptions = {},\n): ToolResult {\n const trailer = buildTrailer(options);\n const cut = \"\\n\\n[shortened; the full result is in the structured output]\";\n const room = MAX_TEXT_CHARS - `\\n\\n${trailer}`.length;\n const text =\n body.length <= room\n ? `${body}\\n\\n${trailer}`\n : `${truncate(body, Math.max(0, room - cut.length))}${cut}\\n\\n${trailer}`;\n\n return { content: [{ type: \"text\", text }], structuredContent: structured };\n}\n\n/**\n * Errors carry no structured payload: the SDK checks it against the tool's\n * declared output schema, and a failure does not fit that shape.\n */\nexport function toToolError(error: unknown): ToolResult {\n const known =\n error instanceof RecipesError\n ? error\n : new RecipesError(\"network_error\", error instanceof Error ? error.message : String(error));\n\n const lines = [`[${known.code}] ${quoteForeign(known.message)}`];\n if (known.details.hint) lines.push(`Hint: ${quoteForeign(known.details.hint)}`);\n return { content: [{ type: \"text\", text: lines.join(\"\\n\") }], isError: true };\n}\n\n/**\n * How much room a body has once its notes and its credit are set aside.\n *\n * A tool rendering several things at once shares this out between them, so each\n * gets an opening rather than the first one filling the block and the rest\n * being cut away.\n */\nexport function roomForBody(options: OkOptions = {}): number {\n return Math.max(200, MAX_TEXT_CHARS - buildTrailer(options).length - 60);\n}\n\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) return text;\n return `${text.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;\n}\n\n/** A compact listing, carrying what it takes to pick one recipe out of many. */\nexport function renderRows(rows: Array<z.infer<typeof rowSchema>>): string {\n return rows\n .map((row, index) => {\n const head = `${index + 1}. ${quoteForeign(row.title)} · ${quoteForeign(row.source_name)} · id: ${row.id}`;\n // The address goes on its own line: a client that renders only text has\n // nothing else to cite from, and a model with an identifier and no link\n // will build one.\n return `${head}\\n ${quoteForeign(row.url)}`;\n })\n .join(\"\\n\");\n}\n\n/**\n * The credit line, naming the sources that actually contributed to this answer.\n *\n * A source that failed, or that was never asked, has published nothing here and\n * crediting it would say it had.\n */\nexport function creditLine(contributors: Array<{ attribution: string; url?: string }>): string {\n if (contributors.length === 0) return \"No source contributed to this answer.\";\n const names = contributors.map((entry) =>\n entry.url ? `${entry.attribution} — ${quoteForeign(entry.url)}` : entry.attribution,\n );\n return names.join(\"\\n\");\n}\n","/**\n * How a tool's arguments are declared, and what happens to one that is not.\n *\n * An argument this server does not declare is a question it cannot answer.\n * Reading it and dropping it produces an answer computed on the defaults, which\n * a caller reads as the answer to what they asked. So an undeclared argument is\n * refused, and the refusal names it and offers the declared name when one is\n * close enough to be the one that was meant.\n */\n\nimport { z } from \"zod\";\n\n/** The code a caller branches on when the arguments cannot produce a request. */\nconst INVALID_INPUT = \"invalid_input\";\n\n/** Declare a tool's arguments, refusing anything outside the declaration. */\nexport function strictInput<Shape extends z.ZodRawShape>(shape: Shape) {\n const declared = Object.keys(shape);\n\n return z.strictObject(shape, {\n error: (issue) =>\n issue.code === \"unrecognized_keys\" ? unknownArgumentMessage(issue.keys, declared) : undefined,\n });\n}\n\nfunction unknownArgumentMessage(keys: readonly string[], declared: readonly string[]): string {\n const named = keys\n .map((key) => {\n const near = nearestArgument(key, declared);\n return near ? `'${key}' (did you mean '${near}'?)` : `'${key}'`;\n })\n .join(\", \");\n\n return (\n `[${INVALID_INPUT}] Unknown ${keys.length > 1 ? \"arguments\" : \"argument\"} ${named}. ` +\n `This tool takes: ${declared.join(\", \")}.`\n );\n}\n\n/**\n * The declared name a caller most plausibly meant, when there is one.\n *\n * Three readings, ordered by how much each claims: the same name written\n * differently, a name that opens or closes the other, and a name a couple of\n * typing slips away. Anything further is left unnamed, because a suggestion\n * that misses sends a caller to an argument answering a different question.\n */\nfunction nearestArgument(key: string, declared: readonly string[]): string | undefined {\n const flatten = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n const flat = flatten(key);\n if (flat.length === 0) return undefined;\n\n const sameName = declared.find((name) => flatten(name) === flat);\n if (sameName) return sameName;\n\n // Either name may be the longer one: a caller can qualify a name this tool\n // keeps plain, or shorten one it spells out.\n const overlapping = declared.find((name) => {\n const other = flatten(name);\n const [shorter, longer] = other.length < flat.length ? [other, flat] : [flat, other];\n // Two characters in common say nothing; three start to.\n return shorter.length >= 3 && (longer.startsWith(shorter) || longer.endsWith(shorter));\n });\n if (overlapping) return overlapping;\n\n let closest: string | undefined;\n let shortest = Number.POSITIVE_INFINITY;\n for (const name of declared) {\n const distance = editDistance(flat, flatten(name));\n if (distance < shortest) {\n shortest = distance;\n closest = name;\n }\n }\n\n // Up to a third of the name may differ. Past that the match is a guess.\n return shortest <= Math.max(1, Math.floor(flat.length / 3)) ? closest : undefined;\n}\n\n/** Single-character insertions, deletions and substitutions between two words. */\nfunction editDistance(left: string, right: string): number {\n let previous: number[] = Array.from({ length: right.length + 1 }, (_, index) => index);\n\n for (let row = 1; row <= left.length; row += 1) {\n const current: number[] = [row];\n for (let column = 1; column <= right.length; column += 1) {\n // Every index here is inside a row this loop has already filled.\n const substitution = previous[column - 1]! + (left[row - 1] === right[column - 1] ? 0 : 1);\n current[column] = Math.min(substitution, previous[column]! + 1, current[column - 1]! + 1);\n }\n previous = current;\n }\n\n return previous[right.length]!;\n}\n","/**\n * get_recipe: read one recipe, from the source its identifier names.\n *\n * Only that source is called. Trying another after a miss would answer a\n * question about one corpus with somebody else's recipe of a different name,\n * which is a worse answer than the absence it replaced.\n */\n\nimport { z } from \"zod\";\nimport type { RecipesClient } from \"../sources/client.js\";\nimport { buildRecipeView, recipeSchema, renderYield, SECTIONS } from \"./recipeView.js\";\nimport type { Section } from \"./recipeView.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { creditLine, ok, quoteForeign, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const getRecipeDescription = [\n \"Read one recipe in full: its ingredients, its steps, what it yields, and whatever times, rating and nutrition its source publishes.\",\n \"'id' must come from search_recipes. It names the source, so this reads the right one without guessing; an identifier no source would have minted is refused, because sending it anywhere would answer about the wrong dish.\",\n \"Pass 'servings' to rescale. Quantities land where a kitchen can follow them: an egg stays whole, anything that pours or cuts can halve, a small measurement moves to a smaller unit before it is rounded, and anything unmultipliable is flagged rather than scaled.\",\n \"A page that states no number of servings comes back as published and says so, because dividing by a yield nobody wrote would answer for a number of people the page never claimed.\",\n \"'sections' decides what comes back, and 'sections_omitted' names what was left out: a field belonging to an omitted section is empty because nobody asked for it, never because the page states nothing.\",\n \"A field a source does not publish is null, never zero. Credit the source and link the url when you repeat any of it.\",\n].join(\" \");\n\nexport const getRecipeInput = strictInput({\n id: z\n .string()\n .min(1)\n .max(500)\n .describe(\"From search_recipes, such as 'marmiton:44078' or 'cookbook:Cookbook:Carbonara'.\"),\n servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Rescale to this many. Left out, the quantities come back as published.\"),\n sections: z\n .array(z.enum(SECTIONS))\n .default([\"ingredients\", \"steps\"])\n .describe(\"Which parts to return. A full recipe is a lot of text, so this defaults to two.\"),\n max_steps: z\n .number()\n .int()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Steps to return. The answer says how many more there are.\"),\n max_step_chars: z\n .number()\n .int()\n .min(80)\n .max(4000)\n .default(600)\n .describe(\"Characters kept per step. Raise it only if a step was cut mid-sentence.\"),\n});\n\nexport const getRecipeOutput = z.object({\n recipe: recipeSchema,\n id_read_as: z\n .string()\n .nullable()\n .describe(\"How a raw identifier was routed, when it was not spelled with its source.\"),\n notes: z.array(z.string()),\n});\n\nexport type GetRecipeArgs = z.infer<typeof getRecipeInput>;\n\nexport async function runGetRecipe(\n client: RecipesClient,\n args: GetRecipeArgs,\n): Promise<ToolResult> {\n try {\n const { recipe, cached, read } = await client.getRecipe(args.id);\n const view = buildRecipeView(recipe, {\n servings: args.servings ?? null,\n sections: args.sections as readonly Section[],\n maxSteps: args.max_steps,\n maxStepChars: args.max_step_chars,\n });\n\n const notes = [...view.notes];\n if (read.inferred) {\n notes.unshift(\n `\"${quoteForeign(args.id)}\" was read as ${read.source.name}'s, because ${read.inferred}. ` +\n \"Spell an id with its source to leave nothing to infer.\",\n );\n }\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n\n const payload = view.payload;\n const lines = [\n `${quoteForeign(payload.title)} · ${quoteForeign(payload.source_name)}`,\n quoteForeign(payload.url),\n renderYield(payload),\n ];\n if (payload.ingredients.length > 0) {\n lines.push(\n \"\",\n \"Ingredients:\",\n ...payload.ingredients.map((entry) =>\n entry.scaling === \"unscaled\"\n ? `- ${quoteForeign(entry.text)} (no quantity)`\n : `- ${quoteForeign(entry.text)}`,\n ),\n );\n }\n if (payload.steps.length > 0) {\n lines.push(\n \"\",\n \"Steps:\",\n ...payload.steps.map((step, index) => `${index + 1}. ${quoteForeign(step)}`),\n );\n }\n\n return ok({ recipe: payload, id_read_as: read.inferred, notes }, lines.join(\"\\n\"), {\n notes,\n credit: creditLine([{ attribution: payload.attribution, url: payload.url }]),\n });\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * scale_ingredients: multiply a list a caller already holds.\n *\n * Nothing here touches the network. The list can come from any of the sources,\n * from a cookbook on a shelf or from a photograph of a card, and it can mix\n * French and English lines: each line is read in the language it is written in\n * and comes back written the same way.\n */\n\nimport { z } from \"zod\";\nimport { invalidInput } from \"../errors.js\";\nimport { scaleIngredients } from \"../recipe/scale.js\";\nimport type { LanguageChoice } from \"../recipe/scale.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { ingredientSchema, ok, quoteForeign, toIngredientPayload, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const scaleIngredientsDescription = [\n \"Multiply a list of ingredient lines, in French, in English, or in a list holding both.\",\n \"Give either 'factor', or 'from_servings' and 'to_servings' and the factor is worked out from them.\",\n \"Quantities land where a kitchen can follow them: an egg stays whole because half of one is not something a cook takes out of the shell, while anything that pours, weighs or cuts can halve, a spoonful shrinks into the smaller spoon before it is rounded, and a pinch keeps whatever size a hand gives it while its count is multiplied.\",\n \"Every line comes back with 'scaling': 'scaled' when the arithmetic landed on the exact product, 'rounded' when something had to move for the line to stay usable, 'unscaled' when the line carries nothing to multiply. A rounded line says what it was rounded from and in which direction.\",\n \"No quantity is converted between measuring systems: grams stay grams and cups stay cups, because a conversion changes what the recipe said.\",\n].join(\" \");\n\nexport const scaleIngredientsInput = strictInput({\n ingredients: z\n .array(z.string().max(500))\n .min(1)\n .max(200)\n .describe(\n \"The lines as written, one ingredient each, such as '200 g de farine' or '3 eggs'. One line, not a whole recipe.\",\n ),\n factor: z\n .number()\n .gt(0)\n .max(1000)\n .optional()\n .describe(\n \"What to multiply by. Give exactly one of: 'factor', or both 'from_servings' and 'to_servings'.\",\n ),\n from_servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"What the list serves now. Give it with 'to_servings', and without 'factor'.\"),\n to_servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"What it should serve. Give it with 'from_servings', and without 'factor'.\"),\n language: z\n .enum([\"auto\", \"fr\", \"en\"])\n .default(\"auto\")\n .describe(\n \"'auto' reads each line on its own, which is what a list holding both languages needs. Name a language to read every line that way.\",\n ),\n});\n\nexport const scaleIngredientsOutput = z.object({\n factor: z.number().describe(\"What every quantity was multiplied by.\"),\n language: z.string().describe(\"How the lines were read.\"),\n ingredients: z.array(ingredientSchema),\n scaled_count: z.number().int().describe(\"Lines whose arithmetic came out exact.\"),\n rounded_count: z.number().int().describe(\"Lines whose value moved to stay usable.\"),\n unscaled_count: z.number().int().describe(\"Lines carrying nothing that can be multiplied.\"),\n notes: z.array(z.string()),\n});\n\nexport type ScaleIngredientsArgs = z.infer<typeof scaleIngredientsInput>;\n\n/**\n * Read the factor off whichever pair of arguments the caller gave.\n *\n * The two ways of saying it are kept apart rather than merged, because a caller\n * who gives both and means different things by them has made a mistake worth\n * hearing about.\n */\nexport function resolveFactor(args: ScaleIngredientsArgs): number {\n const hasPair = args.from_servings !== undefined && args.to_servings !== undefined;\n\n if (args.factor !== undefined && hasPair) {\n throw invalidInput(\n \"Give either 'factor' or the pair of serving counts, not both.\",\n `factor=${args.factor} and ${args.from_servings}→${args.to_servings} would be two different multipliers.`,\n );\n }\n\n if (args.factor !== undefined) return args.factor;\n if (hasPair) return args.to_servings! / args.from_servings!;\n\n if (args.from_servings !== undefined || args.to_servings !== undefined) {\n throw invalidInput(\n \"Serving counts come in pairs: give both 'from_servings' and 'to_servings'.\",\n \"Or give 'factor' on its own.\",\n );\n }\n\n throw invalidInput(\n \"Nothing said how much to multiply by.\",\n \"Give 'factor', or both 'from_servings' and 'to_servings'.\",\n );\n}\n\nexport function runScaleIngredients(args: ScaleIngredientsArgs): ToolResult {\n try {\n const factor = resolveFactor(args);\n const scaled = scaleIngredients(args.ingredients, {\n factor,\n language: args.language as LanguageChoice,\n });\n\n const counts = {\n scaled_count: scaled.filter((entry) => entry.scaling === \"scaled\").length,\n rounded_count: scaled.filter((entry) => entry.scaling === \"rounded\").length,\n unscaled_count: scaled.filter((entry) => entry.scaling === \"unscaled\").length,\n };\n\n const notes: string[] = [];\n if (counts.rounded_count > 0) {\n notes.push(\n `${counts.rounded_count} line(s) did not land on the exact product; each says in its own ` +\n \"note what happened to it.\",\n );\n }\n if (counts.unscaled_count > 0) {\n notes.push(\n `${counts.unscaled_count} line(s) carry no quantity to multiply and are repeated as given.`,\n );\n }\n if (args.language === \"auto\") {\n const languages = new Set(scaled.map((entry) => entry.language));\n if (languages.size > 1) {\n notes.push(\n \"This list holds lines in both languages, and each was read and rewritten in its own.\",\n );\n }\n }\n if (scaled.some((entry) => entry.note?.includes(\"approximate measure\"))) {\n notes.push(\n \"A pinch, a handful and their kind were multiplied by their count. The size of one stays \" +\n \"whatever the cook's hand gives it, and none of them was turned into grams or spoons.\",\n );\n }\n\n const body = scaled\n .map((entry, index) => `${index + 1}. ${quoteForeign(entry.text)}`)\n .join(\"\\n\");\n\n return ok(\n {\n factor: Math.round(factor * 1000) / 1000,\n language: args.language,\n ingredients: scaled.map(toIngredientPayload),\n ...counts,\n notes,\n },\n `Scaled by ${Math.round(factor * 1000) / 1000}:\\n${body}`,\n { notes, credit: \"Source: the list you supplied. Scaled offline; nothing was fetched.\" },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * search_recipes: one question, every source, one list.\n *\n * The calls go out together, so asking them all costs about what asking one\n * costs. What each source said about its own results is reported beside the\n * rows, because those numbers count different things and adding them would\n * invent a figure no source published.\n */\n\nimport { z } from \"zod\";\nimport type { RecipesClient } from \"../sources/client.js\";\nimport type { SourceId } from \"../types.js\";\nimport {\n creditLine,\n ok,\n renderRows,\n reportNotes,\n reportSchema,\n rowSchema,\n toReportPayload,\n toRowPayload,\n toToolError,\n} from \"./shared.js\";\nimport { strictInput } from \"./arguments.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const searchRecipesDescription = [\n \"Search every recipe source this server reads, at the same time, for a dish or an ingredient, and get one merged list.\",\n \"Each row carries the id get_recipe takes, and that id names the source it came from, so nothing has to be guessed afterwards.\",\n \"The sources are written in different languages and count their own results differently, so 'per_source' says what each one answered, what its own number means, and names any that failed. A short list is never evidence of what exists.\",\n \"Some sources keep recipes and reference pages together, so a row can be a page about an ingredient rather than a recipe using it. Only get_recipe can tell them apart, and it says when a page carries no ingredient list.\",\n \"The query goes to each source's own search as free text. There is no filtering: a word naming a diet, a time or a calorie count matches only where that source's index happens to carry it.\",\n \"Rows are interleaved one source at a time rather than ranked, because the sources share no score that could order them against each other.\",\n].join(\" \");\n\nexport const searchRecipesInput = strictInput({\n query: z\n .string()\n .min(1)\n .max(200)\n .describe(\"A dish or an ingredient, in any of the languages the sources publish in.\"),\n limit_per_source: z\n .number()\n .int()\n .min(1)\n .max(25)\n .default(5)\n .describe(\"Rows to take from each source, so one source cannot fill the whole list.\"),\n sources: z\n .array(z.string())\n .optional()\n .describe(\n \"Sources to ask, by id. Left out, they are all asked, which is the point of this tool. The ids are the ones 'per_source' reports.\",\n ),\n});\n\nexport const searchRecipesOutput = z.object({\n query: z.string(),\n results: z.array(rowSchema),\n result_count: z.number().int().describe(\"Rows in this answer, across every source.\"),\n per_source: z.array(reportSchema),\n order: z.string().describe(\"How the list was built, in words.\"),\n notes: z.array(z.string()),\n});\n\nexport type SearchRecipesArgs = z.infer<typeof searchRecipesInput>;\n\n/**\n * Beyond this many words a question is asking for conditions rather than naming\n * a dish, so the answer says that none of them were applied as conditions.\n */\nconst WORDS_BEFORE_SAYING_SO = 4;\n\nexport async function runSearchRecipes(\n client: RecipesClient,\n args: SearchRecipesArgs,\n): Promise<ToolResult> {\n try {\n const merged = await client.searchRecipes(\n args.query,\n args.limit_per_source,\n args.sources as readonly SourceId[] | undefined,\n );\n\n const results = merged.rows.map(toRowPayload);\n const notes = reportNotes(merged.reports);\n\n const answered = merged.reports.filter((report) => report.status === \"answered\");\n const contributed = merged.reports.filter((report) => report.count > 0);\n\n // How the order was built depends on how many sources are in it. Describing\n // a merge that did not happen tells a caller the list means something it\n // does not.\n const order =\n contributed.length > 1\n ? \"One row from each source in turn, in the order each source returned them. No score orders them against each other.\"\n : contributed.length === 1\n ? `Every row came from ${contributed[0]!.name}, in the order it returned them.`\n : \"No source contributed a row.\";\n\n // A long question reads like a request for filtering, and none is applied:\n // the words go to each source's own search as they were typed. A caller\n // told nothing would report \"desserts under 300 calories\" as a filtered\n // answer, when the number was matched as text or matched nothing at all.\n if (args.query.trim().split(/\\s+/).length > WORDS_BEFORE_SAYING_SO) {\n notes.push(\n \"This query went to each source as free text and no filter was applied to it. Words like \" +\n \"'vegan' or 'under 300 calories' help only where a source's own index happens to match \" +\n \"them, so read the rows rather than assuming they meet every condition.\",\n );\n }\n\n if (results.length === 0 && answered.length === merged.reports.length) {\n notes.push(\n \"Every source answered and none holds anything under this wording. Try the dish's name in another language, or name a main ingredient instead.\",\n );\n }\n\n const body =\n results.length > 0\n ? `${results.length} recipes for \"${args.query}\":\\n${renderRows(results)}`\n : answered.length === 0\n ? `No source answered for \"${args.query}\", so nothing here says whether such a recipe exists.`\n : `Nothing came back for \"${args.query}\".`;\n\n return ok(\n {\n query: args.query,\n results,\n result_count: results.length,\n per_source: merged.reports.map(toReportPayload),\n order,\n notes,\n },\n body,\n {\n notes,\n credit: creditLine(\n contributed.map((report) => ({ attribution: `Source: ${report.name}` })),\n ),\n },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["SITE_URL","count","z","z","VOCABULARY","text","text","fraction","bounds","text","text","names","z","z","z","names","z","z","z","z","resolveFactor","z","z"]}
|