Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions openspec/changes/archive/2026-09-29-spanish-copy/archive-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Archive Report: spanish-copy

- **Archived:** 2026-09-29
- **Verdict:** PASS WITH WARNINGS, no blockers (see verify-report.md)
- **Delivered in:** PR #39 (hackathon copy), PR #40 (profile/team/repo commands and team picker), PR #41 (GitHub alerts and final verification)

## Intent

All bot-authored, user-facing Telegram text is neutral Spanish (tuteo). Page-derived analysis values stay verbatim. The LLM prompt, code, identifiers, logs and log codes stay in English.

## Specs merged

- **New main spec:** `openspec/specs/bot-copy/spec.md` (5 requirements).
- **Modified main spec:** `openspec/specs/hackathon-analysis/spec.md`. Its 8 MODIFIED requirement blocks were replaced in place, and the quoted copy is now Spanish. The total stays at 12 requirements, and the delta's "(Previously: …)" annotations were not carried over.

## Implementation

- Two copy catalogs: `src/domain/copy.ts` (domain) and `src/adapters/telegram/copy.ts` (adapter). The adapter catalog imports the domain one, never the reverse.
- Exhaustive typed maps cover roles, fetch-failure kinds, GitHub alert headers and field labels.
- A language guard, `test/copy/catalog-language.test.ts`, fails if either catalog contains common English words.
- Final state: 870 tests passing, typecheck clean, 24/24 tasks checked.

## Follow-ups

- W1–W3 from the verify report are non-blocking.
- Change `hackathon-participation` (roadmap change 4) depended on this change and is now unblocked. Its copy is Spanish from the start.
49 changes: 49 additions & 0 deletions openspec/changes/archive/2026-09-29-spanish-copy/verify-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Verify Report: spanish-copy

**Verdict: PASS WITH WARNINGS** (0 CRITICAL, 3 WARNING, 1 SUGGESTION)

Verified on `main` at c15192b, after PRs #39, #40 and #41 were merged. Mirror of engram `sdd/spanish-copy/verify-report`.

## Evidence

- `npm test`: 71 files, 870/870 passing.
- `npm run typecheck`: clean.
- `tasks.md`: 24/24 checked. That is 10 + 7 + 5 + 2 across Phases 1–4, not the 20 stated in the tasks summary.
- The guard `test/copy/catalog-language.test.ts` covers both catalogs, `src/domain/copy.ts` and `src/adapters/telegram/copy.ts`.
- Exhaustive typed maps exist for roles, fetch-failure kinds, GitHub alert headers, analysis field labels and link/unlink phrases. A new code without copy fails the typecheck.
- The LLM prompt is not touched by this change.

## Coverage

| Requirement | Covered by |
|---|---|
| bot-copy: Spanish-Only Bot-Authored Text | Exact Spanish assertions across `test/adapters/telegram/commands.test.ts`, the domain use-case tests and `test/domain/github.test.ts`, plus the catalog language guard |
| bot-copy: Page-Derived Values Stay Verbatim | Format and use-case tests with page values (not the exact scenario sample, see W1) |
| bot-copy: Code, Prompt and Logs Stay English | Log-code assertions in the use-case and command tests (see W2) |
| bot-copy: Plain Text Within the Telegram Cap | text-limit, `/repos` and GitHub cap tests |
| bot-copy: Technical Codes Are Never Shown Raw | Exhaustive maps (roles, fetch kinds, GitHub headers) with exact-string tests |
| hackathon-analysis (8 MODIFIED requirements) | Exact Spanish strings asserted in the run-hackathon-job, link-analysis-to-topic, request-hackathon-analysis and list-analyses tests, plus the queue and e2e tests |

## Leftover-English grep

A grep of `src/` reply paths for common English reply fragments found hits only in internal places:
- domain exception messages, which are only logged;
- log events and reason codes;
- identifiers;
- comments.

