Skip to content

Commit 7cab7a3

Browse files
hhkaosclaude
andcommitted
Bootstrap monorepo and add @opentechevents/validate
Set up pnpm workspaces with TypeScript (Node 22), vitest, eslint, and a CI workflow (lint + build + test). Add the first package, @opentechevents/validate: - validateEvent(json) / validateFeed(json): pure functions returning {valid, errors} with human-readable messages (ajv 2020-12 composition noise collapsed into single actionable errors) - ote-validate <dir|file> CLI with feed/event auto-detection - OTE v0.2 schemas vendored from opentechevents-spec (commit bbddeb9, provenance annotated), never fetched at runtime - valid/invalid fixtures copied from the spec examples, 33 tests English is the repo's official language (documented in CLAUDE.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0 parents  commit 7cab7a3

35 files changed

Lines changed: 3536 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
ci:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: pnpm/action-setup@v4
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: 22
17+
cache: pnpm
18+
- run: pnpm install --frozen-lockfile
19+
- run: pnpm lint
20+
- run: pnpm build
21+
- run: pnpm test

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
dist/
3+
coverage/
4+
*.tsbuildinfo
5+
.DS_Store

CLAUDE.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# ote-tools
2+
3+
Central monorepo for the OTE organizer kit. Read DESIGN.md before any task.
4+
5+
- **Language: English is the official language of the repo** — all documentation,
6+
code comments, test names, commit messages, error messages and identifiers.
7+
Localized versions of docs/UI may be offered later as translations, but English
8+
is canonical. This holds even when prompts, DESIGN.md or other inputs are in
9+
Spanish.
10+
- Contains: npm packages (@opentechevents/*, pure functions, no UI), reusable
11+
GitHub Actions workflows, and (phase 2) the static web dashboard/editor.
12+
- Does NOT contain: event data (lives in the ote-template forks) or the spec
13+
(lives in OpenTechEvents/opentechevents-spec).
14+
- Stack: TypeScript + Node 22, pnpm workspaces, vitest, ajv for validation.
15+
- OTE schema: v0.2, vendored in packages/validate/schemas/ (copied from the spec,
16+
version annotated). Never fetch the schema at runtime.
17+
- Tests: `pnpm test` at the root. Every new package ships fixtures and tests.
18+
- Convention: connectors never invent data; absent field = absent + warning.

DESIGN.md

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# Kit del organizador — repositorio plantilla (propuesta)
2+
3+
> ⚠️ **Propuesta, no compromiso.** Diseño para discutir. Nada está implementado todavía.
4+
5+
Un **repositorio que el organizador forkea** y que le da, funcionando solo con GitHub y GitHub Pages: su feed OTE publicado, una interfaz para crear/editar/borrar eventos sin escribir JSON a mano, exportación automática a ICS/RSS, e utilidades para difundir cada evento en otras plataformas.
6+
7+
**Para quién**: organizadores técnicos (comunidades GitHub-nativas). El kit es **opcional** — la historia mínima de adopción sigue siendo *"un JSON estático en cualquier URL"*. Esto no es *la* forma de adoptar OTE; es una forma de bajar la barrera y añadir ventajas (UI, exports, difusión) para quien ya vive en GitHub.
8+
9+
**Garantía anti *lock-in***: los datos son del organizador, viven en su repo en formato abierto, y el kit exporta a ICS/RSS en cada build. Si mañana quiere volver a Google Calendar o a cualquier otra herramienta, se lleva sus datos sin fricción.
10+
11+
---
12+
13+
## Dónde vive
14+
15+
Tres repositorios, tres responsabilidades:
16+
17+
| Repo | Responsabilidad | Quién lo toca |
18+
| --- | --- | --- |
19+
| `opentechevents-spec` | El estándar + la página de registro de comunidades adoptantes. | Mantenedores de la spec. |
20+
| `ote-template` | **La plantilla que forkea el organizador**: datos + configuración + workflows finos. | El organizador (su fork). |
21+
| `ote-tools` | Monorepo central: dashboard/editor web, conectores (npm), actions reutilizables, herramienta de migración. | Mantenedores del ecosistema. |
22+
23+
## Decisión clave: fork delgado, código central
24+
25+
La tensión estructural del diseño: el organizador necesita **su copia** (con sus datos) *y* recibir **mejoras** (UI, validación, exportadores, schema nuevo). Esas dos cosas pelean:
26+
27+
- Si la plantilla contiene la UI y la lógica, el día que salga la v2 habrá N forks congelados en v1 que nadie migrará. Un ecosistema de copias rotas daña la marca ("probé OTE y estaba desactualizado").
28+
- Si todo es central, el organizador no es dueño de nada.
29+
30+
Resolución: **en el fork solo vive lo que es del organizador** — sus datos y su configuración. Todo lo que evoluciona vive en `ote-tools` y el fork lo consume versionado:
31+
32+
```
33+
ote-template (fork del organizador)
34+
├── events/*.json ← SUS datos
35+
├── ote.config.json ← SU configuración
36+
├── docs/index.html ← dashboard estático mínimo (enlaces, casi nunca cambia)
37+
└── .github/workflows/ ← workflows de ~5 líneas que llaman a
38+
reusable workflows de ote-tools@v1:
39+
validar · exportar ICS/RSS · build Pages · issue→PR
40+
```
41+
42+
Las mejoras llegan solas (tag flotante `@v1`; *breaking changes* = `@v2`, opt-in). La migración de schema no es código duplicado en N forks: es **una herramienta aparte** en `ote-tools` (`ote migrate`), ejecutable como action o desde el dashboard.
43+
44+
Se elige **fork** (no *template repo* de GitHub): el fork conserva el vínculo con el origen y permite `git pull upstream` para lo poco que sí vive en la plantilla (el dashboard estático, los workflows finos). Con *template repo* ese canal no existe.
45+
46+
## El dashboard: contexto por URL
47+
48+
El Pages del fork sirve una página estática mínima que **enlaza a las herramientas centrales pasando el repo como contexto**:
49+
50+
```
51+
https://tools.opentechevents.org/editor?repo=usuario/mi-comunidad
52+
https://tools.opentechevents.org/import?repo=usuario/mi-comunidad
53+
https://tools.opentechevents.org/publish?repo=usuario/mi-comunidad
54+
```
55+
56+
Cada herramienta lee `?repo=`, hace `fetch` del feed y del `ote.config.json` vía `raw.githubusercontent.com` (CORS abierto) y renderiza. El fork sigue siendo datos + config + una página tonta; las herramientas evolucionan sin tocar ningún fork, y siempre saben qué repositorio están "editando".
57+
58+
## Flujo de escritura: formulario → issue → PR (uniforme)
59+
60+
GitHub Pages puro no puede guardar secretos: OAuth necesita un backend para el intercambio de token. En lugar de montar infraestructura, **el mismo pipeline sirve para el dueño y para la comunidad**:
61+
62+
```
63+
Editor central (formulario) ── genera el JSON del evento
64+
65+
├─ botón "Proponer cambio" ──▶ abre issue prefillado en el fork
66+
│ (JSON en bloque de código, vía URL params;
67+
│ si excede ~8K chars de URL → fallback "copia y pega")
68+
│ │
69+
│ ▼
70+
│ workflow del fork (on: issues.opened)
71+
│ ├─ parsea el JSON del cuerpo
72+
│ ├─ valida contra el JSON Schema
73+
│ │ └─ inválido → comenta qué falta, no abre PR
74+
│ └─ abre PR enlazando el issue
75+
│ └─ el dueño revisa y mergea
76+
77+
└─ botón "Editar directo" ──▶ github.dev con el fichero (solo dueño, con push)
78+
```
79+
80+
Por qué así y no de otra forma:
81+
82+
- **El dueño mergea su propio PR en segundos**; un tercero (un ponente, alguien de la comunidad que detecta un error) espera aprobación. Un solo código, revisión humana garantizada por diseño, cero auth, cero backend.
83+
- **No se usan GitHub Issue Forms como formulario de eventos**: son demasiado rígidos (sin campos condicionales, sin arrays — varios ponentes, varias sesiones). El formulario real es la página del editor; el issue es solo el transporte.
84+
- Coste asumido: la latencia (action + PR + rebuild de Pages) se mide en **minutos, no segundos**. Aceptable para el caso de uso; documentarlo para ajustar expectativas.
85+
86+
## Configuración: `ote.config.json`
87+
88+
Todo lo que hoy es "depende del organizador" vive en un único fichero en su fork, que las herramientas centrales leen:
89+
90+
```jsonc
91+
{
92+
"feed": {
93+
"title": "Eventos de Mi Comunidad",
94+
"description": "",
95+
"license": "CC-BY-4.0"
96+
},
97+
98+
// Qué campos muestra el editor. Presets: "meetup" | "conference" | "all".
99+
// "all" = formulario completo con secciones colapsables ("Avanzado: CFP, patrocinios…").
100+
"profile": "meetup",
101+
// Opcional: perfil personalizado (gana sobre profile)
102+
"customProfile": { "fields": ["cfp", "sponsors"] },
103+
104+
// Plataformas donde el organizador difunde (ver Difusión)
105+
"publish": {
106+
"meetup": { "groupUrl": "https://meetup.com/mi-grupo" },
107+
"eventoswiki": { "enabled": true }
108+
},
109+
110+
// Vinculación con directorios de comunidades (ver Registro)
111+
"linking": { "communityId": "combuilders:mi-comunidad" }
112+
}
113+
```
114+
115+
Racional de los presets: un organizador de meetups no necesita ver campos de CFP; uno de conferencias sí. Una matriz de configuración campo a campo sería sobre-ingeniería para el 90% de los casos — presets primero, perfil custom como escotilla de escape.
116+
117+
## Importar desde fuentes existentes
118+
119+
Objetivo: que quien ya tiene sus eventos en Meetup, un `.ics` de Google Calendar, etc., no re-teclee todo.
120+
121+
| Fuente | Cómo | Limitación honesta |
122+
| --- | --- | --- |
123+
| **iCalendar (`.ics`)** | Subir/URL del fichero → seleccionar qué eventos importar (p. ej. solo futuros) → completar a mano lo que falte. | La conversión pierde los metadatos de descubrimiento que ICS no modela (ver [nota en el sitio](../docs/README.md)); el import lo señala campo a campo, no lo disimula. |
124+
| **JSON-LD / schema.org** (Meetup, Eventbrite, Luma…) | Pegar la URL del evento → extraer el `schema.org/Event` que la página ya expone. | ⚠️ **CORS**: el navegador no puede hacer `fetch` de meetup.com desde la herramienta. Fallback: *"pega el HTML de la página"* (textarea) → se parsea el JSON-LD del texto. Feo pero funciona siempre, cero infraestructura. La vía buena a futuro es la [extensión de navegador](browser-extension.md), que sí lee el DOM. |
125+
| **API de Meetup** || **Descartada**: hoy requiere plan Pro de pago + aprobación OAuth. No se puede contar con ella para el caso común. |
126+
127+
**Nunca se importa en silencio**: el organizador ve la lista, selecciona, revisa y completa. Mismo principio que el [agregador](aggregator.md): un conector no inventa datos.
128+
129+
### Los conectores son paquetes npm, funciones puras, sin UI
130+
131+
`@opentechevents/import-ics`, `@opentechevents/import-jsonld`, `@opentechevents/export-rss`… — entrada → documento(s) OTE (o el inverso). Toda la UI (selección, completado) vive en el dashboard de `ote-tools`, que los importa. Conectores con interfaz propia serían N UIs que mantener con estilos divergentes.
132+
133+
> Relación con el [agregador](aggregator.md): allí se decidió que sus conectores viven **dentro** de `opentechevents-data` (por seguridad de la Action diaria y cambios atómicos), con la puerta abierta a extraerlos como *"paquete publicado y fijado a una versión"* cuando madure. Ese paquete publicado es exactamente esto. La convergencia natural: los conectores compartidos acaban en npm y ambos proyectos los consumen fijados a versión — nunca un `git clone` de HEAD.
134+
135+
## Difusión: publicar el evento en otras plataformas
136+
137+
Dos niveles, deliberadamente separados por coste:
138+
139+
### 1. *Cheat sheet* copy-paste (barato, pronto)
140+
141+
Registrado el evento, la herramienta `publish` genera, por cada plataforma configurada en `ote.config.json`:
142+
143+
- **Enlace directo** a la interfaz de alta de la plataforma (p. ej., configurada la URL del grupo de Meetup, el enlace a "crear evento" de ese grupo).
144+
- **Los datos ya formateados** como esa plataforma los pide, listos para copiar campo a campo.
145+
- Para directorios GitHub que aceptan issues/PRs (confs.tech, developers.events, EventosWiki…): **issue prefillado** en el repo de destino.
146+
147+
Cero APIs, cero secretos, cero mantenimiento de integraciones. Es la versión manual-asistida de la auto-publicación, y cubre precisamente las plataformas **sin** API abierta gratuita (Meetup, LinkedIn…).
148+
149+
### 2. Auto-publicación vía API (caro, después, pluggable)
150+
151+
Solo para plataformas con API abierta que lo permita — p. ej. **eventos.wiki**, que se ha ofrecido a crear una (y habrá más). Diseño:
152+
153+
- Publicador = conector npm más (`@opentechevents/publish-eventoswiki`).
154+
- Credenciales en **repo secrets** del fork; ejecución vía `workflow_dispatch` desde el dashboard o manualmente.
155+
- Cada plataforma se activa en `ote.config.json`.
156+
157+
Lo que **no** se va a hacer: perseguir APIs de pago o con programas de *partner* restringidos (Meetup Pro, LinkedIn). Para esas, el nivel 1 es la respuesta.
158+
159+
## Registro: "mi comunidad usa OTE"
160+
161+
La documentación del kit guía al organizador, tras publicar su feed, a:
162+
163+
1. Añadir el **meta tag de descubrimiento** en la web de su comunidad/conferencia (si la tiene).
164+
2. **Registrarse como adoptante** en `opentechevents-spec`, para difusión y para que directorios y usuarios encuentren el feed.
165+
166+
Para el paso 2, una página en el sitio de la spec ([docs/](../docs/)): formulario con nombre, web y URL del feed OTE → **issue prefillado** en `opentechevents-spec` vía URL params. Con **vinculación pluggable** a directorios de comunidades:
167+
168+
- El campo de nombre autocompleta contra fuentes registradas. La primera: el [directorio de Community Builders](https://github.com/ComBuildersES/communities-directory) (`fetch` de su `communities.json` vía raw.githubusercontent, CORS abierto).
169+
- Si la comunidad existe en la fuente → el issue incluye su ID, y el registro queda **vinculado** a los datos del directorio.
170+
- Si no existe → se envía igualmente, sin vínculo.
171+
172+
Pluggable y no acoplado a ComBuildersES: la spec tiene vocación internacional; el directorio hispano es la primera fuente de vinculación, no la definición de qué es una comunidad.
173+
174+
## Fases
175+
176+
| Fase | Alcance | Resultado |
177+
| --- | --- | --- |
178+
| **1 (MVP)** | Fork delgado (feed ejemplo + `ote.config.json` + workflows finos: validar, exportar ICS/RSS, Pages) + dashboard estático + docs de adopción + página de registro en la spec. | **Forkear → configurar 3 valores → feed OTE + ICS + RSS publicados.** Esto ya es el producto. |
179+
| **2** | Editor central (presets + colapsables + perfil custom) con flujo issue→PR. Import ICS. | Crear/editar eventos sin escribir JSON. La comunidad puede proponer cambios. |
180+
| **3** | *Cheat sheets* copy-paste + issues prefillados para directorios GitHub. Import JSON-LD (con fallback pega-HTML). | Difundir un evento deja de ser re-tecleo. |
181+
| **4** | Publicadores API pluggables (eventos.wiki primero). Herramienta de migración de schema. Extensión de navegador para captura sin CORS. | Automatización. |
182+
183+
La fase 1 es deliberadamente pequeña: **un fork que en minutos publica un feed válido con exports ICS/RSS ya es un producto útil**, aunque los eventos se editen a mano. Todo lo demás es amplificación de eso.
184+
185+
## Riesgos asumidos y descartes
186+
187+
- **Latencia de escritura en minutos** (action + PR + rebuild Pages). Aceptado a cambio de cero backend.
188+
- **GitHub como dependencia dura** del kit. Aceptado: el público objetivo ya vive ahí, y los *datos* no dependen de GitHub — son JSON portable.
189+
- **Issue Forms como formulario de eventos**: descartado (rígidos). Solo transporte.
190+
- **Proxy CORS para leer plataformas**: descartado (backend, mantenimiento, abuso).
191+
- **API de Meetup**: descartada (de pago).
192+
- **Auto-publicación en plataformas sin API abierta**: descartada; su respuesta es el *cheat sheet*.

eslint.config.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import eslint from "@eslint/js";
2+
import tseslint from "typescript-eslint";
3+
4+
export default tseslint.config(
5+
{
6+
ignores: ["**/dist/**", "**/node_modules/**", "**/fixtures/**"],
7+
},
8+
eslint.configs.recommended,
9+
...tseslint.configs.recommended,
10+
{
11+
files: ["**/*.ts"],
12+
rules: {
13+
"@typescript-eslint/no-unused-vars": [
14+
"error",
15+
{ argsIgnorePattern: "^_" },
16+
],
17+
},
18+
},
19+
);

