From fe2d06ce88a3f021b83ce3b7f71724aef3c799ca Mon Sep 17 00:00:00 2001 From: borjaperfra Date: Tue, 15 Sep 2026 11:01:37 +0200 Subject: [PATCH 1/8] fix(docs): las dos rutas de Windows del CLI, con el \n y el \t ya gastados `%LOCALAPPDATA%\Programs\nan` llegaba a la pagina partido en dos por un salto de linea de verdad ("Programs" / "an") y `-InstallDir "C:\tools"` con un tabulador donde iba la barra ("C:ools"). Los dos en ingles y en espanol, desde el #60. Ninguno es un error de sintaxis en ningun sitio: la linea de PowerShell corre perfectamente, solo que instala donde no es. Y una ruta de Windows es justo la cadena que nadie de este equipo vuelve a pegar, asi que sobrevivieron a la revision y a la traduccion. Las dos rutas estan comprobadas contra el install.ps1 que se sirve hoy: $InstallDir = Join-Path $env:LOCALAPPDATA 'Programs\nan', y -InstallDir es un parametro suyo de verdad. Los 1150 tests pasaban con el bug puesto. docsSnippets EJECUTA los snippets, y un snippet que corre es todo lo que pide; estas dos reglas nuevas miran el residuo en vez del resultado: - ningun tabulador literal en las paginas, que es en lo que se queda un \t cuando alguien lo ha interpretado por el camino; - ningun span de codigo inline sin cerrar, que es la marca que deja un \n al convertirse en salto de linea: parte el span y deja las dos lineas con un numero impar de comillas invertidas. Vistas en rojo antes con los cuatro fallos exactos y ni un falso positivo en las otras 20 paginas. Co-Authored-By: Claude Opus 5 (1M context) --- src/content/docs-es/nan-cli.mdx | 5 +-- src/content/docs/nan-cli.mdx | 5 +-- src/tests/lib/docsSnippets.test.ts | 64 ++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/content/docs-es/nan-cli.mdx b/src/content/docs-es/nan-cli.mdx index dd0de9a..5bfcf4a 100644 --- a/src/content/docs-es/nan-cli.mdx +++ b/src/content/docs-es/nan-cli.mdx @@ -81,13 +81,12 @@ En Windows, desde PowerShell: irm https://nan.builders/install.ps1 | iex ``` -Hace el mismo trabajo: resuelve la última release, descarga el `.zip` de tu arquitectura, **verifica su checksum** y deja `nan.exe` en `%LOCALAPPDATA%\Programs -an`, añadiendo esa carpeta a tu `PATH` de usuario si no estaba. Las terminales que ya tuvieras abiertas no lo verán hasta que las reinicies. +Hace el mismo trabajo: resuelve la última release, descarga el `.zip` de tu arquitectura, **verifica su checksum** y deja `nan.exe` en `%LOCALAPPDATA%\Programs\nan`, añadiendo esa carpeta a tu `PATH` de usuario si no estaba. Las terminales que ya tuvieras abiertas no lo verán hasta que las reinicies. Instala para tu usuario y no para toda la máquina, a propósito: el equivalente en Windows del `sudo` es un diálogo de elevación saliendo de un script por tubería, que es peor. Para ponerlo en otro sitio: ```powershell -& ([scriptblock]::Create((irm https://nan.builders/install.ps1))) -InstallDir "C: ools" +& ([scriptblock]::Create((irm https://nan.builders/install.ps1))) -InstallDir "C:\tools" ``` Hay binarios para macOS, Linux y Windows, en Intel y en ARM. diff --git a/src/content/docs/nan-cli.mdx b/src/content/docs/nan-cli.mdx index 0d62b44..e62d4c7 100644 --- a/src/content/docs/nan-cli.mdx +++ b/src/content/docs/nan-cli.mdx @@ -81,13 +81,12 @@ On Windows, from PowerShell: irm https://nan.builders/install.ps1 | iex ``` -It does the same work: resolves the latest release, downloads the `.zip` for your architecture, **verifies its checksum** and puts `nan.exe` in `%LOCALAPPDATA%\Programs -an`, adding that directory to your user `PATH` if it is not there already. Terminals you already had open will not see it until you restart them. +It does the same work: resolves the latest release, downloads the `.zip` for your architecture, **verifies its checksum** and puts `nan.exe` in `%LOCALAPPDATA%\Programs\nan`, adding that directory to your user `PATH` if it is not there already. Terminals you already had open will not see it until you restart them. It installs for your user and not for the whole machine, deliberately: the Windows equivalent of the `sudo` prompt is an elevation dialog out of a piped script, which is worse. To put it elsewhere: ```powershell -& ([scriptblock]::Create((irm https://nan.builders/install.ps1))) -InstallDir "C: ools" +& ([scriptblock]::Create((irm https://nan.builders/install.ps1))) -InstallDir "C:\tools" ``` There are binaries for macOS, Linux and Windows, on Intel and on ARM. diff --git a/src/tests/lib/docsSnippets.test.ts b/src/tests/lib/docsSnippets.test.ts index 988a221..072b28d 100644 --- a/src/tests/lib/docsSnippets.test.ts +++ b/src/tests/lib/docsSnippets.test.ts @@ -269,3 +269,67 @@ describe('every package a snippet imports is a package the page installs', () => ).toEqual([]); }); }); + +/** + * The two shapes a collapsed escape leaves behind. + * + * `%LOCALAPPDATA%\Programs\nan` and `-InstallDir "C:\tools"` were published on + * /docs/nan-cli with their `\n` and their `\t` already spent: the first arrived + * as a real line break mid-path ("Programs" / "an"), the second as a tab + * ("C:ools"). Both survived review in two languages because a Windows path + * is the one string nobody on this team pastes back, and neither is a syntax + * error anywhere - the PowerShell line runs perfectly, it just installs + * somewhere else. + * + * Nothing above caught them: those rules run the snippets, and a snippet that + * runs is all they ask for. These two look at the residue instead. + */ +describe('escapes that were spent before they reached the page', () => { + /** + * A tab in a Markdown source is either a collapsed `\t` or an indentation + * nobody can see. Neither is worth keeping, so the rule is the whole file + * rather than the paths: it costs nothing and it does not need to guess which + * strings are Windows. + */ + test.each(ALL_PAGES.map((p) => p.id))('%s carries no literal tab', (id) => { + const page = ALL_PAGES.find((p) => p.id === id)!; + const hits = page.text + .split('\n') + .map((line, i) => ({ line, n: i + 1 })) + .filter(({ line }) => line.includes('\t')) + .map(({ line, n }) => `${n}: ${line.replace(/\t/g, '').trim()}`); + expect( + hits, + `${id}: a literal tab, which is what a \t looks like once something has ` + + `interpreted it:\n${hits.join('\n')}`, + ).toEqual([]); + }); + + /** + * A `\n` inside inline code leaves a sharper mark than a stray line break: it + * splits the span across two lines, so each of them carries an odd number of + * backticks. Markdown does allow a span to wrap, but no guide here writes one + * that way, so the odd count is the collapsed escape every time. + * + * Fenced blocks are skipped - their backticks are the fence. + */ + test.each(ALL_PAGES.map((p) => p.id))('%s closes every inline code span', (id) => { + const page = ALL_PAGES.find((p) => p.id === id)!; + let inFence = false; + const hits: string[] = []; + page.text.split('\n').forEach((line, i) => { + if (/^\s*```/.test(line)) { + inFence = !inFence; + return; + } + if (inFence) return; + const ticks = (line.match(/`/g) ?? []).length; + if (ticks % 2 === 1) hits.push(`${i + 1}: ${line.trim()}`); + }); + expect( + hits, + `${id}: an inline code span opens on one line and closes on another, ` + + `which is what a \n looks like once it has become a line break:\n${hits.join('\n')}`, + ).toEqual([]); + }); +}); From 29580de2ab320a10f4ed79264a393d7c0820a7b7 Mon Sep 17 00:00:00 2001 From: borjaperfra Date: Tue, 15 Sep 2026 11:03:03 +0200 Subject: [PATCH 2/8] fix(docs): el CLI dejaba de decir dos veces que en Windows hay que compilar La pagina se trajo el instalador de PowerShell en el #60 y se quedo con todo lo que estaba escrito de cuando Windows no tenia binario. Resultado: ofrece `irm https://nan.builders/install.ps1 | iex` arriba, dice "hay binarios para macOS, Linux y Windows" un poco mas abajo, y dos pantallas despues manda a instalar Go. - "Es la via para Windows" en Compilar desde el codigo. La seccion se queda, que es la respuesta buena para quien va a tocar el CLI; lo que no puede ser es la via para una plataforma que ya publicamos. - El known issue entero, "En Windows hay que compilar. Las versiones publicadas traen binarios de macOS y Linux". Lo zanja la release: v0.1.19 publica windows_amd64.zip y windows_arm64.zip. Y de paso el known issue que si sigue en pie ofrecia solo el `.tar.gz` para bajarlo a mano, que en Windows tampoco existe. El test no persigue estas cuatro frases sino la forma del fallo: una pagina que instala una plataforma desde un binario publicado y encima le dice que compile. La siguiente plataforma va a llegar igual, anadiendo el texto nuevo y dejando el viejo. Co-Authored-By: Claude Opus 5 (1M context) --- src/content/docs-es/nan-cli.mdx | 5 ++-- src/content/docs/nan-cli.mdx | 5 ++-- src/tests/lib/docsCliTools.test.ts | 43 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/content/docs-es/nan-cli.mdx b/src/content/docs-es/nan-cli.mdx index 5bfcf4a..6ee2a6f 100644 --- a/src/content/docs-es/nan-cli.mdx +++ b/src/content/docs-es/nan-cli.mdx @@ -129,7 +129,7 @@ Dentro de OpenCode, `/models` te enseña los modelos de NaN ya disponibles.
-Es la vía para Windows, y la que quieres si vas a tocar el CLI. Hace falta [Go](https://go.dev) 1.26 o superior: +Es la que quieres si vas a tocar el CLI. Hace falta [Go](https://go.dev) 1.26 o superior: ```bash git clone https://github.com/helmcode/nan-cli @@ -182,7 +182,6 @@ El resultado es el mismo. Lo único que te pierdes son las pestañas de consumo
-- **En Windows hay que compilar.** Las versiones publicadas traen binarios de macOS y Linux; para Windows, de momento, la vía es `go build`. -- **El instalador pide la lista de versiones a GitHub.** Si estás detrás de un proxy que bloquea `api.github.com`, no encontrará nada que descargar. En ese caso, baja el `.tar.gz` de tu sistema a mano desde la [página de releases](https://github.com/helmcode/nan-cli/releases). +- **El instalador pide la lista de versiones a GitHub.** Si estás detrás de un proxy que bloquea `api.github.com`, no encontrará nada que descargar. En ese caso, baja a mano el archivo de tu sistema desde la [página de releases](https://github.com/helmcode/nan-cli/releases).
diff --git a/src/content/docs/nan-cli.mdx b/src/content/docs/nan-cli.mdx index e62d4c7..61c0f16 100644 --- a/src/content/docs/nan-cli.mdx +++ b/src/content/docs/nan-cli.mdx @@ -129,7 +129,7 @@ Inside OpenCode, `/models` shows you the NaN models, already available.
-This is the path on Windows, and the one you want if you are going to work on the CLI itself. You need [Go](https://go.dev) 1.26 or newer: +This is the one you want if you are going to work on the CLI itself. You need [Go](https://go.dev) 1.26 or newer: ```bash git clone https://github.com/helmcode/nan-cli @@ -182,7 +182,6 @@ The result is the same. The only thing you miss out on are the usage and cost ta
-- **On Windows you have to build.** The published releases carry macOS and Linux binaries; for Windows, the path for now is `go build`. -- **The installer asks GitHub for the list of releases.** If you are behind a proxy that blocks `api.github.com`, it will find nothing to download. In that case, grab the `.tar.gz` for your system by hand from the [releases page](https://github.com/helmcode/nan-cli/releases). +- **The installer asks GitHub for the list of releases.** If you are behind a proxy that blocks `api.github.com`, it will find nothing to download. In that case, grab the archive for your system by hand from the [releases page](https://github.com/helmcode/nan-cli/releases).
diff --git a/src/tests/lib/docsCliTools.test.ts b/src/tests/lib/docsCliTools.test.ts index 14ca2d8..8a680bb 100644 --- a/src/tests/lib/docsCliTools.test.ts +++ b/src/tests/lib/docsCliTools.test.ts @@ -95,3 +95,46 @@ describe.each(['docs', 'docs-es'] as const)('the Pi page in %s', (locale) => { expect(text, 'nothing for Windows').toContain('install.ps1'); }); }); + +/** + * Windows, told once. + * + * The page acquired its PowerShell installer in #60 and kept everything that + * was written when Windows had no binary: "This is the path on Windows" in + * Building from source, and a known issue saying the published releases carry + * macOS and Linux only. So the same page offered a one-liner at the top and + * told you to install Go two screens down, in both languages. + * + * Both halves cannot be true at once, and the release settles it: v0.1.19 + * publishes windows_amd64.zip and windows_arm64.zip. What this pins is the + * shape of the mistake - a page that ships an installer for a platform and + * also tells that platform to compile - because the next platform added will + * arrive the same way, by adding the new text and leaving the old. + */ +describe.each(['docs', 'docs-es'] as const)('the Windows story on the CLI page in %s', (locale) => { + const body = read(locale, 'nan-cli'); + + test('publishes the PowerShell installer', () => { + expect(body).toContain('install.ps1'); + }); + + /** + * Building from source stays on the page - it is the right answer for + * working on the CLI itself. What it may not be is the answer for a + * platform we publish a binary for. + */ + test('never sends Windows off to compile', () => { + const WINDOWS = /windows/i; + const BUILD = /go build|go\.dev|compilar|compile|build from source|desde el c[óo]digo/i; + const guilty = body + .split('\n') + .map((line, i) => ({ line: line.trim(), n: i + 1 })) + .filter(({ line }) => WINDOWS.test(line) && BUILD.test(line)) + .map(({ line, n }) => `${n}: ${line}`); + expect( + guilty, + `${locale}/nan-cli: the page installs Windows from a published binary and ` + + `these lines still tell it to build:\n${guilty.join('\n')}`, + ).toEqual([]); + }); +}); From 33a51d0bdfa6857d23155ac09a831cafa5cc3f79 Mon Sep 17 00:00:00 2001 From: borjaperfra Date: Tue, 15 Sep 2026 11:03:54 +0200 Subject: [PATCH 3/8] fix(docs): /rerank deja de venderse como compatible con OpenAI en el cuerpo Cerraba con "el endpoint es compatible con OpenAI tanto en la autenticacion como en el formato del cuerpo". La mitad es verdad y util -la cabecera Bearer es la misma-, y la otra mitad manda al lector a buscar una especificacion que no existe: OpenAI no define ningun /rerank, asi que no hay cuerpo con el que ser compatible. El cuerpo es nuestro. La pagina ya lo sabia. Tres lineas mas arriba, en el comentario del propio snippet, dice que el endpoint "is not part of the standard OpenAI client". Y /search, el otro endpoint nuestro del mismo fichero, ya cerraba bien: "manda el cuerpo JSON con tu key en el Bearer". Esto es igualar rerank a como ya estaba escrito search. El test es la pagina contra si misma: lo que ella marca como fuera del cliente estandar no puede venderse dos parrafos despues como compatible en su payload. Co-Authored-By: Claude Opus 5 (1M context) --- src/content/docs-es/examples.md | 2 +- src/content/docs/examples.md | 2 +- src/tests/lib/docsSnippets.test.ts | 36 ++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/content/docs-es/examples.md b/src/content/docs-es/examples.md index 5a06d90..f5c772e 100644 --- a/src/content/docs-es/examples.md +++ b/src/content/docs-es/examples.md @@ -187,7 +187,7 @@ for r in response["results"]: print(f"{r['index']}: {r['relevance_score']:.3f}") ``` -También funciona con `requests` a pelo o con cualquier cliente HTTP: el endpoint es compatible con OpenAI tanto en la autenticación como en el formato del cuerpo. +También funciona con `requests` a pelo o con cualquier cliente HTTP: manda el cuerpo JSON con tu key en el Bearer. `/rerank` es un endpoint nuestro, no está en la especificación de OpenAI: la autenticación es la misma, el cuerpo es nuestro. ## model: kokoro diff --git a/src/content/docs/examples.md b/src/content/docs/examples.md index 776ec1c..755a761 100644 --- a/src/content/docs/examples.md +++ b/src/content/docs/examples.md @@ -187,7 +187,7 @@ for r in response["results"]: print(f"{r['index']}: {r['relevance_score']:.3f}") ``` -Also works with raw `requests` or any HTTP client — the endpoint is OpenAI-compatible in authentication and payload format. +Also works with raw `requests` or any HTTP client — send the JSON body with your Bearer key. `/rerank` is an endpoint of ours, not part of the OpenAI specification: the authentication is the same one, the body is ours. ## model: kokoro diff --git a/src/tests/lib/docsSnippets.test.ts b/src/tests/lib/docsSnippets.test.ts index 072b28d..4d1422a 100644 --- a/src/tests/lib/docsSnippets.test.ts +++ b/src/tests/lib/docsSnippets.test.ts @@ -333,3 +333,39 @@ describe('escapes that were spent before they reached the page', () => { ).toEqual([]); }); }); + +/** + * "OpenAI-compatible" said about something OpenAI does not define. + * + * `/rerank` closed with "the endpoint is OpenAI-compatible in authentication + * and payload format". Half of that is true and useful - the Bearer header is + * the same one - and half of it promises a spec to go and read that does not + * exist: OpenAI has no `/rerank`, so there is no payload to be compatible + * with. The body is ours. + * + * The page already knew: three lines above, in the snippet's own comment, it + * says the endpoint "is not part of the standard OpenAI client". And `/search`, + * the other endpoint of ours in the same file, already closes the right way - + * "send the JSON body with your Bearer key". So the rule is the page against + * itself: whatever it flags as outside the standard client cannot be sold two + * paragraphs later as compatible in its payload. + */ +describe('endpoints that are ours are not described as OpenAI-compatible', () => { + const OUTSIDE = /not part of the standard OpenAI client/i; + /** Authentication genuinely is compatible; it is the body that is not. */ + const PAYLOAD_CLAIM = /compatible[^.]*\b(payload|body|cuerpo|formato del cuerpo)\b/i; + + test.each(['docs', 'docs-es'] as const)('%s/examples.md', (locale) => { + const text = normalize(readFileSync(resolve(CONTENT, locale, 'examples.md'), 'utf-8')); + const sections = text.split(/\n(?=## )/); + const guilty = sections + .filter((s) => OUTSIDE.test(s) && PAYLOAD_CLAIM.test(s)) + .map((s) => `${s.split('\n')[0]}: ${PAYLOAD_CLAIM.exec(s)![0]}`); + expect( + guilty, + `${locale}/examples.md: the page says these endpoints are outside the ` + + `standard OpenAI client and then calls their payload compatible with ` + + `it:\n${guilty.join('\n')}`, + ).toEqual([]); + }); +}); From 1cf52fb7629acc4486fbf5b3f4c50a60511b9a10 Mon Sep 17 00:00:00 2001 From: borjaperfra Date: Tue, 15 Sep 2026 11:08:41 +0200 Subject: [PATCH 4/8] fix(docs): el chrome de /es/docs deja de estar en ingles Bajo prosa en espanol, el boton decia "Copy", el aviso "Copied to clipboard", el buscador vacio "No results" y los aria-label "Search documentation" y "Documentation". Tiene una causa concreta y esta en el historial: docsCopyIsEnglish.test existe porque paso lo contrario, el espanol se habia colado en las paginas inglesas -"Copiar", "!Copiado al portapapeles!"- y se arreglo escribiendo esas cadenas en ingles. Correcto para /docs, y deja /es/docs justo asi. El layout ya tenia la tabla que lo resuelve, `T`, con su rama `en` y su rama `es`; estas ocho nunca entraron porque viven en los scripts de cliente, no en el markup. El de busqueda ya iba con define:vars, asi que se le pasa `T` y ya esta. El de los bloques de codigo esta empaquetado y tipado, asi que no puede llevar define:vars sin perder el TypeScript: lee sus etiquetas del dataset del propio toast, que este layout pinta siempre. Sin fallback en ingles, a proposito: un fallback es exactamente como las paginas espanolas acabaron diciendo "Copied to clipboard". El precio es que un data-* que falte deja el boton mudo en vez de en otro idioma, que tambien es silencioso, asi que hay test de que el toast lleva los cinco atributos y de que algo lee los cinco. La regla del test no es "en que idioma esta esto" sino "sale de la tabla": un literal que el lector ve, fuera de `T`, tiene un solo idioma sea el que sea. Verificado renderizado en las dos rutas: /docs sirve "Copied to clipboard" y /es/docs "Copiado al portapapeles", y de paso las dos rutas de Windows salen enteras. Co-Authored-By: Claude Opus 5 (1M context) --- src/layouts/Docs.astro | 52 +++++++++++++----- src/tests/layouts/DocsShell.test.ts | 83 +++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 12 deletions(-) diff --git a/src/layouts/Docs.astro b/src/layouts/Docs.astro index 73576df..528678b 100644 --- a/src/layouts/Docs.astro +++ b/src/layouts/Docs.astro @@ -46,6 +46,14 @@ const T = { next: 'Next →', docs: 'Docs', close: 'Close menu', + searchAria: 'Search documentation', + sidebarAria: 'Documentation', + noResults: 'No results', + copy: 'Copy', + copied: 'Copied', + copyAria: 'Copy', + copyToast: 'Copied to clipboard', + copyFailed: 'Copy failed', }, es: { onThisPage: 'En esta página', @@ -55,6 +63,14 @@ const T = { next: 'Siguiente →', docs: 'Docs', close: 'Cerrar menú', + searchAria: 'Buscar en la documentación', + sidebarAria: 'Documentación', + noResults: 'Sin resultados', + copy: 'Copiar', + copied: 'Copiado', + copyAria: 'Copiar', + copyToast: 'Copiado al portapapeles', + copyFailed: 'No se pudo copiar', }, }[lang]; @@ -206,7 +222,7 @@ const searchData = [
-
-
Copied to clipboard
- -