None of these reach chat users. `runCommand` replies only through the Spanish `errorReplies` catalogs, and unrecognized errors are rethrown without text. `.message` is used only for `ConfigError`, inside log fields.

## Warnings

- **W1:** No test asserts the exact spec sample "Submissions close June 1". Verbatim page values are covered by other samples.
- **W2:** No test asserts the `BadArgument` log code. Other English log codes are asserted.
- **W3:** The label "Permanecer anónimo" for Telegram's "Remain anonymous" was assumed. It lives at `src/adapters/telegram/copy.ts:49` and is asserted in `commands.test.ts:237`. The user did not object. Worth checking against the real Spanish Telegram client.

## Suggestion

- The actual task count is 24, not 20. This is noted here for the archive.

## Blocks archive

None.
97 changes: 97 additions & 0 deletions openspec/specs/bot-copy/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Bot Copy Specification

## Purpose

Defines the language and format of all bot-authored Telegram text: neutral, professional Spanish, plain text, within the Telegram length cap, with external values left verbatim.

## Requirements

### Requirement: Spanish-Only Bot-Authored Text

The system MUST write every bot-authored user-facing Telegram text (replies, refusals, usage lines, labels, list markers, empty-list text, callback alerts, job outcomes, link notes, alert headers) in neutral, professional Spanish. No English sentence or label authored by the bot MAY reach Telegram. Loanwords "slug", "PR" and "Issue" are allowed. The Telegram concept "topic" MUST be written "tema". Roles MUST display as "administrador" and "miembro"; stored role values MUST NOT change.

#### Scenario: Refusal is in Spanish

- GIVEN the caller is not a team admin
- WHEN they run `/hackathon <url>`
- THEN the reply is "Solo un administrador del equipo puede analizar o vincular un hackathon."

#### Scenario: Role display

- GIVEN a member has the stored role `admin` and another has `member`
- WHEN a reply shows their roles
- THEN it shows "administrador" and "miembro" respectively, never `admin` or `member`

#### Scenario: Topic wording

- WHEN any reply refers to a Telegram forum topic
- THEN it uses "tema", never "topic"

### Requirement: Page-Derived Values Stay Verbatim

The system MUST NOT translate, rewrite or localize values that originate outside the bot: extracted field values, snippets, hackathon names and any text taken from a web page or from GitHub. Only the bot's own labels and framing around them are Spanish.

#### Scenario: English page keeps its values

- GIVEN an analysis whose extracted `location` value is "Online" and deadline snippet is "Submissions close June 1"
- WHEN the analysis is formatted for Telegram
- THEN the labels are Spanish
- AND "Online" and "Submissions close June 1" appear unchanged

### Requirement: Code, Prompt and Logs Stay English

The system MUST keep the LLM prompt, command names, argument keywords, identifiers, log events, log reason codes and domain exception messages in English. Copy changes MUST NOT alter extraction, validation or any behavior.

#### Scenario: Prompt unchanged

- WHEN a fresh analysis calls the LLM
- THEN the prompt is byte-identical to the pre-change prompt

#### Scenario: Log codes unchanged

- WHEN a command is refused
- THEN the log reason code (for example `BadArgument`) is the same English code as before

### Requirement: Plain Text Within the Telegram Cap

The system MUST send all bot-authored text as plain text (no `parse_mode`) and each message MUST be at most 4096 characters, including the Spanish truncation line.

#### Scenario: Longer Spanish copy still fits

- GIVEN a listing whose full Spanish text exceeds 4096 characters
- WHEN the reply is built
- THEN it is truncated with a "…y N más" line
- AND the reply is at most 4096 characters and sent without `parse_mode`

### Requirement: Technical Codes Are Never Shown Raw

The system MUST map every code interpolated into user-visible text to a Spanish phrase: page-fetch failure kinds, GitHub kind/action, and link/unlink verbs. Raw codes MUST NOT appear.

| Code | Displayed phrase |
|------|------------------|
| `timeout` | tiempo de espera agotado |
| `too-large` | la página es demasiado grande |
| `http-status` | el sitio respondió con un error |
| `content-type` | el contenido no es una página web |
| `redirects` | demasiadas redirecciones |
| `network` | error de red |