package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "ote-tools",
3+
"private": true,
4+
"type": "module",
5+
"packageManager": "pnpm@11.13.1",
6+
"engines": {
7+
"node": ">=22"
8+
},
9+
"scripts": {
10+
"build": "pnpm -r build",
11+
"lint": "eslint .",
12+
"test": "pnpm -r test",
13+
"typecheck": "pnpm -r typecheck"
14+
},
15+
"devDependencies": {
16+
"@eslint/js": "^9.0.0",
17+
"eslint": "^9.0.0",
18+
"typescript": "^5.5.0",
19+
"typescript-eslint": "^8.0.0"
20+
}
21+
}

packages/validate/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# @opentechevents/validate
2+
3+
Validates OTE **Event** and **Feed** documents against JSON Schema **v0.2**
4+
(vendored in [schemas/](schemas/), no runtime fetch).
5+
6+
## API
7+
8+
```ts
9+
import { validateEvent, validateFeed } from "@opentechevents/validate";
10+
11+
const { valid, errors } = validateEvent(json);
12+
// errors: [{ path: "location.geo.lat", message: "must be <= 90" }, …]
13+
```
14+
15+
Pure functions: they take an already-parsed document and touch neither disk nor
16+
network. `errors` is `[]` when `valid` is `true`; each error carries a readable
17+
`path` (`events[0].startDate`, `(document)` for the root) and a human-readable
18+
`message`.
19+
20+
## CLI
21+
22+
```
23+
ote-validate <dir|file>
24+
```
25+
26+
- File: validates that document.
27+
- Directory: validates every `*.json` recursively (skips `node_modules` and
28+
hidden entries).
29+
- Type detection: an object with an `events` array is a Feed; anything else is
30+
an Event.
31+
- Output: ``/`` per file with indented errors, plus an `N/M valid` summary.
32+
- Exit codes: `0` all valid · `1` some invalid · `2` usage or I/O error.
33+
34+
## Development
35+
36+
```
37+
pnpm build # tsc → dist/
38+
pnpm test # vitest against fixtures/
39+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Fixtures
2+
3+
Copied from `spec/v0.2/examples` in
4+
[opentechevents-spec](https://github.com/OpenTechEvents/opentechevents-spec/tree/main/spec/v0.2/examples)
5+
(commit `bbddeb970643e451542517f4792d7cd78e543eb9`, copied 2026-07-16).
6+
7+
- `valid/` — documents that must pass validation (`feed.json` is a Feed, the
8+
rest are Events).
9+
- `invalid/` — Event documents that must fail, one per error type.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"specVersion":"0.2.0","id":"https://x.example/1","name":"X","startDate":"2026-06-26","timezone":"Europe/Madrid","license":"CC-BY-4.0","attendanceMode":"presencial"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"specVersion":"0.2.0","id":"https://x.example/1","name":"X","startDate":"2026-06-26","timezone":"Europe/Madrid","license":"CC-BY-4.0","location":{"venue":"X","geo":{"lat":200,"lon":0}}}

0 commit comments

Comments
 (0)