#### Scenario: Fetch failure shows a phrase

- GIVEN a fresh analysis fails with fetch kind `timeout`
- WHEN the failure reply is posted
- THEN it reads "No se pudo leer esa página (tiempo de espera agotado). Se conservó el análisis anterior."
- AND it does not contain "timeout"

#### Scenario: GitHub alert header

- GIVEN a pull request `opened` event and an issue `closed` event
- WHEN alerts are posted
- THEN the headers are "PR abierto" and "Issue cerrado"
- AND no raw `pull_request`, `opened` or `closed` code is shown

#### Scenario: Review requested header

- GIVEN a pull request `review_requested` event
- WHEN the alert is posted
- THEN the header is "Revisión solicitada"
33 changes: 20 additions & 13 deletions openspec/specs/hackathon-analysis/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ The system MUST allow only a team admin to run `/hackathon <url>`, in general ch

- GIVEN the caller is a team admin and today's run count is below the cap
- WHEN they run `/hackathon <url>` in the group's general chat
- THEN the system immediately replies "Analyzing <host>…" and returns
- THEN the system immediately replies "Analizando <host>… el resultado se publicará aquí." and returns
- AND the queue consumer later fetches the page, extracts fields, and stores the analysis under a new slug
- AND posts the analysis and its slug as a separate message, unpinned

#### Scenario: Non-admin attempts a fresh analysis

- GIVEN the caller is not a team admin
- WHEN they run `/hackathon <url>`
- THEN the system MUST refuse
- THEN the system MUST refuse with "Solo un administrador del equipo puede analizar o vincular un hackathon."
- AND MUST NOT fetch the page, call the LLM, or count against the cap

### Requirement: Any Member Re-Shows by Slug, Free of Cap
Expand Down Expand Up @@ -56,7 +56,7 @@ The system MUST show the linked topic's cached analysis when `/hackathon` is run

- GIVEN the current topic (or general chat) has no linked analysis
- WHEN a member runs `/hackathon` with no argument
- THEN the system replies with usage instructions
- THEN the system replies "Uso: /hackathon <url o slug>"
- AND does not fetch, extract, or count against the cap

### Requirement: Argument Classified as Slug or URL
Expand Down Expand Up @@ -104,7 +104,7 @@ The system MUST treat a fresh analysis of the same normalized URL, for the same
- GIVEN an analysis already exists under a slug
- WHEN a fresh run for the same URL fails (fetch or extraction failure)
- THEN the system MUST keep the previously stored analysis unchanged
- AND the queue consumer MUST post a clear failure message to the originating chat or topic
- AND the queue consumer MUST post a Spanish failure message to the originating chat or topic, for example "La página tiene muy poco texto legible. Se conservó el análisis anterior."

### Requirement: One Analysis Per Topic, Conflicts Move the Link

Expand All @@ -115,22 +115,23 @@ The system MUST allow at most one linked analysis per topic (nullable `thread_id
- GIVEN the topic has no linked analysis
- WHEN an admin runs `/hackathon <slug>` inside that topic
- THEN the system links and pins the analysis to the topic
- AND a link-only reply reads "Se vinculó <slug> a este tema."

#### Scenario: Topic already holds a different analysis

- GIVEN topic A is linked to analysis `alpha`
- WHEN an admin runs `/hackathon beta` inside topic A
- THEN the system unpins the old pinned message for `alpha`
- AND links and pins `beta` to topic A
- AND the reply states the topic's previous link was replaced
- AND the reply states "Se reemplazó el vínculo anterior del tema (era alpha)."

#### Scenario: Analysis already linked to another topic

- GIVEN analysis `alpha` is linked to topic A
- WHEN an admin runs `/hackathon alpha` inside topic B
- THEN the system unpins the old pinned message in topic A
- AND links and pins `alpha` to topic B
- AND the reply states the analysis moved from topic A to topic B
- AND the reply states "Se movió el vínculo de este análisis desde otro tema."

### Requirement: Pin Failure Falls Back to Unpinned Posting

Expand All @@ -141,7 +142,7 @@ The system MUST still post the analysis when the bot lacks the "can pin messages
- GIVEN the bot does not have "can pin messages" in the chat
- WHEN an admin runs `/hackathon <slug>` inside a topic
- THEN the system posts the analysis unpinned
- AND the reply states that pinning failed
- AND the reply states "No se pudo fijar el mensaje; se publicó sin fijar."

### Requirement: Daily Cap on Fresh Runs

Expand All @@ -151,7 +152,7 @@ The system MUST enforce a per-team daily cap of 5 fetch+LLM runs per UTC day, co

- GIVEN the team has already run 5 fresh analyses in the current UTC day
- WHEN an admin runs `/hackathon <url>` again
- THEN the system MUST refuse with a clear cap-exceeded message
- THEN the system MUST refuse with "Límite diario alcanzado (5 análisis nuevos por día UTC). Volver a mostrar un slug no cuenta."
- AND MUST NOT reserve a slot, fetch the page, or call the LLM

### Requirement: Fresh Analysis Job Safety Under Concurrency and Delivery Faults
Expand All @@ -162,14 +163,14 @@ The system MUST refuse a second fresh analysis request for a team while one is a

- GIVEN a fresh analysis job for the team is already queued or running
- WHEN the same team runs `/hackathon <url>` again
- THEN the system MUST refuse with a clear "already running" reply
- THEN the system MUST refuse with "Ya hay un análisis en curso para este equipo. Espera su resultado."
- AND MUST NOT consume a cap slot

#### Scenario: Enqueue failure

- GIVEN the cap slot and lease were reserved but enqueuing the job fails
- WHEN `/hackathon <url>` is run
- THEN the system MUST reply with a clear "could not start" message
- THEN the system MUST reply "No se pudo iniciar el análisis; inténtalo de nuevo en un minuto. No se contó en el límite diario."
- AND MUST refund the reserved slot so it is not counted against the daily cap

#### Scenario: Duplicate delivery
Expand All @@ -184,9 +185,15 @@ The system MUST refuse a second fresh analysis request for a team while one is a

- GIVEN a job fails with a transient error on every attempt up to the retry limit
- WHEN the final attempt also fails
- THEN the system MUST post a clear failure reply to the originating chat or topic
- THEN the system MUST post "El análisis falló por un error temporal. Inténtalo de nuevo más tarde." to the originating chat or topic
- AND MUST keep any previously stored analysis unchanged

#### Scenario: Fetch failure keeps the prior result

- GIVEN a fresh run fails to read the page with fetch kind `timeout`
- WHEN the consumer posts the failure
- THEN the reply is "No se pudo leer esa página (tiempo de espera agotado). Se conservó el análisis anterior."

### Requirement: Listing Is Read-Only and Truncated

The system MUST let any registered member run `/hackathons` to list slug, name, key deadline, and linked-topic status for all the team's stored analyses, truncated to at most 4096 characters using the same pattern as `/repos`.
Expand All @@ -195,14 +202,14 @@ The system MUST let any registered member run `/hackathons` to list slug, name,

- GIVEN the team has several stored analyses
- WHEN a member runs `/hackathons`
- THEN the reply lists each analysis's slug, name, key deadline, and linked status
- THEN the reply lists each analysis's slug, name, key deadline, and linked status with Spanish labels
- AND the reply is at most 4096 characters

#### Scenario: Listing exceeds the limit

- GIVEN the team has enough stored analyses that the full listing would exceed 4096 characters
- WHEN a member runs `/hackathons`
- THEN the system truncates the reply and appends an "...and N more" note
- THEN the system truncates the reply and appends a "…y N más" line
- AND the reply remains at most 4096 characters

### Requirement: Plain Text Replies
Expand Down
Loading