Compare commits

6 Commits
v0.1.0 ... main

Author SHA1 Message Date
rikrdo
e6feea5ee6 F-003 fix: Sanitize SQL dump for safe dev use 2026-05-25 08:14:34 +02:00
rikrdo
3d41579ad3 F-002 fix: Remove secrets and externalize config 2026-05-25 08:00:05 +02:00
rikrdo
d3a558352d chore: ignore python cache files 2026-05-18 00:31:04 +02:00
rikrdo
aaf33880c4 test: add ARNES self-tests and docs index 2026-05-18 00:30:39 +02:00
rikrdo
b396b6d3c9 refactor: make ARNES external-repo based with ticket publish flow 2026-05-18 00:26:32 +02:00
rikrdo
3ff9b70e4c refactor: complete bootstrap of ARNES agent harness framework
- Add complete agent harness structure with 8 roles (leader, triager, architect, implementer, reviewer, security, qa, documenter)
- Implement strict workflow with 9 stages and mandatory gates
- Add comprehensive verification script and runtime status tracking
- Create artifact-based evidence system with contracts and schemas
- Add agent policy matrix with permissions and anti-cheat rules
- Include test suite (44 tests passing) and CI-ready structure
- Add documentation: README, HOWTO, CHECKPOINTS, templates
- Configure model routing policies and token-aware task assignment
- Add BDD/SDD specification guides and feature templates
- Include starter pack for quick project onboarding

All verification checks pass. Framework ready for production use.
2026-05-17 23:25:35 +02:00
115 changed files with 8707 additions and 237 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
__pycache__/
*.pyc
.pytest_cache/
project/web/index/new/config/local.php
project/web/index/new/logs/*.log
project/web/index/new/logs/*.txt
project/sql/private/

9
AGENTS.local.md.example Normal file
View File

@@ -0,0 +1,9 @@
# AGENTS.local.md (optional example)
Use this file for project-specific rules only.
## Example
- App dir: `project/`
- Deploy target: staging Kubernetes cluster
- Extra rule: DB changes require `work/artifacts/<id>/db.md`
- Extra rule: `scripts/verify.local.sh` must run smoke tests

View File

@@ -1,19 +1,56 @@
# AGENTS.md — Entry point del framework # AGENTS.md — Entry point del template ARNES
Este repositorio es un **template genérico** para cualquier proyecto nuevo o en curso.
## Arranque obligatorio ## Arranque obligatorio
1. Leer `work/current.md`. 1. Usar ARNES dentro de un repo de proyecto real, no dentro del repo fuente de ARNES.
2. Leer `backlog/features.json` y seleccionar **una** feature `pending`. 2. Si es primer uso en proyecto: ejecutar `./scripts/start.sh`.
3. Ejecutar `./scripts/verify.sh`. 3. Leer `work/current.md`.
4. Seguir `harness/workflow.stages.yml` y `harness/agents.matrix.yml`. 4. Leer `backlog/features.json` y seleccionar **una** feature `pending`.
5. Ejecutar `./scripts/verify.sh`.
6. Mostrar estado runtime: `python3 scripts/agent_status.py show`.
7. Seguir `harness/workflow.stages.yml` y `harness/agents.matrix.yml`.
## Ticket creation policy
- Tickets are created by `leader` (or `triager`) only.
- Use: `python3 scripts/new_ticket.py`
- Ticket language: **English caveman**.
- Internal orders/handoffs: **English caveman**.
## Estado visible del arnés
- Estado runtime: `work/runtime-status.json`.
- Mostrar: `python3 scripts/agent_status.py show`.
- Actualizar transición:
- `python3 scripts/agent_status.py set --feature-id F-123 --stage build --agent implementer --action "Implementando" --state running --next-agent reviewer --waiting-for "work/artifacts/F-123/implementer.md"`
- Cerrar/idle:
- `python3 scripts/agent_status.py reset`
## Reglas duras ## Reglas duras
- Una sola feature en `in_progress`. - Una sola feature en `in_progress`.
- Ningún agente pasa código por chat: todo va a `work/artifacts/<feature_id>/`. - Ningún agente pasa código por chat: todo va a `work/artifacts/<feature_id>/`.
- `implementer` nunca marca `done`. - `implementer` nunca marca `done`.
- `done` requiere gates aprobados: `reviewer`, `security`, `qa`. - `done` requiere gates aprobados: `reviewer`, `security`, `qa`.
- `done` requiere evidencia de `documenter`: `work/artifacts/<feature_id>/documenter.md`.
- `done` requiere publish final con commit+push del ticket: `work/artifacts/<feature_id>/publish.json`.
- Si `verify.sh` falla, no se cierra la feature. - Si `verify.sh` falla, no se cierra la feature.
## Modelo por tarea (token-aware)
- Use smallest model that fits task.
- Routing config: `harness/models.profiles.yml`
- Rules: `harness/policies/model-routing.md`
## Git publish por ticket
- Al terminar una feature/ticket, `leader` debe ejecutar:
- `python3 scripts/publish_ticket.py --feature-id F-123`
- Esto crea commit + push del ticket y deja evidencia en `work/artifacts/<feature_id>/publish.json`.
## Extensión por proyecto (overlay)
- Opcional: `AGENTS.local.md` para reglas específicas del proyecto actual.
- Opcional: `scripts/verify.local.sh` para checks de dominio.
- El core de ARNES debe seguir siendo agnóstico.
## Reentrada (context loss) ## Reentrada (context loss)
- Releer `work/current.md` y artefactos de la feature activa. - Releer `work/current.md` y artefactos de la feature activa.
- Ejecutar `./scripts/verify.sh`. - Ejecutar `./scripts/verify.sh`.
- Mostrar `python3 scripts/agent_status.py show`.
- Continuar desde “Próximo paso”. - Continuar desde “Próximo paso”.

View File

@@ -1,17 +1,22 @@
# CHECKPOINTS # CHECKPOINTS
## C1 — Estructura ## C1 — Estructura
- [ ] Existe `harness/`, `spec/`, `backlog/`, `work/`, `scripts/`, `platforms/`. - [ ] Existe `project/`, `harness/`, `spec/`, `backlog/`, `work/`, `scripts/`, `platforms/`.
- [ ] `project/README.md` existe como placeholder mínimo.
## C2 — Estado ## C2 — Estado
- [ ] Máximo una feature en `in_progress`. - [ ] Máximo una feature en `in_progress`.
- [ ] Estados válidos en backlog. - [ ] Estados válidos en backlog.
- [ ] Tipos de ticket válidos en backlog.
- [ ] `work/runtime-status.json` válido y visible con `scripts/agent_status.py`.
## C3 — Gates ## C3 — Gates
- [ ] Toda feature `done` tiene `reviewer.json` aprobado. - [ ] Toda feature `done` tiene `reviewer.json` aprobado.
- [ ] Toda feature `done` tiene `security.json` aprobado. - [ ] Toda feature `done` tiene `security.json` aprobado.
- [ ] Toda feature `done` tiene `qa.json` aprobado. - [ ] Toda feature `done` tiene `qa.json` aprobado.
- [ ] Toda feature `done` tiene `leader-close.json` válido. - [ ] Toda feature `done` tiene `leader-close.json` válido.
- [ ] Toda feature `done` tiene `documenter.md`.
- [ ] Toda feature `done` tiene `publish.json` con commit+push del ticket.
## C4 — Verificación ## C4 — Verificación
- [ ] `./scripts/verify.sh` termina en OK. - [ ] `./scripts/verify.sh` termina en OK.

41
HOWTO-FEATURE.md Normal file
View File

@@ -0,0 +1,41 @@
# HOWTO-FEATURE — Crear una feature con SDD y BDD
## Flujo corto
1. Crear ticket en backlog (`python3 scripts/new_ticket.py`)
2. `design` (architect)
3. `build` (implementer)
4. `review/security/qa`
5. `documentation_gate`
6. `close`
7. `publish` (`python3 scripts/publish_ticket.py --feature-id F-001`)
## Artefactos esperados
- `work/artifacts/<feature_id>/triage.md` (opcional)
- `work/artifacts/<feature_id>/architect.md` (opcional)
- `work/artifacts/<feature_id>/implementer.md`
- `work/artifacts/<feature_id>/reviewer.json`
- `work/artifacts/<feature_id>/security.json`
- `work/artifacts/<feature_id>/qa.json`
- `work/artifacts/<feature_id>/documenter.md`
- `work/artifacts/<feature_id>/leader-close.json`
- `work/artifacts/<feature_id>/publish.json`
## Ticket style
- English caveman
- short title
- short acceptance bullets
- clear scope in/out
## BDD notes
- Put `.feature` files in `spec/bdd/features/`
- Put steps in `features/steps/`
- Use tags like `@F-001`, `@smoke`, `@regression`
## Close rule
Feature can be `done` only if:
- review approved
- security approved
- qa approved
- documenter evidence exists
- publish evidence exists (`publish.json`)
- `./scripts/verify.sh` is green

183
HOWTO.md
View File

@@ -1,145 +1,78 @@
# HOWTO — Cómo usar ARNES Framework # HOWTO (breve) — iniciar ARNES en proyecto nuevo o ya empezado
Guía rápida para arrancar proyectos nuevos usando este framework. ## 1) Proyecto nuevo (greenfield)
---
## Fórmula base (siempre igual)
1. **Crear repo nuevo**
2. **Copiar ARNES Framework dentro del repo**
3. **Configurar spec + backlog**
4. **Ejecutar verificación**
5. **Empezar implementación por features (una a la vez)**
---
## 1) Crear repo
```bash ```bash
mkdir mi-proyecto mkdir mi-proyecto && cd mi-proyecto
cd mi-proyecto
git init git init
``` # instalar/copiAR ARNES dentro de este repo de proyecto
/path/to/arnes/scripts/install_into_repo.sh .
--- ./scripts/start.sh
## 2) Copiar framework
Desde tu copia local de ARNES:
```bash
cp -R /ruta/a/arnes/* .
cp -R /ruta/a/arnes/.[!.]* . 2>/dev/null || true
```
> Si usas plantilla remota, clónala y copia su contenido al repo nuevo.
---
## 3) Personalizar proyecto
Edita mínimo:
- `README.md` (contexto del proyecto)
- `spec/product.md` (qué construir)
- `spec/tech.md` (stack y límites técnicos)
- `spec/acceptance.md` (criterios de aceptación)
- `backlog/features.json` (features iniciales en `pending`)
- `harness/agents.matrix.yml` (roles/permisos)
- `harness/workflow.stages.yml` (flujo y gates)
---
## 4) Elegir plataforma (pi.dev u opencode)
Usa el adaptador correspondiente:
- `platforms/pi/`
- `platforms/opencode/`
El núcleo del framework no cambia; solo cambian prompts/hooks/permisos de plataforma.
---
## 5) Inicializar estado de trabajo
Verifica que existan y estén limpios:
- `work/current.md`
- `work/history.md`
- `work/artifacts/`
Pon solo **1 feature activa** (`in_progress`) como máximo.
---
## 6) Ejecutar verificación inicial
```bash
./scripts/verify.sh ./scripts/verify.sh
python3 scripts/agent_status.py show
``` ```
Si falla, **no empezar implementación** hasta dejar todo en verde. Después:
- Mete tu código dentro de `project/` (o indica otra ruta en el wizard).
- Edita `backlog/features.json` (`project`, `description`).
- Crea tu primera feature `pending` (puedes usar `starter-pack/backlog.features.bootstrap.json`).
- Empieza el ciclo SDD (una feature a la vez).
--- ---
## 7) Ciclo operativo por feature ## 2) Proyecto ya empezado (brownfield)
Orden obligatorio: Copia al repo existente solo el core ARNES y coloca el código real en `project/` (o usa otra ruta al lanzar el wizard). Recomendado:
1. `leader` orquesta ```bash
2. `architect` define/ajusta diseño /path/to/arnes/scripts/install_into_repo.sh .
3. `implementer` implementa + tests ```
4. `reviewer` gate técnico
5. `security` gate seguridad
6. `qa` gate funcional
7. `leader` cierra si todo está aprobado
Reglas clave: Contenido core:
- una feature a la vez - `harness/`
- evidencia en disco (`work/artifacts/<feature>/...`) - `spec/`
- nadie marca `done` si falta un gate - `backlog/`
- `work/`
- `scripts/`
- `platforms/`
- `AGENTS.md`, `CHECKPOINTS.md`
Luego ejecuta:
```bash
./scripts/start.sh
./scripts/verify.sh
python3 scripts/agent_status.py show
```
Y añade checks del dominio en:
- `scripts/verify.local.sh` (opcional)
--- ---
## 8) Cierre de feature Crear ticket nuevo (leader/triager, EN caveman):
```bash
python3 scripts/new_ticket.py
```
Antes de pasar a `done`: Tipos soportados:
- `feature`
- `fix`
- `bug`
- `chore`
- `verify.sh` en verde Al final del ticket:
- review aprobado ```bash
- security aprobado python3 scripts/publish_ticket.py --feature-id F-001
- qa aprobado ```
- resumen en `work/history.md`
--- Modelo por tarea:
- Config base en `harness/models.profiles.yml`
- Reglas en `harness/policies/model-routing.md`
## 9) Manejo de pérdida de contexto (memoria) ## Reglas operativas mínimas
- Máximo una feature en `in_progress`.
Si una sesión se corta: - `done` requiere gates `review/security/qa` aprobados.
- `done` requiere publish final con commit+push del ticket.
1. leer `work/current.md` - Evidencia siempre en `work/artifacts/<feature_id>/`.
2. revisar `backlog/features.json` - Si `verify.sh` falla, no se cierra la feature.
3. abrir artefactos de la feature activa
4. ejecutar `./scripts/verify.sh`
5. continuar desde “Próximo paso”
---
## 10) Checklist rápido de arranque
- [ ] Repo creado
- [ ] Framework copiado
- [ ] Specs escritas
- [ ] Backlog definido
- [ ] Matriz de agentes configurada
- [ ] Workflow de stages configurado
- [ ] Verificación inicial OK
- [ ] Primera feature en `pending`
---
## Comando mental (resumen)
**Crear repo → copiar framework → definir spec/backlog → verificar → ejecutar pipeline de 6 agentes con gates obligatorios.**

32
Makefile Normal file
View File

@@ -0,0 +1,32 @@
.PHONY: verify start ticket publish install clean help
verify:
./scripts/verify.sh
start:
./scripts/start.sh
ticket:
python3 scripts/new_ticket.py
publish:
@test -n "$(FEATURE_ID)" || (echo "Use: make publish FEATURE_ID=F-001" && exit 1)
python3 scripts/publish_ticket.py --feature-id $(FEATURE_ID)
install:
@test -n "$(TARGET)" || (echo "Use: make install TARGET=/path/to/project-repo" && exit 1)
./scripts/install_into_repo.sh $(TARGET)
clean:
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true
help:
@echo "ARNES template - commands:"
@echo ""
@echo " make verify - Verify harness core"
@echo " make start - First-run wizard"
@echo " make ticket - Create ticket (EN caveman)"
@echo " make publish FEATURE_ID=.. - Commit and push one ticket"
@echo " make install TARGET=.. - Install ARNES into external repo"
@echo " make clean - Clean cache files"

150
README.md
View File

@@ -1,6 +1,9 @@
# ARNES Framework (agnóstico) — Diseño v0.1 # ARNES Framework (agnóstico) — Diseño v0.1
Framework para construir aplicaciones con agentes autónomos, con control estricto de calidad, seguridad y trazabilidad. Framework para construir aplicaciones con agentes autónomos, con control estricto de calidad, seguridad y trazabilidad.
Convención recomendada: el código real del proyecto vive dentro de `project/`.
Cada proyecto real debe vivir en **su propio repo git**, distinto del repo fuente de ARNES.
Compatible por diseño con **pi.dev** y **opencode** mediante adaptadores. Compatible por diseño con **pi.dev** y **opencode** mediante adaptadores.
--- ---
@@ -25,43 +28,49 @@ Permitir que agentes implementen features de forma autónoma **sin perder contro
--- ---
## Matriz de agentes (6) ## Matriz de agentes (8)
1. **leader** 1. **leader**
- Orquesta etapas y handoffs. - Orquesta etapas y handoffs.
- No implementa código de producto. - Da órdenes internas en English caveman.
2. **architect** 2. **triager**
- Convierte requests en tickets claros.
- Escribe tickets en English caveman.
3. **architect**
- Define/ajusta diseño técnico y contratos. - Define/ajusta diseño técnico y contratos.
- Puede editar documentación y diseño.
3. **implementer** 4. **implementer**
- Implementa una sola feature + tests. - Implementa feature + tests.
- No puede aprobar ni cerrar. - No puede aprobar ni cerrar.
4. **reviewer** 5. **reviewer**
- Revisión técnica vs arquitectura/convenios. - Gate técnico.
- No edita código, solo aprueba/rechaza.
5. **security** 6. **security**
- Gate de seguridad: secretos, dependencias, SAST básico, hardening checks. - Gate de seguridad.
- No edita código.
6. **qa** 7. **qa**
- Gate de calidad funcional: aceptación, integración/E2E, regresión. - Gate funcional.
- No edita código.
8. **documenter**
- Documenta fix/feature/bug y actualiza docs.
--- ---
## Flujo de trabajo (pipeline) ## Flujo de trabajo (pipeline)
1. `intake` (leader) 1. `triage_translate` (leader/triager)
2. `design` (architect) 2. `intake` (leader)
3. `build` (implementer) 3. `design` (architect)
4. `review_gate` (reviewer) 4. `build` (implementer)
5. `security_gate` (security) ✅ 5. `review_gate` (reviewer) ✅
6. `qa_gate` (qa) ✅ 6. `security_gate` (security) ✅
7. `close` (leader) 7. `qa_gate` (qa) ✅
8. `documentation_gate` (documenter) ✅
9. `close` (leader)
10. `publish` (leader) ✅
**Regla:** no hay `done` si cualquier gate falla. **Regla:** no hay `done` si cualquier gate falla.
@@ -77,9 +86,11 @@ Permitir que agentes implementen features de forma autónoma **sin perder contro
### Evidencia obligatoria por etapa ### Evidencia obligatoria por etapa
Cada agente escribe artefactos en disco: Cada agente escribe artefactos en disco:
- `work/artifacts/<feature>/implementer.md` - `work/artifacts/<feature>/implementer.md`
- `work/artifacts/<feature>/reviewer.md` - `work/artifacts/<feature>/reviewer.json`
- `work/artifacts/<feature>/security.md` - `work/artifacts/<feature>/security.json`
- `work/artifacts/<feature>/qa.md` - `work/artifacts/<feature>/qa.json`
- `work/artifacts/<feature>/leader-close.json`
- `work/artifacts/<feature>/publish.json`
Respuesta de agente siempre: `done -> <ruta>` o `blocked -> <ruta>`. Respuesta de agente siempre: `done -> <ruta>` o `blocked -> <ruta>`.
@@ -98,33 +109,59 @@ Respuesta de agente siempre: `done -> <ruta>` o `blocked -> <ruta>`.
```text ```text
. .
├── project/ # código real del proyecto
│ └── README.md
├── README.md ├── README.md
├── AGENTS.md
├── CHECKPOINTS.md
├── harness/ ├── harness/
│ ├── agents.matrix.yml │ ├── agents.matrix.yml
│ ├── workflow.stages.yml │ ├── workflow.stages.yml
│ ├── models.profiles.yml
│ ├── policies/ │ ├── policies/
│ │ ├── security.md
│ │ ├── quality.md
│ │ └── governance.md
│ └── contracts/ │ └── contracts/
│ ├── handoff.md
│ └── evidence.schema.json
├── spec/ ├── spec/
│ ├── product.md │ ├── product.md
│ ├── tech.md │ ├── tech.md
── acceptance.md ── acceptance.md
│ ├── sdd/
│ └── bdd/
├── backlog/ ├── backlog/
│ └── features.json │ └── features.json
├── work/ ├── work/
│ ├── current.md │ ├── current.md
│ ├── history.md │ ├── history.md
│ ├── runtime-status.json
│ └── artifacts/ │ └── artifacts/
── scripts/ ── scripts/
── verify.sh ── start.sh
│ ├── new_ticket.py
│ ├── agent_status.py
│ └── verify.sh
├── defaults/
│ └── flask-skeleton/
└── platforms/
``` ```
--- ---
## Estado runtime visible
- Estado en tiempo real: `work/runtime-status.json`
- CLI: `python3 scripts/agent_status.py show|set|reset`
## Overlays por proyecto (sin contaminar el core)
- Reglas locales: `AGENTS.local.md` (opcional)
- Checks locales: `scripts/verify.local.sh` (opcional)
- El template base sigue agnóstico.
## Lenguaje y modelos
- Política de lenguaje: `harness/policies/language.md` (English caveman interno)
- Routing de modelos: `harness/models.profiles.yml`
- Reglas de routing: `harness/policies/model-routing.md`
## Manejo de pérdidas de memoria (context loss) ## Manejo de pérdidas de memoria (context loss)
Sí: el framework está diseñado para eso. Sí: el framework está diseñado para eso.
@@ -161,10 +198,47 @@ El núcleo no cambia; solo el adaptador.
--- ---
## Inicio rápido
- Instalar ARNES en repo externo: `./scripts/install_into_repo.sh /path/to/project-repo`
- Ejecuta wizard: `./scripts/start.sh`
- Crear ticket: `python3 scripts/new_ticket.py`
- Publicar ticket: `python3 scripts/publish_ticket.py --feature-id F-001`
- Guía breve: `HOWTO.md`
- Starter pack: `starter-pack/README.md`
- Adaptación del template: `TEMPLATE.md`
- Índice de docs: `docs/README.md`
- Layout del repo: `docs/repository-layout.md`
- Referencia de scripts: `docs/scripts-reference.md`
- Manual Skeleton (uso + mejoras): `docs/skeleton-manual.md`
## Tipos de tarea / ticket
`python3 scripts/new_ticket.py` soporta estos tipos:
- `feature`: nueva capacidad
- `fix`: corrección de comportamiento roto
- `bug`: incidencia reportada o defecto claro
- `chore`: trabajo interno, refactor, setup, mantenimiento
Además guarda campos estructurados:
- `problem`
- `goal`
- `scope_in`
- `scope_out`
- `priority`
- `risk`
- `acceptance`
Convención recomendada:
- usar `feature` para trabajo nuevo visible
- usar `fix` o `bug` para reparación
- usar `chore` para cambios internos sin valor funcional directo
## Próximos pasos sugeridos ## Próximos pasos sugeridos
1. Definir `agents.matrix.yml` completo (permisos exactos por rutas). 1. Instalar/copiar ARNES en un repo de proyecto real distinto del repo fuente.
2. Definir `workflow.stages.yml` con transiciones válidas. 2. Definir el backlog inicial del proyecto real.
3. Diseñar `features.json` con estados y criterios de aceptación. 3. Configurar overlay opcional (`AGENTS.local.md`, `scripts/verify.local.sh`).
4. Especificar `scripts/verify.sh` (lint/test/security/qa gates). 4. Ejecutar `./scripts/verify.sh` y `python3 scripts/agent_status.py show`.
5. Crear adaptadores `platforms/pi` y `platforms/opencode`. 5. Empezar la primera feature `pending` con pipeline completo y terminar con commit+push del ticket.

37
TEMPLATE.md Normal file
View File

@@ -0,0 +1,37 @@
# TEMPLATE.md — Cómo adaptar ARNES a cualquier proyecto
## 1) Clonar y renombrar contexto
- Pon el código real dentro de `project/` (o elige otra ruta en `./scripts/start.sh`).
- Ajusta `backlog/features.json` (`project`, `description`).
- Crea primeras features reales en `features[]`.
## 2) Reglas específicas (sin tocar core)
- Opcional: crea `AGENTS.local.md` con reglas del dominio.
- Opcional: crea `scripts/verify.local.sh` con checks propios del stack.
- Mantén tickets y órdenes internas en English caveman (`harness/policies/language.md`).
- Usa tipos de ticket consistentes: `feature`, `fix`, `bug`, `chore`.
- Ajusta routing de modelos por rol/tarea en `harness/models.profiles.yml`.
## 3) Flujo estándar
0. Instalar ARNES en repo externo: `./scripts/install_into_repo.sh /path/to/project-repo`
1. `./scripts/start.sh` (primer uso)
2. `python3 scripts/new_ticket.py` (leader/triager)
3. `python3 scripts/agent_status.py show`
4. Seleccionar 1 feature `pending` y pasarla a `in_progress`
5. Implementar con artefactos en `work/artifacts/<feature_id>/`
6. Cerrar con gates `review/security/qa` + `documenter` aprobados
7. Publicar ticket: `python3 scripts/publish_ticket.py --feature-id F-001`
8. `python3 scripts/agent_status.py reset`
## 4) Contrato de cierre
- `status=done` exige:
- `reviewer.json` APPROVED
- `security.json` APPROVED
- `qa.json` APPROVED
- `leader-close.json` APPROVED
- `publish.json` PUBLISHED
- `./scripts/verify.sh` OK
## 5) Principio de template
- El core ARNES es agnóstico.
- Todo lo específico de proyecto vive en overlays (`AGENTS.local.md`, `verify.local.sh`, docs propias).

View File

@@ -1,24 +1,156 @@
{ {
"project": "nuevo-proyecto", "project": "template-project",
"description": "Backlog inicial del proyecto", "description": "Template ARNES agnóstico para cualquier proyecto",
"rules": { "rules": {
"one_feature_at_a_time": true, "one_feature_at_a_time": true,
"require_review_gate": true, "require_review_gate": true,
"require_security_gate": true, "require_security_gate": true,
"require_qa_gate": true, "require_qa_gate": true,
"valid_status": ["pending", "in_progress", "blocked", "done"] "valid_status": [
"pending",
"in_progress",
"blocked",
"done"
],
"valid_types": [
"feature",
"fix",
"bug",
"chore"
]
},
"template_feature_schema": {
"id": "F-001",
"type": "feature",
"title": "Short ticket title",
"problem": "Need change",
"goal": "Make flow better",
"scope_in": [
"Core flow"
],
"scope_out": [
"No redesign"
],
"priority": "med",
"risk": "low",
"description": "Problem: ... Goal: ... Scope IN: ... Scope OUT: ... Type: ... Priority: ... Risk: ...",
"acceptance": [
"Flow works end to end",
"No break old behavior"
],
"status": "pending",
"created_at": "YYYY-MM-DD",
"gates": {
"review": false,
"security": false,
"qa": false
}
}, },
"features": [ "features": [
{ {
"id": "F-001", "id": "F-001",
"title": "Definir estructura inicial", "type": "feature",
"description": "Bootstrap del proyecto con estructura base.", "title": "Document and move legacy PHP app into ARNES project layout",
"acceptance": [ "problem": "Legacy PHP app lives in temporary folder and has no ARNES design record",
"Estructura base creada", "goal": "Create SDD record and move code and SQL into stable project layout",
"Tests o checks iniciales definidos", "scope_in": [
"Artefactos de gate configurados" "SDD docs",
"ADR for layout",
"move project/new to project/web/index/new",
"move SQL dump to project/sql"
], ],
"status": "pending" "scope_out": [
"No functional refactor",
"No production deploy",
"No OpenAI or auth rewrite yet"
],
"priority": "high",
"risk": "med",
"description": "Problem: Legacy PHP app lives in temporary folder and has no ARNES design record. Goal: Create SDD record and move code and SQL into stable project layout. Scope IN: SDD docs, ADR for layout, move project/new to project/web/index/new, move SQL dump to project/sql. Scope OUT: No functional refactor, No production deploy, No OpenAI or auth rewrite yet. Type: feature. Priority: high. Risk: med.",
"acceptance": [
"SDD docs exist and explain current legacy app structure",
"ADR records why code moves under project/web and SQL under project/sql",
"Legacy code is moved with same contents and no file loss",
"SQL dump is kept as local development baseline in project/sql",
"verify.sh is green"
],
"status": "blocked",
"created_at": "2026-05-25",
"gates": {
"review": false,
"security": false,
"qa": false
}
},
{
"id": "F-002",
"type": "fix",
"title": "Remove secrets and externalize config",
"problem": "Secrets live in repo and prod URLs live in code",
"goal": "Move secrets and config out of source files",
"scope_in": [
"config loader",
"replace hardcoded DB and OpenAI values",
"centralize base URLs and external endpoints",
"setup docs"
],
"scope_out": [
"No business logic refactor",
"No deploy automation",
"No auth redesign"
],
"priority": "high",
"risk": "high",
"description": "Problem: Secrets live in repo and prod URLs live in code. Goal: Move secrets and config out of source files. Scope IN: config loader, replace hardcoded DB and OpenAI values, centralize base URLs and external endpoints, setup docs. Scope OUT: No business logic refactor, No deploy automation, No auth redesign. Type: fix. Priority: high. Risk: high.",
"acceptance": [
"No hard-coded API or DB secrets stay in versioned PHP files",
"Config values load from one local config source",
"Prod URLs and external endpoints are configurable",
"Legacy pages still point to valid local config keys after change",
"verify.sh is green"
],
"status": "done",
"created_at": "2026-05-25",
"gates": {
"review": false,
"security": false,
"qa": false
}
},
{
"id": "F-003",
"type": "fix",
"title": "Sanitize SQL dump for safe dev use",
"problem": "Repo keeps production-like SQL dump with sensitive data risk",
"goal": "Keep dev database baseline without sensitive live data in repo",
"scope_in": [
"review dump scope",
"define safe replacement strategy",
"remove or redact sensitive data",
"document local data handling"
],
"scope_out": [
"No app logic change",
"No production DB changes",
"No schema redesign"
],
"priority": "high",
"risk": "high",
"description": "Problem: Repo keeps production-like SQL dump with sensitive data risk. Goal: Keep dev database baseline without sensitive live data in repo. Scope IN: review dump scope, define safe replacement strategy, remove or redact sensitive data, document local data handling. Scope OUT: No app logic change, No production DB changes, No schema redesign. Type: fix. Priority: high. Risk: high.",
"acceptance": [
"Repo no longer stores raw sensitive production-like SQL dump as current dev baseline",
"Safe dev data handling is documented",
"Replacement dump or import path keeps local development possible",
"Security risk note for SQL data is addressed",
"verify.sh is green"
],
"status": "done",
"created_at": "2026-05-25",
"gates": {
"review": false,
"security": false,
"qa": false
}
} }
] ]
} }

View File

@@ -0,0 +1,17 @@
# Default UI assets (Flask + Skeleton)
Estos archivos se usan como **default** cuando el `scripts/start.sh` configure stack por defecto:
- Python/Flask
- MariaDB
- Skeleton CSS
## Origen
Copiados desde:
- `~/git/Skeleton-2.0.4/css/normalize.css`
- `~/git/Skeleton-2.0.4/css/skeleton.css`
- `~/git/Skeleton-2.0.4/images/favicon.png`
## Ubicación de destino recomendada en proyecto
- `static/css/normalize.css`
- `static/css/skeleton.css`
- `static/images/favicon.png`

View File

@@ -0,0 +1,12 @@
# Upstream notes — Skeleton
Repositorio revisado: `https://github.com/getskeleton/Skeleton`
Versión base usada: `2.0.4` (2014)
Archivos copiados al template:
- `css/normalize.css`
- `css/skeleton.css`
- `images/favicon.png`
Referencia rápida de uso y mejoras:
- `docs/skeleton-manual.md`

View File

@@ -0,0 +1,427 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

View File

@@ -0,0 +1,418 @@
/*
* Skeleton V2.0.4
* Copyright 2014, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
* 12/29/2014
*/
/* Table of contents
- Grid
- Base Styles
- Typography
- Links
- Buttons
- Forms
- Lists
- Code
- Tables
- Spacing
- Utilities
- Clearing
- Media Queries
*/
/* Grid
*/
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box; }
.column,
.columns {
width: 100%;
float: left;
box-sizing: border-box; }
/* For devices larger than 400px */
@media (min-width: 400px) {
.container {
width: 85%;
padding: 0; }
}
/* For devices larger than 550px */
@media (min-width: 550px) {
.container {
width: 80%; }
.column,
.columns {
margin-left: 4%; }
.column:first-child,
.columns:first-child {
margin-left: 0; }
.one.column,
.one.columns { width: 4.66666666667%; }
.two.columns { width: 13.3333333333%; }
.three.columns { width: 22%; }
.four.columns { width: 30.6666666667%; }
.five.columns { width: 39.3333333333%; }
.six.columns { width: 48%; }
.seven.columns { width: 56.6666666667%; }
.eight.columns { width: 65.3333333333%; }
.nine.columns { width: 74.0%; }
.ten.columns { width: 82.6666666667%; }
.eleven.columns { width: 91.3333333333%; }
.twelve.columns { width: 100%; margin-left: 0; }
.one-third.column { width: 30.6666666667%; }
.two-thirds.column { width: 65.3333333333%; }
.one-half.column { width: 48%; }
/* Offsets */
.offset-by-one.column,
.offset-by-one.columns { margin-left: 8.66666666667%; }
.offset-by-two.column,
.offset-by-two.columns { margin-left: 17.3333333333%; }
.offset-by-three.column,
.offset-by-three.columns { margin-left: 26%; }
.offset-by-four.column,
.offset-by-four.columns { margin-left: 34.6666666667%; }
.offset-by-five.column,
.offset-by-five.columns { margin-left: 43.3333333333%; }
.offset-by-six.column,
.offset-by-six.columns { margin-left: 52%; }
.offset-by-seven.column,
.offset-by-seven.columns { margin-left: 60.6666666667%; }
.offset-by-eight.column,
.offset-by-eight.columns { margin-left: 69.3333333333%; }
.offset-by-nine.column,
.offset-by-nine.columns { margin-left: 78.0%; }
.offset-by-ten.column,
.offset-by-ten.columns { margin-left: 86.6666666667%; }
.offset-by-eleven.column,
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
.offset-by-one-third.column,
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
.offset-by-two-thirds.column,
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
.offset-by-one-half.column,
.offset-by-one-half.columns { margin-left: 52%; }
}
/* Base Styles
*/
/* NOTE
html is set to 62.5% so that all the REM measurements throughout Skeleton
are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
font-size: 62.5%; }
body {
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
line-height: 1.6;
font-weight: 400;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #222; }
/* Typography
*/
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 2rem;
font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
/* Larger than phablet */
@media (min-width: 550px) {
h1 { font-size: 5.0rem; }
h2 { font-size: 4.2rem; }
h3 { font-size: 3.6rem; }
h4 { font-size: 3.0rem; }
h5 { font-size: 2.4rem; }
h6 { font-size: 1.5rem; }
}
p {
margin-top: 0; }
/* Links
*/
a {
color: #1EAEDB; }
a:hover {
color: #0FA0CE; }
/* Buttons
*/
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
display: inline-block;
height: 38px;
padding: 0 30px;
color: #555;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 38px;
letter-spacing: .1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
color: #333;
border-color: #888;
outline: 0; }
.button.button-primary,
button.button-primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
color: #FFF;
background-color: #33C3F0;
border-color: #33C3F0; }
.button.button-primary:hover,
button.button-primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
color: #FFF;
background-color: #1EAEDB;
border-color: #1EAEDB; }
/* Forms
*/
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea,
select {
height: 38px;
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
background-color: #fff;
border: 1px solid #D1D1D1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none; }
textarea {
min-height: 65px;
padding-top: 6px;
padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
border: 1px solid #33C3F0;
outline: 0; }
label,
legend {
display: block;
margin-bottom: .5rem;
font-weight: 600; }
fieldset {
padding: 0;
border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
display: inline; }
label > .label-body {
display: inline-block;
margin-left: .5rem;
font-weight: normal; }
/* Lists
*/
ul {
list-style: circle inside; }
ol {
list-style: decimal inside; }
ol, ul {
padding-left: 0;
margin-top: 0; }
ul ul,
ul ol,
ol ol,
ol ul {
margin: 1.5rem 0 1.5rem 3rem;
font-size: 90%; }
li {
margin-bottom: 1rem; }
/* Code
*/
code {
padding: .2rem .5rem;
margin: 0 .2rem;
font-size: 90%;
white-space: nowrap;
background: #F1F1F1;
border: 1px solid #E1E1E1;
border-radius: 4px; }
pre > code {
display: block;
padding: 1rem 1.5rem;
white-space: pre; }
/* Tables
*/
th,
td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #E1E1E1; }
th:first-child,
td:first-child {
padding-left: 0; }
th:last-child,
td:last-child {
padding-right: 0; }
/* Spacing
*/
button,
.button {
margin-bottom: 1rem; }
input,
textarea,
select,
fieldset {
margin-bottom: 1.5rem; }
pre,
blockquote,
dl,
figure,
table,
p,
ul,
ol,
form {
margin-bottom: 2.5rem; }
/* Utilities
*/
.u-full-width {
width: 100%;
box-sizing: border-box; }
.u-max-full-width {
max-width: 100%;
box-sizing: border-box; }
.u-pull-right {
float: right; }
.u-pull-left {
float: left; }
/* Misc
*/
hr {
margin-top: 3rem;
margin-bottom: 3.5rem;
border-width: 0;
border-top: 1px solid #E1E1E1; }
/* Clearing
*/
/* Self Clearing Goodness */
.container:after,
.row:after,
.u-cf {
content: "";
display: table;
clear: both; }
/* Media Queries
*/
/*
Note: The best way to structure the use of media queries is to create the queries
near the relevant code. For example, if you wanted to change the styles for buttons
on small devices, paste the mobile query code up in the buttons section and style it
there.
*/
/* Larger than mobile */
@media (min-width: 400px) {}
/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {}
/* Larger than tablet */
@media (min-width: 750px) {}
/* Larger than desktop */
@media (min-width: 1000px) {}
/* Larger than Desktop HD */
@media (min-width: 1200px) {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

17
docs/README.md Normal file
View File

@@ -0,0 +1,17 @@
# Docs index
## Core docs
- `../README.md` — framework overview
- `../HOWTO.md` — quick start
- `../HOWTO-FEATURE.md` — feature workflow
- `../TEMPLATE.md` — how to adapt ARNES
## Reference docs
- `repository-layout.md` — repo structure and separation rules
- `scripts-reference.md` — start/verify/ticket/publish/install scripts
- `skeleton-manual.md` — default Skeleton UI notes
## Harness source of truth
- `../harness/agents.matrix.yml` — roles and edit boundaries
- `../harness/workflow.stages.yml` — ordered workflow stages
- `../harness/policies/` — governance, security, quality, language, model routing

40
docs/repository-layout.md Normal file
View File

@@ -0,0 +1,40 @@
# Repository layout
## Core idea
- ARNES core lives at repository root once installed into a project repo.
- The source repo of ARNES is not the repo where product work should happen.
- Real project code lives in `project/` by default.
- Project-specific rules live in overlays, not in core files.
## Main directories
- `project/` — real app code
- `backlog/` — ticket list and feature state
- `work/` — runtime state, history, artifacts
- `harness/` — workflow, roles, policies, contracts
- `spec/` — product, tech, acceptance, SDD, BDD source docs
- `features/` — optional executable BDD runner assets
- `scripts/` — start, verify, ticket creation, publish, install, runtime status
- `platforms/` — platform adapters (pi, opencode)
- `defaults/` — optional starter assets
- `tests/` — self-tests for the ARNES source repo only
## Recommended separation
- Core ARNES should stay generic.
- Domain checks go in `scripts/verify.local.sh`.
- Domain rules go in `AGENTS.local.md`.
- Real code should not be mixed into `harness/`, `work/`, `backlog/`, or `spec/`.
- Source-repo self-tests under `tests/` are not part of installed project repos.
## Default project shape
```text
project/
├── README.md
├── templates/
├── static/
│ ├── css/
│ ├── js/
│ └── images/
└── ...
```
This shape is only a default. The wizard can target another app directory if needed.

69
docs/scripts-reference.md Normal file
View File

@@ -0,0 +1,69 @@
# Scripts reference
## `./scripts/install_into_repo.sh /path/to/project-repo`
Copies ARNES core into a different project repo.
What it does:
- refuses to install into the ARNES source repo itself
- initializes git repo at target if missing
- copies ARNES core files into target repo
## `./scripts/start.sh`
Interactive bootstrap wizard.
What it does:
- asks project metadata
- chooses default app directory (`project/` by default)
- defaults Python/Flask projects to `python3 -m unittest discover -s project/tests -v`
- seeds a minimal bootstrap smoke test under `project/tests/` for Python/Flask
- writes `harness/project.config.json`
- creates `scripts/verify.local.sh`
- can seed one bootstrap ticket
- resets runtime status
## `./scripts/verify.sh`
Core harness verification.
What it checks:
- required core files exist
- project is inside a git repo
- warns if no git remote exists
- backlog JSON is valid
- only one feature is `in_progress`
- done features have all required artifacts, including publish evidence
- runtime status JSON is valid
- source-repo self-tests run automatically if `tests/` exists
- optional local overlay runs if present
## `python3 scripts/new_ticket.py`
Interactive ticket creator.
Writes one new backlog entry with:
- `type`
- `title`
- `problem`
- `goal`
- `scope_in`
- `scope_out`
- `priority`
- `risk`
- `acceptance`
## `python3 scripts/publish_ticket.py --feature-id F-001`
Final publish step for one ticket.
What it does:
- validates git repo, remote, and git identity
- writes `work/artifacts/<feature_id>/publish.json`
- creates one commit for the ticket
- pushes the branch to remote
## `python3 scripts/agent_status.py`
Runtime status helper.
Commands:
- `show`
- `set`
- `reset`
The `set` command validates stage and agent names against harness files.

107
docs/skeleton-manual.md Normal file
View File

@@ -0,0 +1,107 @@
# Manual de uso — Skeleton 2.0.4 (para ARNES template)
> Fuente revisada: `https://github.com/getskeleton/Skeleton` (v2.0.4, 2014).
## 1) Qué es y cuándo usarlo
Skeleton es un boilerplate CSS ligero (no framework UI completo). Ideal para:
- paneles internos
- CRUDs
- prototipos rápidos
- apps server-rendered (Flask + Jinja)
No ideal para:
- diseño de componentes complejos
- design systems avanzados
- apps con alta complejidad visual
## 2) Archivos base
En este template se incluyen en:
- `defaults/flask-skeleton/static/css/normalize.css`
- `defaults/flask-skeleton/static/css/skeleton.css`
- `defaults/flask-skeleton/static/images/favicon.png`
Uso recomendado en HTML:
```html
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
```
## 3) Grid (12 columnas)
Estructura:
```html
<div class="container">
<div class="row">
<div class="six columns">...</div>
<div class="six columns">...</div>
</div>
</div>
```
Clases comunes:
- `one` ... `twelve columns`
- `one-half column`
- `one-third column`
- `two-thirds column`
Offsets:
- `offset-by-one ... offset-by-eleven`
Breakpoints relevantes:
- `min-width: 400px`
- `min-width: 550px` (aquí se activa grid multi-columna)
- `750px`, `1000px`, `1200px`
## 4) Utilidades clave
- `u-full-width` → ancho 100%
- `u-max-full-width` → max-width 100%
- `u-pull-right`, `u-pull-left`
- `u-cf` (clear float)
## 5) Formularios y botones
Inputs/select/textarea ya traen estilo base.
Patrones recomendados:
- siempre usar `u-full-width` en formularios de app
- usar `.button` y `.button.button-primary`
- mantener labels visibles (`label` + `for`)
## 6) Tablas
Skeleton trae estilo mínimo. Para tablas largas:
- envolver en `.table-responsive` propia del proyecto
- controlar overflow horizontal en móviles
## 7) Limitaciones conocidas (por antigüedad)
- Sistema basado en **floats** (no flex/grid nativo)
- Tipografía por defecto antigua (Raleway de Google Fonts vía URL legacy)
- No incluye componentes modernos (modal, tabs, toast, etc.)
- No incluye tokens de diseño ni theming avanzado
## 8) Mejoras recomendadas en proyectos nuevos
Mantener Skeleton como base, pero añadir capa moderna propia:
1. **Layout helper CSS local**
- utilidades flex (`.d-flex`, `.justify-between`, etc.)
- spacing consistente (`.mb-1`, `.mb-2`, ...)
2. **Responsive wrappers**
- `.table-responsive`
- patrones mobile-first para filtros/toolbar
3. **Componentes mínimos reutilizables**
- modal base
- badges
- pagination bar
- alertas/confirmaciones
4. **Accesibilidad**
- foco visible
- contraste de colores
- labels/aria en acciones icon-only
5. **No tocar upstream directamente**
- dejar `skeleton.css` y `normalize.css` sin modificar
- personalización en `custom.css` del proyecto
## 9) Regla de mantenimiento para ARNES
- Skeleton se trata como dependencia estática base.
- Cualquier override va en CSS del proyecto.
- Si un proyecto requiere UI compleja, considerar migración progresiva a capa de componentes propia.

11
features/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Executable BDD assets
This directory is for executable BDD helpers.
Recommended split:
- `spec/bdd/features/` = source-of-truth scenarios in Gherkin
- `features/steps/` = executable step definitions and runner config
- `features/behave.ini` = Behave runner config
Keep feature text in `spec/bdd/features/`.
Keep runner-specific code in `features/`.

8
features/behave.ini Normal file
View File

@@ -0,0 +1,8 @@
[behave]
paths = features/
format = pretty
# Examples:
# behave features/
# behave features/ --tags @smoke
# behave features/ --tags ~@slow

0
features/steps/.gitkeep Normal file
View File

View File

@@ -0,0 +1,48 @@
# Common Steps
Steps reutilizables para múltiples features.
## Navigation
```python
@given('el usuario está en la página principal')
def step_at_home_page(context):
context.current_page = "home"
@when('el usuario hace clic en el elemento de menú "{menu_item}"')
def step_click_menu(context, menu_item):
context.menu_clicked = menu_item
```
## Error Handling
```python
@given('la conexión a internet está disponible')
def step_internet_available(context):
context.internet_available = True
@given('el servidor no responde')
def step_server_down(context):
context.server_responding = False
@then('el sistema muestra toast "{message}"')
def step_show_toast(context, message):
context.toast_message = message
```
## User Session
```python
@given('el usuario tiene sesión activa')
def step_user_logged_in(context):
context.user_logged_in = True
context.token = "valid_token"
@then('el sistema muestra indicador de carga')
def step_show_loading(context):
context.showing_loading = True
@then('después de timeout muestra error "{message}"')
def step_timeout_error(context, message):
assert context.timeout_error == message
```

View File

@@ -2,29 +2,44 @@ version: 1
roles: roles:
leader: leader:
can_edit: ["work/", "backlog/", "spec/", "harness/"] emoji: "🧭"
cannot_edit: ["src/", "tests/"] can_edit: ["work/", "backlog/", "spec/", "harness/", "AGENTS.md", "CHECKPOINTS.md"]
cannot_edit: ["project/", "tests/"]
responsibilities: responsibilities:
- plan - plan
- orchestrate - orchestrate
- enforce_gates - enforce_gates
- publish_ticket_changes
- close_feature - close_feature
- issue_orders_in_english_caveman
triager:
emoji: "🧩"
can_edit: ["backlog/", "work/artifacts/", "spec/"]
cannot_edit: ["project/", "tests/", "backlog/features.json:status=done"]
responsibilities:
- normalize_requests
- create_tickets_in_english_caveman
- define_scope_acceptance
architect: architect:
emoji: "🏗️"
can_edit: ["spec/", "harness/contracts/", "docs/"] can_edit: ["spec/", "harness/contracts/", "docs/"]
cannot_edit: ["src/", "tests/", "backlog/features.json:status"] cannot_edit: ["project/", "tests/", "backlog/features.json:status"]
responsibilities: responsibilities:
- design - design
- update_contracts - update_contracts
implementer: implementer:
can_edit: ["src/", "tests/", "work/artifacts/"] emoji: "🛠️"
can_edit: ["project/", "tests/", "work/artifacts/"]
cannot_edit: cannot_edit:
- "backlog/features.json:done" - "backlog/features.json:done"
- "work/history.md" - "work/history.md"
- "work/artifacts/*/reviewer.json" - "work/artifacts/*/reviewer.json"
- "work/artifacts/*/security.json" - "work/artifacts/*/security.json"
- "work/artifacts/*/qa.json" - "work/artifacts/*/qa.json"
- "work/artifacts/*/publish.json"
- "work/artifacts/*/leader-close.json" - "work/artifacts/*/leader-close.json"
responsibilities: responsibilities:
- implement_feature - implement_feature
@@ -32,15 +47,17 @@ roles:
- produce_implementer_evidence - produce_implementer_evidence
reviewer: reviewer:
emoji: "🔍"
can_edit: ["work/artifacts/"] can_edit: ["work/artifacts/"]
cannot_edit: ["src/", "tests/", "backlog/"] cannot_edit: ["project/", "tests/", "backlog/"]
responsibilities: responsibilities:
- technical_review - technical_review
- emit_reviewer_verdict - emit_reviewer_verdict
security: security:
emoji: "🔒"
can_edit: ["work/artifacts/"] can_edit: ["work/artifacts/"]
cannot_edit: ["src/", "tests/", "backlog/"] cannot_edit: ["project/", "tests/", "backlog/"]
responsibilities: responsibilities:
- sast - sast
- dependency_review - dependency_review
@@ -48,16 +65,27 @@ roles:
- emit_security_verdict - emit_security_verdict
qa: qa:
emoji: "🧪"
can_edit: ["work/artifacts/"] can_edit: ["work/artifacts/"]
cannot_edit: ["src/", "tests/", "backlog/"] cannot_edit: ["project/", "tests/", "backlog/"]
responsibilities: responsibilities:
- acceptance_traceability - acceptance_traceability
- integration_e2e_checks - integration_e2e_checks
- regression_checks - regression_checks
- emit_qa_verdict - emit_qa_verdict
documenter:
emoji: "📚"
can_edit: ["docs/", "spec/", "README.md", "HOWTO.md", "work/artifacts/"]
cannot_edit: ["project/", "tests/", "backlog/features.json:status"]
responsibilities:
- document_feature_changes
- update_user_docs
- emit_documenter_summary
anti_cheat: anti_cheat:
- "Implementer cannot promote feature to done" - "Implementer cannot promote feature to done"
- "Done requires reviewer/security/qa approved artifacts" - "Done requires reviewer/security/qa approved artifacts"
- "Done requires documenter evidence"
- "Leader close requires verify.sh success" - "Leader close requires verify.sh success"
- "Evidence must be on disk; chat-only claims are invalid" - "Evidence must be on disk; chat-only claims are invalid"

View File

@@ -0,0 +1,51 @@
version: 1
policy:
goal: "Use smallest model that can do task well"
fallback_order: ["tiny", "small", "medium", "large"]
profiles:
tiny:
use_for:
- status updates
- file moves
- boilerplate JSON
- simple docs formatting
small:
use_for:
- triage ticket drafting
- reviewer/security/qa short verdicts
- changelog/doc updates
- refactors with low logic risk
medium:
use_for:
- architecture decisions
- non-trivial implementation
- multi-file integration changes
large:
use_for:
- complex debugging
- deep root-cause analysis
- migrations with high risk
- ambiguous requirements
role_defaults:
leader: small
triager: small
architect: medium
implementer: medium
reviewer: small
security: small
qa: small
documenter: tiny
stage_overrides:
triage_translate: small
intake: small
design: medium
build: medium
review_gate: small
security_gate: small
qa_gate: small
documentation_gate: tiny
close: small

View File

@@ -0,0 +1,22 @@
# Policy: Language and style
## Internal language
- Internal artifacts, tickets, and leader orders must be in **English**.
- User chat can be in any language.
## Style mode: Caveman English
- Short words.
- Short lines.
- One idea per line.
- No fluff.
- No long intros.
- Prefer bullets.
## Ticket writing rules
- Title: 410 words.
- Acceptance: 36 bullets max.
- Keep scope explicit (in/out).
- Use active verbs: Fix, Add, Move, Remove, Validate.
## Runtime action rules
- `agent_status.action` should be concise (<= 60 chars).

View File

@@ -0,0 +1,24 @@
# Policy: Model routing
Use model by task complexity, not by habit.
## Core rule
- Start small.
- Escalate only when blocked or quality poor.
## Escalation triggers
- Repeated failed attempts.
- Ambiguous requirements.
- Cross-module side effects.
- Security-critical code paths.
## De-escalation triggers
- Routine CRUD edits.
- Mechanical refactors.
- Artifact writing.
- Status/timeline updates.
## Required behavior
- Record chosen model class in artifact header when work is non-trivial.
- Keep outputs concise to reduce token burn.
- If `harness/project.config.json` has `model_mode=lean`, prefer tiny/small whenever possible.

View File

@@ -4,6 +4,15 @@ feature_states:
allowed: [pending, in_progress, blocked, done] allowed: [pending, in_progress, blocked, done]
stages: stages:
- name: triage_translate
owner: leader
optional: true
input:
- backlog/features.json
- work/current.md
output:
- work/artifacts/<feature_id>/triage.md
- name: intake - name: intake
owner: leader owner: leader
input: input:
@@ -41,6 +50,12 @@ stages:
output: output:
- work/artifacts/<feature_id>/qa.json - work/artifacts/<feature_id>/qa.json
- name: documentation_gate
owner: documenter
required: true
output:
- work/artifacts/<feature_id>/documenter.md
- name: close - name: close
owner: leader owner: leader
required: true required: true
@@ -48,8 +63,16 @@ stages:
- work/artifacts/<feature_id>/leader-close.json - work/artifacts/<feature_id>/leader-close.json
- work/history.md - work/history.md
- name: publish
owner: leader
required: true
output:
- work/artifacts/<feature_id>/publish.json
close_requirements: close_requirements:
- reviewer.json.verdict == "APPROVED" - reviewer.json.verdict == "APPROVED"
- security.json.verdict == "APPROVED" - security.json.verdict == "APPROVED"
- qa.json.verdict == "APPROVED" - qa.json.verdict == "APPROVED"
- documenter.md exists
- publish.json.verdict == "PUBLISHED"
- scripts/verify.sh exit_code == 0 - scripts/verify.sh exit_code == 0

11
project/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Project code lives here
Current project layout:
- `project/web/index/new/` — legacy PHP web module copied from production
- `project/web/index/new/config/local.example.php` — versioned local config template
- `project/web/index/new/config/local.php` — ignored local config with real values
- `project/sql/db-25052026.sql` — sanitized local development SQL baseline
- `project/sql/private/` — optional ignored path for private raw dumps
ARNES core stays outside this folder.

25
project/sql/README.md Normal file
View File

@@ -0,0 +1,25 @@
# SQL baselines for local development
## Tracked baseline
- `db-25052026.sql`
- Purpose: safe local baseline for the legacy PHP module
- Content: schema and synthetic seed data only
- Safe for commit and push
## Private local data
- If you need a private raw snapshot, keep it outside git.
- Recommended local ignored path: `project/sql/private/`
- Do not commit raw customer, order, or production-like data back to this repo.
## Local import
Example:
```bash
mysql -u root -p < project/sql/db-25052026.sql
```
The sanitized baseline includes the tables used by:
- `index.php`
- `productos_bulk_update.php`
- `productos_modificados.php`
- `worker_bulk.php`

170
project/sql/db-25052026.sql Normal file
View File

@@ -0,0 +1,170 @@
-- Sanitized development baseline for legacy PHP module
-- Synthetic data only. Safe for local development and version control.
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
CREATE DATABASE IF NOT EXISTS `legacy_module_dev` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `legacy_module_dev`;
DROP TABLE IF EXISTS `oc_product_queue`;
DROP TABLE IF EXISTS `oc_product_description`;
DROP TABLE IF EXISTS `oc_product`;
DROP TABLE IF EXISTS `oc_attribute_description`;
DROP TABLE IF EXISTS `oc_tax_class`;
DROP TABLE IF EXISTS `oc_url_alias`;
DROP TABLE IF EXISTS `oc_manufacturer_to_store`;
DROP TABLE IF EXISTS `oc_manufacturer`;
DROP TABLE IF EXISTS `oc_category_to_store`;
DROP TABLE IF EXISTS `oc_category_description`;
DROP TABLE IF EXISTS `oc_category`;
CREATE TABLE `oc_category` (
`category_id` int(11) NOT NULL,
PRIMARY KEY (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `oc_category_description` (
`category_id` int(11) NOT NULL,
`language_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`category_id`,`language_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `oc_category_to_store` (
`category_id` int(11) NOT NULL,
`store_id` int(11) NOT NULL,
PRIMARY KEY (`category_id`,`store_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `oc_manufacturer` (
`manufacturer_id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
`image` varchar(255) DEFAULT NULL,
`sort_order` int(3) NOT NULL DEFAULT 0,
PRIMARY KEY (`manufacturer_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `oc_manufacturer_to_store` (
`manufacturer_id` int(11) NOT NULL,
`store_id` int(11) NOT NULL,
PRIMARY KEY (`manufacturer_id`,`store_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `oc_url_alias` (
`url_alias_id` int(11) NOT NULL AUTO_INCREMENT,
`query` varchar(255) NOT NULL,
`keyword` varchar(255) NOT NULL,
PRIMARY KEY (`url_alias_id`),
UNIQUE KEY `uq_keyword` (`keyword`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `oc_tax_class` (
`tax_class_id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(64) NOT NULL,
PRIMARY KEY (`tax_class_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `oc_attribute_description` (
`attribute_id` int(11) NOT NULL,
`language_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`attribute_id`,`language_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `oc_product` (
`product_id` int(11) NOT NULL AUTO_INCREMENT,
`ean` varchar(14) NOT NULL DEFAULT '',
`image` varchar(255) DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `oc_product_description` (
`product_id` int(11) NOT NULL,
`language_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`description` text NOT NULL,
`meta_description` varchar(255) NOT NULL DEFAULT '',
`meta_keyword` varchar(255) NOT NULL DEFAULT '',
`tag` text NOT NULL,
`u_title` varchar(255) NOT NULL DEFAULT '',
`u_h1` varchar(255) NOT NULL DEFAULT '',
`u_h2` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`product_id`,`language_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `oc_product_queue` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL,
`processed` tinyint(1) DEFAULT 0,
`processed_at` datetime DEFAULT NULL,
`result_es` tinyint(1) DEFAULT 0,
`result_en` tinyint(1) DEFAULT 0,
`needs_verify` tinyint(1) DEFAULT 0,
`log` text DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uq_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `oc_category` (`category_id`) VALUES
(1),
(2),
(3);
INSERT INTO `oc_category_description` (`category_id`, `language_id`, `name`) VALUES
(1, 4, 'Despensa ecológica'),
(2, 4, 'Suplementos naturales'),
(3, 4, 'Cuidado personal');
INSERT INTO `oc_category_to_store` (`category_id`, `store_id`) VALUES
(1, 0),
(2, 0),
(3, 0);
INSERT INTO `oc_manufacturer` (`manufacturer_id`, `name`, `image`, `sort_order`) VALUES
(1, 'Herbolario Demo', '', 0),
(2, 'Bio Sample', '', 0),
(3, 'Natural Test Labs', '', 0);
INSERT INTO `oc_manufacturer_to_store` (`manufacturer_id`, `store_id`) VALUES
(1, 0),
(2, 0),
(3, 0);
INSERT INTO `oc_url_alias` (`url_alias_id`, `query`, `keyword`) VALUES
(1, 'manufacturer_id=1', 'herbolario-demo'),
(2, 'manufacturer_id=2', 'bio-sample'),
(3, 'manufacturer_id=3', 'natural-test-labs');
INSERT INTO `oc_tax_class` (`tax_class_id`, `title`) VALUES
(1, 'IVA 4%'),
(2, 'IVA 10%'),
(3, 'IVA 21%');
INSERT INTO `oc_attribute_description` (`attribute_id`, `language_id`, `name`) VALUES
(1, 4, 'bio'),
(2, 4, 'vegano'),
(3, 4, 'sin-gluten'),
(4, 4, 'sin-azucar'),
(5, 4, 'rico-en-fibra'),
(6, 4, 'sin-lactosa'),
(7, 4, 'cruelty-free'),
(8, 4, 'apto-infantil');
INSERT INTO `oc_product` (`product_id`, `ean`, `image`, `status`) VALUES
(1001, '8437000000011', 'catalog/demo/te-verde-demo.jpg', 1),
(1002, '8437000000028', 'catalog/demo/magnesio-demo.jpg', 1),
(1003, '8437000000035', 'catalog/demo/jabon-demo.jpg', 1);
INSERT INTO `oc_product_description` (`product_id`, `language_id`, `name`, `description`, `meta_description`, `meta_keyword`, `tag`, `u_title`, `u_h1`, `u_h2`) VALUES
(1001, 4, 'Té verde ecológico demo', '<p>Infusión demo para pruebas locales.</p>', 'Producto demo para entorno local.', '', '', 'Té verde ecológico demo', 'Té verde ecológico demo', 'Beneficios del té verde demo'),
(1001, 1, 'Demo organic green tea', '<p>Demo infusion for local development.</p>', 'Demo product for local environment.', '', '', 'Demo organic green tea', 'Demo organic green tea', 'Benefits of demo green tea'),
(1002, 4, 'Magnesio natural demo', '<p>Complemento demo para pruebas locales.</p>', 'Suplemento demo para entorno local.', '', '', 'Magnesio natural demo', 'Magnesio natural demo', 'Beneficios del magnesio demo'),
(1002, 1, 'Demo natural magnesium', '<p>Demo supplement for local development.</p>', 'Demo supplement for local environment.', '', '', 'Demo natural magnesium', 'Demo natural magnesium', 'Benefits of demo magnesium'),
(1003, 4, 'Jabón suave demo', '<p>Producto cosmético demo para pruebas locales.</p>', 'Cosmético demo para entorno local.', '', '', 'Jabón suave demo', 'Jabón suave demo', 'Cuidado suave demo'),
(1003, 1, 'Demo gentle soap', '<p>Demo cosmetic product for local development.</p>', 'Demo cosmetic for local environment.', '', '', 'Demo gentle soap', 'Demo gentle soap', 'Gentle care demo');
COMMIT;

View File

@@ -0,0 +1,14 @@
# Legacy PHP module
Important paths:
- `bootstrap.php` — shared config loader
- `config/local.example.php` — copy template
- `config/local.php` — real local values, ignored by git
- `db/conn.php` — shared DB connection helper
- `worker_bulk.php` — CLI worker
Setup:
1. Review `config/README.md`.
2. Fill `config/local.php` with local values.
3. Import `project/sql/db-25052026.sql` into local MariaDB if needed.
4. See `project/sql/README.md` for safe SQL baseline and private data handling.

View File

@@ -0,0 +1,140 @@
<?php
if (defined('LEGACY_MODULE_BOOTSTRAPPED')) {
return;
}
define('LEGACY_MODULE_BOOTSTRAPPED', true);
define('LEGACY_MODULE_ROOT', __DIR__);
define('LEGACY_MODULE_CONFIG_DIR', LEGACY_MODULE_ROOT . '/config');
define('LEGACY_MODULE_LOCAL_CONFIG', LEGACY_MODULE_CONFIG_DIR . '/local.php');
define('LEGACY_MODULE_EXAMPLE_CONFIG', LEGACY_MODULE_CONFIG_DIR . '/local.example.php');
function legacy_default_config() {
return [
'db' => [
'host' => '127.0.0.1',
'port' => 3306,
'database' => '',
'user' => '',
'password' => '',
'charset' => 'utf8',
],
'openai' => [
'api_key' => '',
'model' => 'gpt-4o-mini',
'endpoint' => 'https://api.openai.com/v1/chat/completions',
],
'store' => [
'name' => 'Natural - Mercado de Vida',
'language_es' => 4,
'language_en' => 1,
'image_base_url' => 'https://example.local/image/',
'product_base_url' => 'https://example.local/index.php?route=product/product&product_id=',
],
'routes' => [
'login_url' => '../login.php',
'success_url' => 'https://example.local/producto-nuevo/success.php',
],
'security' => [
'form_password_hash' => '',
],
'paths' => [
'log_dir' => LEGACY_MODULE_ROOT . '/logs',
'worker_log' => LEGACY_MODULE_ROOT . '/logs/worker.log',
'prompt_en' => LEGACY_MODULE_ROOT . '/inc/prompt_en.md',
'prompt_es' => LEGACY_MODULE_ROOT . '/inc/prompt_es.md',
],
'worker' => [
'batch_size' => 20,
'min_html_length' => 500,
],
];
}
function legacy_deep_merge(array $base, array $override) {
foreach ($override as $key => $value) {
if (is_array($value) && isset($base[$key]) && is_array($base[$key])) {
$base[$key] = legacy_deep_merge($base[$key], $value);
continue;
}
$base[$key] = $value;
}
return $base;
}
function legacy_config_file_data($path) {
if (!is_file($path)) {
return [];
}
$data = require $path;
return is_array($data) ? $data : [];
}
function legacy_normalize_config(array $config) {
foreach (['image_base_url', 'product_base_url'] as $key) {
if (isset($config['store'][$key])) {
$config['store'][$key] = trim((string)$config['store'][$key]);
}
}
if (isset($config['routes']['login_url'])) {
$config['routes']['login_url'] = trim((string)$config['routes']['login_url']);
}
if (isset($config['routes']['success_url'])) {
$config['routes']['success_url'] = trim((string)$config['routes']['success_url']);
}
return $config;
}
$legacy_config = legacy_default_config();
$legacy_config_source = LEGACY_MODULE_EXAMPLE_CONFIG;
if (is_file(LEGACY_MODULE_LOCAL_CONFIG)) {
$legacy_config = legacy_deep_merge($legacy_config, legacy_config_file_data(LEGACY_MODULE_LOCAL_CONFIG));
$legacy_config_source = LEGACY_MODULE_LOCAL_CONFIG;
} elseif (is_file(LEGACY_MODULE_EXAMPLE_CONFIG)) {
$legacy_config = legacy_deep_merge($legacy_config, legacy_config_file_data(LEGACY_MODULE_EXAMPLE_CONFIG));
}
$legacy_config = legacy_normalize_config($legacy_config);
function legacy_config_all() {
global $legacy_config;
return $legacy_config;
}
function legacy_config($path, $default = null) {
$value = legacy_config_all();
foreach (explode('.', $path) as $segment) {
if (!is_array($value) || !array_key_exists($segment, $value)) {
return $default;
}
$value = $value[$segment];
}
return $value;
}
function legacy_config_source() {
global $legacy_config_source;
return $legacy_config_source;
}
function legacy_new_mysqli() {
$host = legacy_config('db.host', '127.0.0.1');
$user = legacy_config('db.user', '');
$password = legacy_config('db.password', '');
$database = legacy_config('db.database', '');
$port = (int) legacy_config('db.port', 3306);
$charset = legacy_config('db.charset', 'utf8');
$db = new mysqli($host, $user, $password, $database, $port);
if (!$db->connect_errno && $charset) {
$db->set_charset($charset);
}
return $db;
}

View File

@@ -0,0 +1,17 @@
# Local config setup
1. Copy `local.example.php` to `local.php`.
2. Fill real local DB, OpenAI, and URL values in `local.php`.
3. Keep `local.php` out of git.
Config keys used by the legacy module:
- `db.*`
- `openai.*`
- `store.*`
- `routes.*`
- `security.form_password_hash`
- `worker.*`
The module loads:
- real local values from `config/local.php`
- safe fallback values from `config/local.example.php`

View File

@@ -0,0 +1,35 @@
<?php
return [
'db' => [
'host' => '127.0.0.1',
'port' => 3306,
'database' => 'CHANGE_ME_DB_NAME',
'user' => 'CHANGE_ME_DB_USER',
'password' => 'CHANGE_ME_DB_PASSWORD',
'charset' => 'utf8',
],
'openai' => [
'api_key' => 'CHANGE_ME_OPENAI_API_KEY',
'model' => 'gpt-4o-mini',
'endpoint' => 'https://api.openai.com/v1/chat/completions',
],
'store' => [
'name' => 'Natural - Mercado de Vida',
'language_es' => 4,
'language_en' => 1,
'image_base_url' => 'https://example.local/image/',
'product_base_url' => 'https://example.local/index.php?route=product/product&product_id=',
],
'routes' => [
'login_url' => '../login.php',
'success_url' => 'https://example.local/producto-nuevo/success.php',
],
'security' => [
'form_password_hash' => 'CHANGE_ME_FORM_PASSWORD_HASH',
],
'worker' => [
'batch_size' => 20,
'min_html_length' => 500,
],
];

View File

@@ -0,0 +1,396 @@
option:disabled {
font-weight: bolder;
color: #FF5722;
}
#atrib_zone, #edit_zone {
border: #ccc 3px dashed;
padding: 15px 15px 0px 15px;
}
.ck-editor__editable_inline {
min-height: 250px;
}
input[name=modelo], input[name=url] {
pointer-events: none;
background: whitesmoke;
color: #FF5722;
font-weight: bolder;
}
#imgPreview {
text-align: center;
padding-bottom: 7px;
}
.infoText {
font-size: small;
font-weight: bolder;
color: #FF5722;
}
input[type="file"] {
display: none;
}
.uploadImg {
border: 1px solid #ccc;
/*display: inline-block;*/
padding: 20px 12px 6px 12px;
cursor: pointer;
text-align: center;
height: 37px;
border-radius: 4px;
}
#imgElement {
max-width: 170px;
max-height: 120px;
}
#crearProducto {
height: 110px;
}
#loading {
display: none;
}
#ia_link {
/* display: none; */
background-color: #FFC107;
border-color: #FFC107;
}
/* Definimos la animación de parpadeo */
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
/* Aplicamos la animación al elemento */
#ia_link {
animation: blink 2s ease-in-out infinite;
transition: opacity 0.5s ease-in-out;
}
/* Agregamos un efecto de transición suave */
#ia_link:hover {
opacity: 0.8;
}
/* ============================================================
Natural - Mercado de Vida
Panel de administración SEO y descripciones
Complemento para Skeleton Boilerplate
============================================================ */
/* ---------- Variables ---------- */
:root {
--color-primary: #0074D9;
--color-success: #28a745;
--color-danger: #dc3545;
--color-warning: #ffae00;
--color-light: #f4f4f4;
--color-muted: #666;
}
/* ---------- Contenedores ---------- */
.container {
max-width: 1100px;
margin: 0 auto;
padding: 30px 0 50px;
}
/* ---------- Tablas ---------- */
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
background: #fff;
box-shadow: 0 0 3px rgba(0,0,0,0.05);
border: 1px solid #ddd;
}
th, td {
border: 1px solid #ddd;
padding: 10px 10px;
vertical-align: top;
}
th {
background: var(--color-light);
text-align: left;
color: #222;
}
tr:hover td {
background: #f9f9f9;
}
/* ---------- Imágenes ---------- */
td img {
width: 140px;
border-radius: 6px;
display: block;
margin: 0 auto 8px;
}
/* ---------- Textos de idiomas ---------- */
.lang-title {
font-weight: bold;
color: var(--color-primary);
margin-bottom: 4px;
}
.lang-section {
line-height: 1.5em;
font-size: 1rem;
text-align: justify;
}
/* ---------- Badges ---------- */
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: .85rem;
color: #fff;
margin-left: 6px;
}
.badge.ok {
background: var(--color-success);
}
.badge.miss {
background: var(--color-danger);
}
.badge.warn {
background: var(--color-warning);
color: #000;
}
/* ---------- Botones ---------- */
.button-primary,
button.button-primary {
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all .2s ease;
}
.button-primary:hover {
opacity: 0.9;
}
/* Botón de verificación manual */
.toggle-btn {
background: var(--color-warning);
color: #000;
border: none;
border-radius: 5px;
padding: 5px 12px;
cursor: pointer;
font-size: 0.9rem;
margin-top: 6px;
transition: all .2s ease-in-out;
}
.toggle-btn.active {
background: var(--color-success);
color: #fff;
}
/* Toolbar superior */
.toolbar {
margin: 10px 0 15px;
}
.toolbar button {
margin-right: 8px;
}
/* ---------- Caja de ID y estado ---------- */
.idbox {
font-size: .9rem;
color: #333;
text-align: center;
}
.idbox small {
color: var(--color-muted);
}
.filter-bar {
margin-bottom: 20px;
}
.filter-actions a {
display: inline-block;
margin-top: 8px;
}
.alert-box {
background: #f5f8fb;
border: 1px solid #d9e3ec;
border-radius: 6px;
padding: 18px;
}
.table-description {
max-height: 120px;
overflow: auto;
}
.log-panel {
margin-top: 20px;
background: #f9f9f9;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.log-panel__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
border-bottom: 1px solid #e0e0e0;
}
.log-panel__status {
font-size: 0.95rem;
color: #666;
}
.log-panel__body {
margin: 0;
padding: 14px;
max-height: 360px;
overflow: auto;
font-size: 1.3rem;
line-height: 1.45;
background: #111;
color: #f1f1f1;
}
.pagination-row {
margin-top: 18px;
text-align: center;
}
.pagination-row .button {
margin: 0 4px;
}
.pagination-row .button.disabled {
pointer-events: none;
opacity: .4;
}
.pagination-row .pagination-meta {
margin: 0 12px;
color: var(--color-muted);
}
/* ---------- Loader ---------- */
.loader-container {
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 30px;
}
.loader {
border: 5px solid #eee;
border-top: 5px solid var(--color-primary);
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 10px;
font-weight: bold;
color: #333;
animation: fadeText 1.5s infinite;
}
@keyframes fadeText {
0%,100% { opacity: 0.2; }
50% { opacity: 1; }
}
/* ---------- Paginación ---------- */
.pagination {
text-align: center;
margin-top: 25px;
}
.pagination a {
display: inline-block;
padding: 6px 12px;
margin: 0 4px;
text-decoration: none;
border-radius: 4px;
background: var(--color-primary);
color: #fff;
}
.pagination a.disabled {
background: #ccc;
pointer-events: none;
}
.pagination span {
display: inline-block;
margin: 0 10px;
font-weight: bold;
}
/* ---------- Responsive ---------- */
@media (max-width: 768px) {
td img { width: 100px; }
table, th, td { font-size: 1.3rem; }
.toolbar { text-align: center; }
.lang-section { font-size: 1.3rem;
text-align: justify;}
}
.last {
margin: 10px 0px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
margin-top: 26px;
}
.checkbox-label .label-body {
margin: 0;
}
/* Alineación vertical suave y estética Skeleton */
#missing {
position: relative;
top: 2px;
margin-right: 5px;
}
label[for="missing"] {
font-weight: 500;
cursor: pointer;
display: inline-block;
color: #555;
}

View File

@@ -0,0 +1,39 @@
/* The Modal (background) */
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
padding-top: 100px; /* Location of the box */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
/* Modal Content */
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 20px;
border: 1px solid #888;
max-width: 750px;
width: 70%;
}
/* The Close Button */
.close {
color: #aaaaaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: #000;
text-decoration: none;
cursor: pointer;
}

427
project/web/index/new/css/normalize.css vendored Normal file
View File

@@ -0,0 +1,427 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

427
project/web/index/new/css/skeleton.css vendored Normal file
View File

@@ -0,0 +1,427 @@
/*
* Skeleton V2.0.4
* Copyright 2014, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
* 12/29/2014
*/
/* Table of contents
- Grid
- Base Styles
- Typography
- Links
- Buttons
- Forms
- Lists
- Code
- Tables
- Spacing
- Utilities
- Clearing
- Media Queries
*/
/* Grid
*/
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box; }
.column,
.columns {
width: 100%;
float: left;
box-sizing: border-box; }
/* For devices larger than 400px */
@media (min-width: 400px) {
.container {
width: 85%;
padding: 0; }
}
/* For devices larger than 550px */
@media (min-width: 550px) {
.container {
width: 80%; }
.column,
.columns {
margin-left: 4%; }
.column:first-child,
.columns:first-child {
margin-left: 0; }
.one.column,
.one.columns { width: 4.66666666667%; }
.two.columns { width: 13.3333333333%; }
.three.columns { width: 22%; }
.four.columns { width: 30.6666666667%; }
.five.columns { width: 39.3333333333%; }
.six.columns { width: 48%; }
.seven.columns { width: 56.6666666667%; }
.eight.columns { width: 65.3333333333%; }
.nine.columns { width: 74.0%; }
.ten.columns { width: 82.6666666667%; }
.eleven.columns { width: 91.3333333333%; }
.twelve.columns { width: 100%; margin-left: 0; }
.one-third.column { width: 30.6666666667%; }
.two-thirds.column { width: 65.3333333333%; }
.one-half.column { width: 48%; }
/* Offsets */
.offset-by-one.column,
.offset-by-one.columns { margin-left: 8.66666666667%; }
.offset-by-two.column,
.offset-by-two.columns { margin-left: 17.3333333333%; }
.offset-by-three.column,
.offset-by-three.columns { margin-left: 26%; }
.offset-by-four.column,
.offset-by-four.columns { margin-left: 34.6666666667%; }
.offset-by-five.column,
.offset-by-five.columns { margin-left: 43.3333333333%; }
.offset-by-six.column,
.offset-by-six.columns { margin-left: 52%; }
.offset-by-seven.column,
.offset-by-seven.columns { margin-left: 60.6666666667%; }
.offset-by-eight.column,
.offset-by-eight.columns { margin-left: 69.3333333333%; }
.offset-by-nine.column,
.offset-by-nine.columns { margin-left: 78.0%; }
.offset-by-ten.column,
.offset-by-ten.columns { margin-left: 86.6666666667%; }
.offset-by-eleven.column,
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
.offset-by-one-third.column,
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
.offset-by-two-thirds.column,
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
.offset-by-one-half.column,
.offset-by-one-half.columns { margin-left: 52%; }
}
/* Base Styles
*/
/* NOTE
html is set to 62.5% so that all the REM measurements throughout Skeleton
are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
font-size: 62.5%; }
body {
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
line-height: 1.6;
font-weight: 400;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #222; }
/* Typography
*/
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 2rem;
font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
/* Larger than phablet */
@media (min-width: 550px) {
h1 { font-size: 5.0rem; }
h2 { font-size: 4.2rem; }
h3 { font-size: 3.6rem; }
h4 { font-size: 3.0rem; }
h5 { font-size: 2.4rem; }
h6 { font-size: 1.5rem; }
}
p {
margin-top: 0; }
/* Links
*/
a {
color: #1EAEDB; }
a:hover {
color: #0FA0CE; }
/* Buttons
*/
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
display: inline-block;
height: 38px;
padding: 0 30px;
color: #555;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 38px;
letter-spacing: .1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
color: #333;
border-color: #888;
outline: 0; }
.button.button-primary,
button.button-primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
color: #FFF;
background-color: #70ad47;
border-color: #70ad47; }
.button.button-primary:hover,
button.button-primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
color: #FFF;
background-color: #70ad47de;
border-color: #70ad47de; }
/* Forms
*/
input[type="email"],
input[type="number"],
input[type="date"],
input[type="search"],
input[type="text"],
input[type="color"],
input[type="tel"],
input[type="url"],
input[type="password"],
input[type="file"],
textarea,
select {
height: 38px;
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
background-color: #fff;
border: 1px solid #D1D1D1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="date"],
input[type="search"],
input[type="text"],
input[type="color"],
input[type="tel"],
input[type="url"],
input[type="password"],
input[type="file"],
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none; }
textarea {
min-height: 65px;
padding-top: 6px;
padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="date"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="color"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
input[type="file"]:focus,
textarea:focus,
select:focus {
border: 1px solid #70ad47;
outline: 0; }
label,
legend {
display: block;
margin-bottom: .5rem;
font-weight: 600; }
fieldset {
padding: 0;
border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
display: inline; }
label > .label-body {
display: inline-block;
margin-left: .5rem;
font-weight: normal; }
/* Lists
*/
ul {
list-style: circle inside; }
ol {
list-style: decimal inside; }
ol, ul {
padding-left: 0;
margin-top: 0; }
ul ul,
ul ol,
ol ol,
ol ul {
margin: 1.5rem 0 1.5rem 3rem;
font-size: 90%; }
li {
margin-bottom: 1rem; }
/* Code
*/
code {
padding: .2rem .5rem;
margin: 0 .2rem;
font-size: 90%;
white-space: nowrap;
background: #F1F1F1;
border: 1px solid #E1E1E1;
border-radius: 4px; }
pre > code {
display: block;
padding: 1rem 1.5rem;
white-space: pre; }
/* Tables
*/
th,
td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #E1E1E1; }
th:first-child,
td:first-child {
padding-left: 0; }
th:last-child,
td:last-child {
padding-right: 0; }
/* Spacing
*/
button,
.button {
margin-bottom: 1rem; }
input,
textarea,
select,
fieldset {
margin-bottom: 1.5rem; }
pre,
blockquote,
dl,
figure,
table,
p,
ul,
ol,
form {
margin-bottom: 2.5rem; }
/* Utilities
*/
.u-full-width {
width: 100%;
box-sizing: border-box; }
.u-max-full-width {
max-width: 100%;
box-sizing: border-box; }
.u-pull-right {
float: right; }
.u-pull-left {
float: left; }
/* Misc
*/
hr {
margin-top: 3rem;
margin-bottom: 3.5rem;
border-width: 0;
border-top: 1px solid #E1E1E1; }
/* Clearing
*/
/* Self Clearing Goodness */
.container:after,
.row:after,
.u-cf {
content: "";
display: table;
clear: both; }
/* Media Queries
*/
/*
Note: The best way to structure the use of media queries is to create the queries
near the relevant code. For example, if you wanted to change the styles for buttons
on small devices, paste the mobile query code up in the buttons section and style it
there.
*/
/* Larger than mobile */
@media (min-width: 400px) {}
/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {}
/* Larger than tablet */
@media (min-width: 750px) {}
/* Larger than desktop */
@media (min-width: 1000px) {}
/* Larger than Desktop HD */
@media (min-width: 1200px) {}

View File

@@ -0,0 +1,187 @@
@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');
@import url('https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css');
*{ margin: 0; padding: 0;}
body{
font-family: 'Roboto', sans-serif;
font-style: normal;
font-weight: 300;
font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 15px;
background: #eee;
}
.intro{
background: #fff;
padding: 60px 30px;
color: #333;
margin-bottom: 15px;
line-height: 1.5;
text-align: center;
}
.intro h1 {
font-size: 18pt;
padding-bottom: 15px;
}
.intro p{
font-size: 14px;
}
.action{
text-align: center;
display: block;
margin-top: 20px;
}
a.btn {
text-decoration: none;
color: #666;
border: 2px solid #666;
padding: 10px 15px;
display: inline-block;
margin-left: 5px;
}
a.btn:hover{
background: #666;
color: #fff;
transition: .3s;
-webkit-transition: .3s;
}
.btn:before{
font-family: FontAwesome;
font-weight: normal;
margin-right: 10px;
}
.github:before{content: "\f09b"}
.down:before{content: "\f019"}
.back:before{content:"\f112"}
.credit{
background: #fff;
padding: 12px;
font-size: 9pt;
text-align: center;
color: #333;
margin-top: 40px;
}
.credit span:before{
font-family: FontAwesome;
color: #e41b17;
content: "\f004";
}
.credit a{
color: #333;
text-decoration: none;
}
.credit a:hover{
color: #1DBF73;
}
.credit a:hover:after{
font-family: FontAwesome;
content: "\f08e";
font-size: 9pt;
position: absolute;
margin: 3px;
}
main{
background: #fff;
padding:: 20px;
}
article li{
color: #444;
font-size: 15px;
margin-left: 33px;
line-height: 1.5;
padding: 5px;
}
article h1,
article h2,
article h3,
article h4,
article p{
padding: 14px;
color: #333;
}
article p{
font-size: 15px;
line-height: 1.5;
}
@media only screen and (min-width: 720px){
main{
max-width: 720px;
margin-left: auto;
margin-right: auto;
padding: 24px;
}
}
.set-overlayer,
.set-glass,
.set-sticky {
cursor: pointer;
height: 45px;
line-height: 45px;
padding: 0 15px;
color: #333;
font-size: 16px;
}
.set-overlayer:after,
.set-glass:after,
.to-active:after,
.set-sticky:after {
font-family: FontAwesome;
font-size: 18pt;
position: relative;
float: right;
}
.set-overlayer:after,
.set-glass:after,
.set-sticky:after {
content: "\f204";
transition: .6s;
}
.to-active:after {
content: "\f205";
color: #008080;
transition: .6s;
}
.set-overlayer,
.set-glass,
.set-sticky,
.source,
.theme-tray {
margin: 10px;
background: #f2f2f2;
border-radius: 5px;
border: 2px solid #f1f1f1;
box-sizing: border-box;
}
/* Syntax Highlighter*/
pre.prettyprint {
padding: 15px !important;
margin: 10px;
border: 0 !important;
background: #f2f2f2;
overflow: auto;
}
.source {
white-space: pre;
overflow: auto;
max-height: 400px;
}
code{
border:1px solid #ddd;
padding: 2px;
border-radius: 2px;
}

View File

@@ -0,0 +1,11 @@
<?php
require_once dirname(__DIR__) . '/bootstrap.php';
$con = legacy_new_mysqli();
if ($con->connect_errno) {
echo 'Failed to connect to MySQL.';
}
?>

View File

@@ -0,0 +1,269 @@
<?php include ('./inc/header.php'); ?>
<?php
ini_set('max_execution_time', 120);
set_time_limit(120);
$producto = "";
$ean = "";
$autoGenerar = false;
$formato = isset($_POST['formato']) ? $_POST['formato'] : 'plano';
// Si viene por URL
if (isset($_GET['name']) && !empty($_GET['name'])) {
$producto = urldecode($_GET['name']);
$autoGenerar = true;
}
// Si viene por POST
if (isset($_POST['prompt'])) {
$producto = trim($_POST['prompt']);
$autoGenerar = true;
}
// Detectar EAN (8 a 13 dígitos seguidos)
if (preg_match('/\b\d{8,13}\b/', $producto, $matches)) {
$ean = $matches[0];
}
if ($autoGenerar && !empty($producto)) {
// Prompt según formato seleccionado
if ($formato === 'plano') {
$prompt = "
Eres un redactor SEO experto en productos naturales, ecológicos y saludables.
Genera una descripción en texto plano sin formato ni negritas ni guiones, lista para pegar en CKEditor.
Sobre el producto: \"$producto\"
Instrucciones:
1. Prioriza la información del fabricante o distribuidor oficial.
2. Si el producto tiene un código EAN, utilízalo para obtener información nutricional en OpenFoodFacts.
3. No menciones ni enlaces fuentes externas.
4. No inventes datos no verificables.
Estructura del texto:
Descripción:
Ingredientes:
Información nutricional (por 100 g):
Beneficios para la salud:
Por qué deberías probarlo:
Keywords:
Reglas:
- No uses HTML, JSON ni emojis.
- No repitas el nombre del producto en exceso.
";
} else {
$prompt = "
Eres un redactor SEO especializado en productos naturales y saludables.
Genera una descripción optimizada, estructurada y lista para publicación web.
Producto: \"$producto\"
Prioriza datos del fabricante o distribuidor oficial, y si el producto tiene un código EAN, usa OpenFoodFacts para complementar la información nutricional.
No incluyas ni menciones enlaces externos, ni nombres de tiendas online o marketplaces.
Estructura del texto:
### Descripción
Breve texto atractivo sobre origen, uso y beneficios.
### Ingredientes
Lista completa y verificada.
### Información nutricional (por 100 g)
Calorías, grasas, hidratos, azúcares, proteínas, fibra y sal. Indica si provienen del fabricante o fuente pública.
### Beneficios para la salud
Texto breve explicando las propiedades y usos.
### Por qué deberías probarlo
Cierre aspiracional, natural y coherente.
### Keywords
Lista de términos relevantes separados por comas.
Reglas:
- No uses HTML ni JSON.
- No uses emojis.
- No menciones fuentes ni enlaces externos.
";
}
$respuesta = obtener_respuesta($prompt);
// Detectar enlaces válidos (solo OpenFoodFacts o fabricante)
$fuentes = [];
// --- Verificar si el EAN existe realmente en OpenFoodFacts ---
if (!empty($ean)) {
$url_off = "https://world.openfoodfacts.org/product/$ean";
$headers = @get_headers($url_off);
if ($headers && strpos($headers[0], '200') !== false) {
$fuentes[] = "Información verificada en OpenFoodFacts: $url_off";
}
}
// --- Verificar si existe ficha oficial del fabricante (ejemplo: Terpenic Labs) ---
$producto_slug = strtolower(str_replace(' ', '-', $producto));
$url_fabricante = "https://www.terpenic.com/product-page/" . urlencode($producto_slug);
$headers = @get_headers($url_fabricante);
if ($headers && strpos($headers[0], '200') !== false) {
$fuentes[] = "Ficha oficial del fabricante: $url_fabricante";
}
// --- Agregar los enlaces válidos al final del texto (copiable) ---
if (!empty($fuentes)) {
$respuesta .= "\n\n" . implode("\n", $fuentes);
}
}
function obtener_respuesta($prompt) {
$apiKey = trim((string) legacy_config('openai.api_key', ''));
$model = legacy_config('openai.model', 'gpt-4o-mini');
$endpoint = legacy_config('openai.endpoint', 'https://api.openai.com/v1/chat/completions');
if ($apiKey === '' || strpos($apiKey, 'CHANGE_ME_') === 0) {
return "⚠️ Configura openai.api_key en config/local.php.";
}
$ch = curl_init($endpoint);
$data = array(
'model' => $model,
'messages' => array(
array('role' => 'system', 'content' => 'Eres un redactor SEO experto en e-commerce.'),
array('role' => 'user', 'content' => $prompt)
),
'temperature' => 0.6,
'max_tokens' => 1200
);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
));
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
$result = curl_exec($ch);
if (curl_errno($ch)) {
return "⚠️ Error de conexión: " . curl_error($ch);
}
$http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_status !== 200) {
return "⚠️ Error HTTP $http_status — el servidor no respondió correctamente.";
}
$response = json_decode($result, true);
if (isset($response['choices'][0]['message']['content'])) {
return $response['choices'][0]['message']['content'];
} else {
return "⚠️ No se pudo generar respuesta. Detalle:\n" . json_encode($response, JSON_PRETTY_PRINT);
}
}
?>
<style>
.loader-container {
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 30px;
}
.loader {
border: 5px solid #f3f3f3;
border-top: 5px solid #0074D9;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg);} 100% { transform: rotate(360deg);} }
.loading-text {
margin-top: 10px;
font-weight: bold;
font-size: 1.1rem;
color: #333;
animation: fadeText 1.5s infinite;
}
@keyframes fadeText {
0%,100% {opacity: 0.2;} 50% {opacity: 1;}
}
</style>
<div class="container">
<form method="POST" onsubmit="showLoader()">
<div class="row" style="margin-top: 20px">
<div class="ten columns">
<input class="u-full-width" type="text" name="prompt" id="prompt"
placeholder="Ingresa el nombre del producto"
value="<?php echo htmlspecialchars($producto); ?>">
</div>
<div class="two columns">
<button class="button button-primary u-full-width" type="submit">Generar</button>
</div>
</div>
<div class="row" style="margin-top:10px;">
<label>
<input type="checkbox" name="formato" value="formato" <?php echo ($formato==='formato')?'checked':''; ?>>
<span>Con formato para SEO</span>
</label>
</div>
</form>
<div id="loader" class="loader-container">
<div class="loader"></div>
<div class="loading-text">Generando contenido...</div>
</div>
<?php if ($autoGenerar && !isset($respuesta)) { ?>
<script>document.addEventListener("DOMContentLoaded",()=>{document.getElementById("loader").style.display="flex";});</script>
<?php } ?>
<?php if (isset($respuesta)) { ?>
<script>document.addEventListener("DOMContentLoaded",()=>{document.getElementById("loader").style.display="none";});</script>
<div class="row" style="margin-top:20px;">
<div class="twelve columns">
<pre id="texto-copiar" style="background:#f9f9f9;padding:15px;border-radius:6px;white-space:pre-wrap;font-size:1rem;"><?php echo htmlspecialchars($respuesta); ?></pre>
</div>
</div>
<div class="row" style="margin-top:10px;">
<div class="two columns">
<button class="button button-primary u-full-width" onclick="refreshPage()">
<i class="fa fa-refresh fa-lg"></i>
</button>
</div>
<div class="ten columns">
<button class="u-full-width" onclick="copiarTexto()">Copiar texto</button>
</div>
</div>
<?php } ?>
</div>
<script>
function showLoader(){document.getElementById("loader").style.display="flex";}
function copiarTexto(){
var e=document.getElementById("texto-copiar");
var t=e.innerText;
navigator.clipboard.writeText(t);
e.style.backgroundColor="#ffffa0";
setTimeout(()=>{e.style.backgroundColor="";alert("Texto copiado al portapapeles");},500);
}
function refreshPage(){location.reload();}
</script>
<?php include ('./inc/footer.php'); ?>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -0,0 +1,27 @@
<fieldset>
<?php
$atributos = mysqli_query($con," SELECT `oc_attribute_description`.`attribute_id` , `oc_attribute_description`.`language_id` , `oc_attribute_description`.`name` FROM `oc_attribute_description` WHERE `oc_attribute_description`.`language_id` = '4' ORDER BY `oc_attribute_description`.`name`");
$grupo = 0;
$row_count = mysqli_num_rows($atributos);
//echo $row_count;
while ($row = mysqli_fetch_assoc($atributos))
{
if ($grupo == 0) {
echo '<div class="three columns">';
}
echo '<input type="checkbox" name="atributos[]" value="'. $row['attribute_id'] .'" > &nbsp;' . ucwords(str_replace("-"," ", $row['name'])) . ' &nbsp; <br>';
$grupo++;
if ($grupo == 4) {
echo '</div>';
$grupo = 0;
}
}
?>
</fieldset>

View File

@@ -0,0 +1,184 @@
<!-- SCRIPTS JS
-->
<!-- EDITOR CKEDITOR
-->
<script src="https://code.jquery.com/jquery.min.js"></script>
<script src="https://cdn.ckeditor.com/ckeditor5/22.0.0/classic/ckeditor.js"></script>
<script>
ClassicEditor
.create( document.querySelector( '#editor' ) )
.then( editor => {
console.log( editor );
} )
.catch( error => {
console.error( error );
} );
</script>
<!-- SEO URL
-->
<script src="./js/slugify.js"></script>
<script>
$(document).ready(function(){
$('#slug,#slug-span').slugify('#slug-source');
});
</script>
<!-- CONVIERTE TEXTO A MAYÚSCULAS
-->
<script>
$(".text-uppercase").keyup(function () {
this.value = this.value.toLocaleUpperCase();
this.value = this.value.replace(/['"]+/g, '');
});
</script>
<!-- CONVIERTE TEXTO A MINÚSCULAS
-->
<script>
$(".text-lowercase").keyup(function () {
this.value = this.value.toLocaleLowerCase();
this.value = this.value.replace(/['"]+/g, '');
});
</script>
<!-- AJAX - REFRESCA LISTA DE MARCAS
-->
<script>
function refreshBrand()
{
$.ajax({
url: './inc/marcas.php',
type: 'post',
success: function(data) {
$('.newbrand').html(data);
}
});
}
</script>
<!-- VENTANA MODAL CREAR NUEVA MARCA
-->
<script>
// GET THE MODAL
var modal = document.getElementById("marcanueva");
// GET THE BUTTON THAT OPENS THE MODAL
var btn = document.getElementById("newbrand");
// GET THE <SPAN> ELEMENT THAT CLOSES THE MODAL
var span = document.getElementsByClassName("close")[0];
// WHEN THE USER CLICKS THE BUTTON, OPEN THE MODAL
btn.onclick = function() {
modal.style.display = "block";
}
// WHEN THE USER CLICKS ON <SPAN> (X), CLOSE THE MODAL
span.onclick = function() {
modal.style.display = "none";
}
// WHEN THE USER CLICKS ANYWHERE OUTSIDE OF THE MODAL, CLOSE IT
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
}
}
// WHEN THE USER CLICKS THE BUTTON <GUARDAR>, CLOSE THE MODAL
function cerrarModal(idModal) {
var idModal = idModal;
var modala = document.getElementById(idModal);
modala.style.display = "none";
return false;
}
</script>
<!-- AJAX - CREAR NUEVA MARCA
-->
<script>
function createBrand()
{
var nombreMarca= $("#nombreMarca").val();
$.ajax({
url: './inc/newmarca.php',
type: 'post',
data: "nombreMarca=" + nombreMarca,
beforeSend: function(){
$('#guardarMarca').hide();
$('#loading').show();
},
complete: function(){
$('#loading').hide();
$('#guardarMarca').show();
cerrarModal('marcanueva');
},
success: function(data) {
refreshBrand();
}
});
}
</script>
<!-- ENTER - STOP SUBMITTING FORM
-->
<script>
document.getElementById("newproduct").onkeypress = function(e) {
var key = e.charCode || e.keyCode || 0;
if (key == 13) {
e.preventDefault();
}
}
</script>
<!-- INPUT 'DATE' PARA SAFARI
-->
<script src="https://cdn.jsdelivr.net/webshim/1.12.4/extras/modernizr-custom.js"></script>
<script src="https://cdn.jsdelivr.net/webshim/1.12.4/polyfiller.js"></script>
<script>
webshims.setOptions('waitReady', false);
webshims.setOptions('forms-ext', {type: 'date'});
webshims.setOptions('forms-ext', {type: 'time'});
webshims.polyfill('forms forms-ext');
</script>
<script>
function setLink() {
var nameValue = document.getElementById("slug-source").value;
var brandSelect = document.getElementById("marca");
var brandValue = brandSelect.options[brandSelect.selectedIndex].text;
var url = "./describe.php?name=" + encodeURIComponent(nameValue) + " " + encodeURIComponent(brandValue);
document.getElementById("ia_link").href = url;
}
function validateSelect() {
var brandSelect = document.getElementById("marca");
var ia_link = document.getElementById("ia_link");
if (brandSelect.value) {
ia_link.style.display = "block";
var element = document.getElementById("url_div");
element.className = "ten columns";
} else {
ia_link.style.display = "none";
}
}
</script>
<!-- FIN SCRIPTS JS
-->
</body>
</html>

View File

@@ -0,0 +1,49 @@
<?php
require_once dirname(__DIR__) . '/bootstrap.php';
session_start();
$_SESSION['after_login'] = $_SERVER['REQUEST_URI'];
$_SESSION['acceso'] = TRUE;
if (empty($_SESSION['logged'])) {
header('Location: ' . legacy_config('routes.login_url', '../login.php'));
exit;
}
require_once dirname(__DIR__) . '/db/conn.php';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<!-- Basic Page Needs
-->
<meta charset="utf-8">
<title>Nuevo Producto</title>
<meta name="description" content="Carga rápida de productos - Natural - Mercado de Vida">
<meta name="author" content="rikrdo.es">
<!-- Mobile Specific Metas
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- FONT
-->
<link href="https://fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous">
<!-- CSS
-->
<link rel="stylesheet" href="./css/normalize.css?<?php echo filemtime('./css/normalize.css') ?>" type="text/css">
<link rel="stylesheet" href="./css/skeleton.css?<?php echo filemtime('./css/skeleton.css') ?>" type="text/css">
<link rel="stylesheet" href="./css/modal.css?<?php echo filemtime('./css/modal.css') ?>" type="text/css">
<link rel="stylesheet" href="./css/custom.css?<?php echo filemtime('./css/custom.css') ?>" type="text/css">
<!-- Favicon
-->
<link rel="icon" type="image/png" href="../images/favicon.png">
</head>
<body>

View File

@@ -0,0 +1,12 @@
<?php require ('../db/conn.php');?>
<select name="marca" class="u-full-width" id="marca" required>
<option value="" selected disabled>MARCA</option>
<?php
$marcas = mysqli_query($con," SELECT `oc_manufacturer`.`manufacturer_id` , `oc_manufacturer`.`name` FROM `oc_manufacturer` ORDER BY `oc_manufacturer`.`name`");
while ($row = mysqli_fetch_assoc($marcas))
{
echo '<option value="'. $row['manufacturer_id'] .'" >' . $row['name'] . ' </option>';
}
?>
</select>

View File

@@ -0,0 +1,31 @@
<?php
require ('../db/conn.php');
$nombreMarca=trim($_POST['nombreMarca']);
setlocale(LC_ALL, 'en_US.UTF8'); // NECESARIO PARA CONVERTIR CARACTERES ESPECIALES AL ALFABETO INGLÉS
$urlMarca = htmlspecialchars($nombreMarca);
$urlMarca = html_entity_decode($urlMarca);
$urlMarca = strip_tags($urlMarca);
$urlMarca = preg_replace("/[^a-z0-9 ]/i", " ", iconv('UTF-8', 'ASCII//TRANSLIT', $urlMarca)); // ELIMINA CARACTERES ESPECIALES
$urlMarca = preg_replace("/\s+/", " ", $urlMarca); // QUITA DOBLE ESPACIO
$urlMarca = trim($urlMarca); // QUITA ESPACIO AL INICIO Y AL FINAL
$urlMarca = strtolower(str_replace(" ", "-", $urlMarca)); // REEMPLAZA ESPACIO POR GUION
$insert_manufacturer = "INSERT INTO `oc_manufacturer` (`manufacturer_id`,`name`, `image`, `sort_order`) VALUES (NULL , '". $nombreMarca ."', '', 0)";
if (mysqli_query($con, $insert_manufacturer))
{
$last_id = mysqli_insert_id($con);
mysqli_query($con, "INSERT INTO `oc_manufacturer_to_store` (`manufacturer_id`, `store_id`) VALUES (". $last_id .", 0)");
mysqli_query($con, "INSERT INTO `oc_url_alias` (`url_alias_id`, `query`, `keyword`)
VALUES (NULL , 'manufacturer_id=" . $last_id . "' , '" . $urlMarca . "')");
}
else
{
echo "<code>Error: " . $insert_manufacturer . "</code><br>" . mysqli_error($con);
}
mysqli_close($con);
?>

View File

@@ -0,0 +1,50 @@
You are an expert SEO copywriter specialized in natural, organic, and healthy products.
Produce an HTML-formatted description that feels authoritative and practical.
Use **bold** text and lists when helpful; keep emojis limited to the section labels provided below.
Product name: "$producto"
EAN (if any): "$ean"
General rules:
- Prioritize information from the official manufacturer or distributor.
You may consult these sites only as silent references (never mention or link them): nutritienda.com, veritas.es, naturitas.es, iherb.com, dietisur.es, openfoodfacts.org.
- Do not invent unverifiable facts. When data is missing, state that it is pending confirmation from the manufacturer.
- Only reference external sources if they are the official manufacturer or (when appropriate) OpenFoodFacts.
- Avoid `<h1>`, `<h2>`, `<h3>` tags. Stick to `<p>`, `<b>`, `<ul>`, `<li>`, `<h4>`, `<i>`, `<br>`.
- Never wrap the answer in Markdown code fences (` ``` `) or any kind of code block; return plain HTML only.
- Keep the tone informative, natural, and aligned with *Natural Mercado de Vida*.
- When an EAN is provided, query the OpenFoodFacts API `https://world.openfoodfacts.net/api/v2/product/{EAN}`.
* If the response indicates the product is missing or lacks meaningful fields (status ≠ 1 or no name/ingredients/nutrition), do **not** include the OpenFoodFacts link and note that information is pending verification.
* Only add the public UI link (`https://world.openfoodfacts.org/product/{EAN}`) when the API returns substantial data for that product.
Determine the product category before writing:
1. **Edible products / beverages / dietary supplements:** include ingredients and nutritional information if available.
2. **Topical cosmetics or hygiene items (creams, soaps, etc.):** mention key ingredients or active compounds only when they are typically disclosed; otherwise explain that the composition is pending verification. No nutritional data.
3. **Household products (cleaners, detergents, etc.):** focus on functional ingredients or key features; no nutritional information.
4. **Accessories, utensils, equipment, grooming tools (e.g., combs, bottles, blenders):** skip ingredients and nutritional information entirely. Highlight materials, design details, usage tips, and care instructions.
5. If the product type does not fit any of the above, use judgment and omit sections that are clearly irrelevant.
Section guidelines (adapt as needed based on the category analysis):
🪴 **Description:**
Concise overview of purpose, origin, and main benefits. Always include this section.
🌿 **Ingredients / Key components:**
- Include only when the product category reasonably has a composition list (foods, supplements, cosmetics, household consumables).
- If data is missing, note that it is pending verification.
- For accessories or tools, replace this section with **Key Features** describing materials or build qualities.
🍎 **Nutritional information (per 100 g / per serving):**
- Provide full table (calories, fats, carbohydrates, sugars, proteins, fiber, salt) only for edible items or supplements when data exists.
- If unavailable, state it is pending verification.
- Omit this section entirely for non-ingestible products.
💚 **Health benefits / Functional benefits:**
Explain how the product supports wellbeing, personal care, or practical use. Adjust wording to fit the category.
**Why you should try it / Usage tips / Care instructions:**
Close with an aspirational or practical paragraph encouraging its use, tailored to the product type.
OpenFoodFacts link:
If the item is edible or a supplement **and** the EAN is available, add a final sentence linking to the corresponding OpenFoodFacts page. Do not add this link for non-food items.

View File

@@ -0,0 +1,49 @@
Eres un redactor SEO experto en productos naturales, ecológicos y saludables.
Genera una descripción en HTML que resulte rigurosa y útil para la tienda.
Nombre del producto: "$producto"
EAN (si existe): "$ean"
Reglas generales:
- Prioriza la información del fabricante o distribuidor oficial.
Puedes consultar estas webs solo como referencia interna (no las cites ni enlaces): nutritienda.com, veritas.es, naturitas.es, iherb.com, dietisur.es, openfoodfacts.org.
- No inventes datos que no puedas respaldar. Si la información falta, indícalo como pendiente de verificación en la web del fabricante.
- Solo menciona fuentes externas si son el fabricante u OpenFoodFacts cuando corresponda.
- Emplea únicamente etiquetas HTML sencillas: `<p>`, `<b>`, `<ul>`, `<li>`, `<h4>`, `<i>`, `<br>`. Evita `<h1>`, `<h2>`, `<h3>`.
- No devuelvas el contenido dentro de bloques de código ni fences Markdown (` ``` `); responde únicamente con HTML plano.
- Estilo profesional, cercano y acorde a *Natural Mercado de Vida*.
- Cuando exista EAN, consulta la API `https://world.openfoodfacts.net/api/v2/product/{EAN}`.
* Si la respuesta indica que el producto no existe o no aporta datos relevantes (status ≠ 1 o sin nombre/ingredientes/nutrición), no añadas el enlace a OpenFoodFacts e indica que la información está pendiente.
* Solo agrega el enlace público (`https://world.openfoodfacts.org/product/{EAN}`) cuando la API devuelva información sustancial del producto.
Antes de redactar, identifica el tipo de producto:
1. **Alimentos, bebidas o complementos alimenticios:** incluye ingredientes y tabla nutricional si están disponibles.
2. **Cosmética o higiene (cremas, champús, jabones, etc.):** menciona ingredientes o activos principales solo si suelen publicarse; de lo contrario, explica que la composición está pendiente de verificación. No aportes información nutricional.
3. **Limpieza del hogar y detergentes:** describe ingredientes funcionales o características clave; no incluyas datos nutricionales.
4. **Accesorios, utensilios, equipamiento, herramientas de cuidado personal (ej. peines, botellas, batidoras):** omite ingredientes e información nutricional. En su lugar, destaca materiales, diseño, uso y mantenimiento.
5. Si el producto no encaja en las categorías anteriores, aplica criterio y elimina las secciones que no sean pertinentes.
Guía de secciones (adáptalas según lo anterior):
🪴 **Descripción:**
Resumen breve sobre finalidad, origen y beneficios principales. Siempre debe aparecer.
🌿 **Ingredientes / Componentes clave / Características:**
- Usa “Ingredientes” solo cuando el producto realmente tenga una lista de composición.
- Para accesorios o herramientas, cambia el título por “Características principales” y describe materiales, tecnología o acabados.
- Si no hay datos confirmados, indica que están pendientes de verificación.
🍎 **Información nutricional (por 100 g o por dosis):**
- Solo para alimentos o complementos cuando haya datos.
- Si la información no está disponible, menciona que falta confirmación.
- Elimina por completo esta sección cuando el producto no sea ingerible.
💚 **Beneficios para la salud / Beneficios funcionales:**
Explica cómo contribuye al bienestar, cuidado personal o utilidad práctica según el caso.
**Por qué deberías probarlo / Consejos de uso / Mantenimiento:**
Cierre inspirador o práctico que anime a utilizarlo, adaptado al tipo de producto.
Enlace a OpenFoodFacts:
Si es un producto comestible o suplemento y el EAN está disponible, añade al final una frase con enlace a su ficha en OpenFoodFacts. No incluyas este enlace para artículos no alimentarios.

View File

@@ -0,0 +1,190 @@
<?php include ('./inc/header.php'); ?>
<div class="container">
<!-- FORMULARIO
-->
<form enctype="multipart/form-data" action="<?php echo htmlspecialchars(legacy_config('routes.success_url', '')); ?>" name = "newproduct" id = "newproduct" method = "POST">
<input name="pwd" type="hidden" value="<?php echo htmlspecialchars(legacy_config('security.form_password_hash', '')); ?>">
<!-- CODIGO, SEO URL
-->
<div class="row" style = "margin-top: 20px">
<div class="ten columns" id = "url_div">
<span class="slug-ouput"> <b><input type ="text" name="url" value="" placeholder="SEO URL" class="u-full-width" id="slug" tabindex="-1" /></b></span>
</div>
<div class="two columns">
<a id="ia_link" href="" class="button button-primary u-full-width" target="_blank"><i class="fa fa-lightbulb-o fa-lg" aria-hidden="true"></i></a>
</div>
</div>
<!-- NOMBRE DEL PRODUCTO
-->
<div class="row" >
<div class="nine columns">
<label for="slug-source">Nombre del Artículo: </label>
<input type ="text" name="nombre" value="" class="u-full-width text-uppercase" id="slug-source" oninput="setLink()" required />
</div>
<div class="three columns">
<label for="slug-source">EAN: </label>
<input type ="text" name="ean" value="" class="u-full-width" id="ean" />
</div>
</div>
<!-- CATEGORIAS, MARCA
-->
<div class="row">
<div class="six columns">
<select name="categoria" class="u-full-width" id="categoria" required >
<option value="" selected disabled>CATEGORÍA</option>
<?php
$categorias = mysqli_query($con," SELECT * FROM `oc_category`, `oc_category_description`, `oc_category_to_store` WHERE `oc_category`.`category_id` = `oc_category_description`.`category_id` AND `oc_category_description`.`category_id`= `oc_category_to_store`.`category_id` AND `oc_category_description`.`language_id` = 4 ORDER BY `oc_category_description`.`name` ASC");
while ($row = mysqli_fetch_assoc($categorias))
{
echo '<option value="'. $row['category_id'] .'" >' . $row['name'] . ' </option>';
}
?>
</select>
</div>
<div class="four columns newbrand">
<select name="marca" class="u-full-width" id="marca" onchange="validateSelect(),setLink()" required>
<option value="" selected disabled>MARCA</option>
<?php
$marcas = mysqli_query($con," SELECT `oc_manufacturer`.`manufacturer_id` , `oc_manufacturer`.`name` FROM `oc_manufacturer` ORDER BY `oc_manufacturer`.`name`");
while ($row = mysqli_fetch_assoc($marcas))
{
echo '<option value="'. $row['manufacturer_id'] .'" >' . $row['name'] . ' </option>';
}
?>
</select>
</div>
<div class="two columns">
<!-- Abre Ventana Modal -->
<span class="button button-primary u-full-width" id="newbrand"><i class="fa fa-pencil fa-lg" aria-hidden="true"></i></span>
</div>
</div>
<!-- PVP, COSTE, IVA
-->
<div class="row">
<div class="four columns">
<label for="pvp">PVP: </label>
<input type ="number" name="pvp" value="" class="u-full-width" id="pvp" step="any" required />
</div>
<div class="four columns">
<label for="coste">Coste: </label>
<input type ="number" name="coste" value="" class="u-full-width" id="coste" step="any" required />
</div>
<div class="four columns">
<label for="iva">IVA: </label>
<select name="iva" class="u-full-width" id="iva" required >
<?php
$iva = mysqli_query($con," SELECT `tax_class_id` , `title` FROM `oc_tax_class` ORDER BY `title` ASC");
while ($row = mysqli_fetch_assoc($iva))
{
echo '<option value="'. $row['tax_class_id'] .'" >' . substr($row['title'], 4) . ' </option>';
}
?>
</select>
</div>
</div>
<!-- CANTIDAD, CADUCIDAD, PESO
-->
<div class="row">
<div class="four columns">
<label for="cantidad">Cantidad: </label>
<input type ="number" name="cantidad" value="" class="u-full-width" id="cantidad" required>
</div>
<div class="four columns">
<label for="caducidad">Caducidad: </label>
<input type ="date" name="caducidad" value="" min="<?php echo date('Y-m-d');?>" class="u-full-width" id="caducidad" required />
</div>
<div class="four columns">
<label for="peso">Peso: </label>
<select name="peso" class="u-full-width" id="peso" required>
<option value="0.100">100gr</option>
<option value="0.250" selected>250gr</option>
<option value="0.500">500gr</option>
<option value="0.750">750gr</option>
<option value="1.000">1kg</option>
<option value="1.500">1,5kg</option>
<option value="2.000">2kg</option>
</select>
</div>
</div>
<!-- FIELDSET ATRIBUTOS
-->
<div class="row" id="atrib_zone">
<div class="twelve columns">
<?php include ('inc/atributos.php'); ?>
</div>
</div>
<!-- EDITOR JS
-->
<div class="row" style = "margin-top: 20px">
<div class="twelve columns">
<textarea name="editor" id="editor" placeholder="Descripcion del producto..." class="u-full-width"></textarea>
</div>
</div>
<!-- CARGAR IMAGEN X URL
-->
<div class="row" style = "margin-top: 20px">
<div class="twelve columns">
<input type ="text" name="image_path" id="image_path" class="u-full-width" placeholder="Pegar URL de la Imagen" onchange="document.getElementById('imgElement').src = $('#image_path').val()" />
</div>
</div>
<!-- UPLOAD IMAGEN LOCAL
-->
<div class="row" style = "margin-top: 20px">
<div class="four columns" id="imgPreview">
<img id="imgElement" alt="Imagen de Producto" src="./images/nopic.png" />
</div>
<div class="four columns" >
<p class="infoText">* Formatos soportados JPG y PNG</p>
<label for="imagen" class="uploadImg">
<i class="fa fa-cloud-upload"> </i> Cargar Imágen
</label>
<input type="file" class="u-full-width" name="imagen" id="imagen" onchange="document.getElementById('imgElement').src = window.URL.createObjectURL(this.files[0])">
</div>
<div class="four columns" >
<button type="submit" class="button-primary u-full-width" name="submit" id="crearProducto" >Guardar Producto</button>
</div>
</div>
</form>
</div>
<!-- VENTANA MODAL 'NUEVA MARCA'
-->
<div id="marcanueva" class="modal">
<!-- CONTENIDO VENTANA MODAL -->
<div class="modal-content">
<span class="close">&times;</span>
<form enctype="multipart/form-data" action="inc/newmarca.php" method = "POST">
<div class="row" >
<div class="twelve columns">
<input type ="text" name="nombreMarca" value="" class="u-full-width text-uppercase" id="nombreMarca" onfocus="this.value=''" placeholder="Nombre de la Marca" required />
</div>
<div class="modalfoot" >
<button onclick="createBrand();return false" class="button button-primary u-full-width"id="guardarMarca" >Guardar</button>
<button class="button button-primary u-full-width" id="loading" disabled><i class="fa fa-spinner fa-spin"></i> Guardando...</button>
</div>
</div>
</form>
</div>
</div>
<?php
mysqli_close($con);
include ('./inc/footer.php');
?>

View File

@@ -0,0 +1,613 @@
/*! Slugify - v0.1.0 - 2013-05-22
* https://github.com/madflow/jquery-slugify
* Copyright (c) 2013 madflow; Licensed MIT */
;(function($) {
$.fn.slugify = function(source, options) {
return this.each(function() {
var $target = $(this),
$source = $(source);
$target.on('keyup change',function() {
if($target.val() !== '' && $target.val() !== undefined) {
$target.data('locked', true);
} else {
$target.data('locked', false);
}
});
$source.on('keyup change',function() {
if( true === $target.data('locked')) {return;}
if($target.is('input') || $target.is('textarea')) {
$target.val($.slugify($source.val(), options));
} else {
$target.text($.slugify($source.val(), options));
}
});
});
};
// Static method.
$.slugify = function(sourceString, options) {
// Override default options with passed-in options.
options = $.extend({}, $.slugify.options, options);
sourceString = $.trim(sourceString); // Trim
sourceString = sourceString.toLowerCase(); // Lower Case
$.each(options.replaceMap, function(key, value) { // Special char map
sourceString = sourceString.replace(new RegExp(key, 'g'), value || options.invalid);
});
return sourceString
.replace(/\s+/g, options.whitespace) // Replace whitespace characters
.replace(new RegExp('[^a-z0-9 '+ options.whitespace +']', 'g'), options.invalid); // Replace invalid characters
};
// Default options
$.slugify.options = {
whitespace: '-',
invalid: '',
replaceMap: {
'á': 'a',
'Ã ': 'a',
'â': 'a',
'ä': 'ae',
'ã': 'a',
'æ': 'ae',
'ç': 'c',
'é': 'e',
'è': 'e',
'ê': 'e',
'ë': 'e',
'ẽ': 'e',
'í': 'i',
'ì': 'i',
'î': 'i',
'ï': 'i',
'Ä©': 'i',
'ó': 'o',
'ò': 'o',
'ô': 'o',
'ö': 'oe',
'õ': 'o',
'Å“': 'oe',
'ß': 'ss',
'ú': 'u',
'ù': 'u',
'û': 'u',
'ü': 'ue',
'Å©': 'u',
'ă': 'a',
'ắ': 'a',
'ằ': 'a',
'ẵ': 'a',
'ẳ': 'a',
'ấ': 'a',
'ầ': 'a',
'ẫ': 'a',
'ẩ': 'a',
'ÇŽ': 'a',
'Ã¥': 'a',
'Ç»': 'a',
'ÇŸ': 'a',
'ȧ': 'a',
'Ç¡': 'a',
'Ä…': 'a',
'ā': 'a',
'ả': 'a',
'ȁ': 'a',
'ȃ': 'a',
'ạ': 'a',
'ặ': 'a',
'ậ': 'a',
'ḁ': 'a',
'â±¥': 'a',
'ᶏ': 'a',
'ɐ': 'a',
'É‘': 'a',
'ḃ': 'b',
'ḅ': 'b',
'ḇ': 'b',
'Æ€': 'b',
'É“': 'b',
'ƃ': 'b',
'ᵬ': 'b',
'á¶€': 'b',
'þ': 'b',
'ć': 'c',
'ĉ': 'c',
'č': 'c',
'Ä‹': 'c',
'ḉ': 'c',
'ȼ': 'c',
'ƈ': 'c',
'É•': 'c',
'ď': 'd',
'ḋ': 'd',
'ḑ': 'd',
'ḍ': 'd',
'ḓ': 'd',
'ḏ': 'd',
'Ä‘': 'd',
'É–': 'd',
'É—': 'd',
'ƌ': 'd',
'áµ­': 'd',
'ᶁ': 'd',
'á¶‘': 'd',
'È¡': 'd',
'∂': 'd',
'Ä•': 'e',
'ế': 'e',
'ề': 'e',
'á»…': 'e',
'ể': 'e',
'Ä›': 'e',
'Ä—': 'e',
'È©': 'e',
'ḝ': 'e',
'Ä™': 'e',
'Ä“': 'e',
'ḗ': 'e',
'ḕ': 'e',
'ẻ': 'e',
'È…': 'e',
'ȇ': 'e',
'ẹ': 'e',
'ệ': 'e',
'ḙ': 'e',
'ḛ': 'e',
'ɇ': 'e',
'á¶’': 'e',
'ḟ': 'f',
'Æ’': 'f',
'áµ®': 'f',
'á¶‚': 'f',
'ǵ': 'g',
'ÄŸ': 'g',
'ĝ': 'g',
'ǧ': 'g',
'Ä¡': 'g',
'Ä£': 'g',
'ḡ': 'g',
'Ç¥': 'g',
'É ': 'g',
'ᶃ': 'g',
'Ä¥': 'h',
'ÈŸ': 'h',
'ḧ': 'h',
'ḣ': 'h',
'ḩ': 'h',
'ḥ': 'h',
'ḫ': 'h',
'ẖ': 'h',
'ħ': 'h',
'ⱨ': 'h',
'Ä­': 'i',
'ǐ': 'i',
'ḯ': 'i',
'į': 'i',
'Ä«': 'i',
'ỉ': 'i',
'ȉ': 'i',
'È‹': 'i',
'ị': 'i',
'ḭ': 'i',
'ɨ': 'i',
'áµ»': 'i',
'á¶–': 'i',
'i': 'i',
'ı': 'i',
'ĵ': 'j',
'ɉ': 'j',
'ǰ': 'j',
'È·': 'j',
'ʝ': 'j',
'ÉŸ': 'j',
'Ê„': 'j',
'ḱ': 'k',
'Ç©': 'k',
'Ä·': 'k',
'ḳ': 'k',
'ḵ': 'k',
'Æ™': 'k',
'ⱪ': 'k',
'á¶„': 'k',
'ĺ': 'l',
'ľ': 'l',
'ļ': 'l',
'ḷ': 'l',
'ḹ': 'l',
'ḽ': 'l',
'ḻ': 'l',
'Å‚': 'l',
'Å€': 'l',
'Æš': 'l',
'ⱡ': 'l',
'É«': 'l',
'ɬ': 'l',
'á¶…': 'l',
'É­': 'l',
'È´': 'l',
'ḿ': 'm',
'ṁ': 'm',
'ṃ': 'm',
'ᵯ': 'm',
'ᶆ': 'm',
'ɱ': 'm',
'Å„': 'n',
'ǹ': 'n',
'ň': 'n',
'ñ': 'n',
'á¹…': 'n',
'ņ': 'n',
'ṇ': 'n',
'ṋ': 'n',
'ṉ': 'n',
'n̈': 'n',
'ɲ': 'n',
'Æž': 'n',
'Å‹': 'n',
'áµ°': 'n',
'ᶇ': 'n',
'ɳ': 'n',
'ȵ': 'n',
'ŏ': 'o',
'ố': 'o',
'ồ': 'o',
'á»—': 'o',
'ổ': 'o',
'Ç’': 'o',
'È«': 'o',
'Å‘': 'o',
'ṍ': 'o',
'ṏ': 'o',
'È­': 'o',
'ȯ': 'o',
˜˜': 'o',
'ȱ': 'o',
'ø': 'o',
'Ç¿': 'o',
'Ç«': 'o',
'Ç­': 'o',
'ō': 'o',
'ṓ': 'o',
'ṑ': 'o',
'ỏ': 'o',
'ȍ': 'o',
'ȏ': 'o',
'Æ¡': 'o',
'á»›': 'o',
'ờ': 'o',
'ỡ': 'o',
'ở': 'o',
'ợ': 'o',
'ọ': 'o',
'á»™': 'o',
'ɵ': 'o',
'É”': 'o',
'ṕ': 'p',
'á¹—': 'p',
'áµ½': 'p',
'Æ¥': 'p',
'p̃': 'p',
'áµ±': 'p',
'ᶈ': 'p',
'É‹': 'q',
'Æ£': 'q',
'Ê ': 'q',
'Å•': 'r',
'Å™': 'r',
'á¹™': 'r',
'Å—': 'r',
'È‘': 'r',
'È“': 'r',
'á¹›': 'r',
'ṝ': 'r',
'ṟ': 'r',
'ɍ': 'r',
'ɽ': 'r',
'áµ²': 'r',
'ᶉ': 'r',
'ɼ': 'r',
'ɾ': 'r',
'áµ³': 'r',
'Å›': 's',
'á¹¥': 's',
'ŝ': 's',
'Å¡': 's',
'á¹§': 's',
'ṡẛ': 's',
'ÅŸ': 's',
'á¹£': 's',
'ṩ': 's',
'È™': 's',
's̩': 's',
'áµ´': 's',
'á¶Š': 's',
'Ê‚': 's',
'È¿': 's',
'Å¥': 't',
'ṫ': 't',
'Å£': 't',
'á¹­': 't',
'È›': 't',
'á¹±': 't',
'ṯ': 't',
'ŧ': 't',
'ⱦ': 't',
'Æ­': 't',
'ʈ': 't',
'̈ẗ': 't',
'áµµ': 't',
'Æ«': 't',
'ȶ': 't',
'Å­': 'u',
'Ç”': 'u',
'ů': 'u',
'ǘ': 'u',
'ǜ': 'u',
'Çš': 'u',
'Ç–': 'u',
'ű': 'u',
'á¹¹': 'u',
'ų': 'u',
'Å«': 'u',
'á¹»': 'u',
'á»§': 'u',
'È•': 'u',
'È—': 'u',
'ư': 'u',
'ứ': 'u',
'ừ': 'u',
'ữ': 'u',
'á»­': 'u',
'á»±': 'u',
'ụ': 'u',
'á¹³': 'u',
'á¹·': 'u',
'á¹µ': 'u',
'ʉ': 'u',
'áµ¾': 'u',
'á¶™': 'u',
'á¹½': 'v',
'ṿ': 'v',
'Ê‹': 'v',
'ᶌ': 'v',
'â±´': 'v',
'ẃ': 'w',
'ẁ': 'w',
'ŵ': 'w',
'ẅ': 'w',
'ẇ': 'w',
'ẉ': 'w',
'ẘ': 'w',
'ẍ': 'x',
'ẋ': 'x',
'ᶍ': 'x',
'ý': 'y',
'ỳ': 'y',
'Å·': 'y',
'ẙ': 'y',
'ÿ': 'y',
'ỹ': 'y',
'ẏ': 'y',
'ȳ': 'y',
'á»·': 'y',
'ỵ': 'y',
'ɏ': 'y',
'Æ´': 'y',
'ʏ': 'y',
'ź': 'z',
'ẑ': 'z',
'ž': 'z',
'ż': 'z',
'ẓ': 'z',
'ẕ': 'z',
'ƶ': 'z',
'È¥': 'z',
'ⱬ': 'z',
'áµ¶': 'z',
'á¶Ž': 'z',
'ʐ': 'z',
'Ê‘': 'z',
'É€': 'z',
'α': 'a',
'β': 'b',
'γ': 'g',
'É£': 'g',
'δ': 'd',
'ð': 'd',
'ε': 'e',
'ζ': 'z',
'η': 'i',
'θ': 'th',
'ι': 'i',
'κ': 'k',
'λ': 'l',
'μ': 'm',
'µ': 'm',
'ν': 'n',
'ξ': 'x',
'ο': 'o',
'Ï€': 'p',
'ρ': 'r',
'σ': 's',
'Ï‚': 's',
'Ï„': 't',
'Ï…': 'u',
'φ': 'f',
'χ': 'ch',
'ψ': 'ps',
'ω': 'o',
'á¾³': 'a',
'ά': 'a',
'á½°': 'a',
'á¾´': 'a',
'á¾²': 'a',
'á¾¶': 'a',
'á¾·': 'a',
'á¼€': 'a',
'á¾€': 'a',
'ἄ': 'a',
'ᾄ': 'a',
'ἂ': 'a',
'ᾂ': 'a',
'ἆ': 'a',
'ᾆ': 'a',
'ἁ': 'a',
'ᾁ': 'a',
'á¼…': 'a',
'á¾…': 'a',
'ἃ': 'a',
'ᾃ': 'a',
'ἇ': 'a',
'ᾇ': 'a',
'á¾±': 'a',
'á¾°': 'a',
'έ': 'e',
'á½²': 'e',
'ἐ': 'e',
'á¼”': 'e',
'á¼’': 'e',
'ἑ': 'e',
'ἕ': 'e',
'ἓ': 'e',
'ῃ': 'i',
'ή': 'i',
'á½´': 'i',
'á¿„': 'i',
'á¿‚': 'i',
'ῆ': 'i',
'ῇ': 'i',
'á¼ ': 'i',
'ᾐ': 'i',
'ἤ': 'i',
'á¾”': 'i',
'á¼¢': 'i',
'á¾’': 'i',
'ἦ': 'i',
'á¾–': 'i',
'ἡ': 'i',
'ᾑ': 'i',
'á¼¥': 'i',
'ᾕ': 'i',
'á¼£': 'i',
'ᾓ': 'i',
'á¼§': 'i',
'á¾—': 'i',
'ί': 'i',
'á½¶': 'i',
'á¿–': 'i',
'á¼°': 'i',
'á¼´': 'i',
'á¼²': 'i',
'á¼¶': 'i',
'á¼±': 'i',
'á¼µ': 'i',
'á¼³': 'i',
'á¼·': 'i',
'ÏŠ': 'i',
'ΐ': 'i',
'á¿’': 'i',
'á¿—': 'i',
'á¿‘': 'i',
'ῐ': 'i',
'ό': 'o',
'ὸ': 'o',
'á½€': 'o',
'ὄ': 'o',
'ὂ': 'o',
'ὁ': 'o',
'á½…': 'o',
'ὃ': 'o',
'ύ': 'u',
'ὺ': 'u',
'ῦ': 'u',
'ὐ': 'u',
'á½”': 'u',
'á½’': 'u',
'á½–': 'u',
'ὑ': 'u',
'ὕ': 'u',
'ὓ': 'u',
'á½—': 'u',
'Ï‹': 'u',
'ΰ': 'u',
'á¿¢': 'u',
'á¿§': 'u',
'á¿¡': 'u',
'á¿ ': 'u',
'ῳ': 'o',
'ÏŽ': 'o',
'á¿´': 'o',
'á½¼': 'o',
'ῲ': 'o',
'á¿¶': 'o',
'á¿·': 'o',
'á½ ': 'o',
'á¾ ': 'o',
'ὤ': 'o',
'ᾤ': 'o',
'á½¢': 'o',
'á¾¢': 'o',
'ὦ': 'o',
'ᾦ': 'o',
'ὡ': 'o',
'ᾡ': 'o',
'á½¥': 'o',
'á¾¥': 'o',
'á½£': 'o',
'á¾£': 'o',
'á½§': 'o',
'á¾§': 'o',
'ῤ': 'r',
'á¿¥': 'r',
'а': 'a',
'б': 'b',
'в': 'v',
'г': 'g',
'д': 'd',
'е': 'e',
'Ñ‘': 'e',
'ж': 'zh',
'з': 'z',
'и': 'i',
'й': 'j',
'к': 'k',
'л': 'l',
'м': 'm',
'н': 'n',
'о': 'o',
'п': 'p',
'Ñ€': 'r',
'с': 'n',
'Ñ‚': 't',
'у': 'u',
'Ñ„': 'f',
'Ñ…': 'h',
'ц': 'ts',
'ч': 'ch',
'ш': 'sh',
'щ': 'sh',
'ÑŠ': '',
'Ñ‹': 'i',
'ь': '',
'э': 'n',
'ÑŽ': 'yu',
'я': 'ya',
'Ñ–': 'j',
'ѳ': 'f',
'Ñ£': 'e',
'ѵ': 'i',
'Ñ•': 'z',
'ѯ': 'ks',
'ѱ': 'ps',
'Ñ¡': 'o',
'Ñ«': 'yu',
'ѧ': 'ya',
'Ñ­': 'yu',
'Ñ©': 'ya'
}
};
}(jQuery));

View File

@@ -0,0 +1,269 @@
<?php
require_once __DIR__ . '/bootstrap.php';
ini_set('max_execution_time', 120);
set_time_limit(120);
$LANG_ES = (int) legacy_config('store.language_es', 4);
$IMG_BASE = legacy_config('store.image_base_url', 'https://example.local/image/');
$LOG_PATH = legacy_config('paths.worker_log', __DIR__ . '/logs/worker.log');
if (isset($_GET['action']) && $_GET['action'] === 'log_tail') {
header('Content-Type: application/json; charset=utf-8');
$limit = isset($_GET['limit']) ? max(10, min(200, (int)$_GET['limit'])) : 80;
if (!file_exists($LOG_PATH)) {
echo json_encode(['ok' => false, 'message' => 'Aún no hay registros.']);
} else {
$lines = file($LOG_PATH, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$tail = array_slice($lines, -$limit);
echo json_encode([
'ok' => true,
'updated' => date('H:i:s'),
'log' => implode("\n", $tail)
]);
}
exit;
}
if (isset($_GET['action']) && $_GET['action'] === 'show_log') {
include('./inc/header.php');
$logEndpoint = 'productos_bulk_update.php?action=log_tail&limit=120';
echo "<div class='container'>
<h3>📊 Progreso del worker</h3>
<p>Puedes dejar esta ventana abierta para monitorear la cola en tiempo real.</p>
<div class='row'>
<div class='twelve columns'>
<div class='log-panel'>
<div class='log-panel__header'>
<strong>Últimas entradas</strong>
<span id='log-status' class='log-panel__status'>Actualizando...</span>
</div>
<pre id='log-viewer' class='log-panel__body'>Cargando log...</pre>
</div>
</div>
</div>
<div class='row'>
<div class='twelve columns'>
<button onclick=\"window.location.href='productos_bulk_update.php'\" class='button last'>← Volver al listado</button>
</div>
</div>
</div>
<script>
(function(){
const viewer = document.getElementById('log-viewer');
const status = document.getElementById('log-status');
const endpoint = '$logEndpoint';
const refresh = () => {
fetch(endpoint + '&_=' + Date.now(), {cache:'no-store'})
.then(r => r.json())
.then(data => {
if (!data.ok) {
viewer.textContent = data.message || 'Sin datos disponibles.';
status.textContent = 'Sin registros';
return;
}
viewer.textContent = data.log || 'Sin registros recientes.';
status.textContent = 'Actualizado ' + (data.updated || '');
viewer.scrollTop = viewer.scrollHeight;
})
.catch(() => {
status.textContent = 'Error al actualizar';
});
};
refresh();
setInterval(refresh, 5000);
})();
</script>";
include('./inc/footer.php');
exit;
}
include('./inc/header.php');
$db = legacy_new_mysqli();
if ($db->connect_errno) die("❌ Error DB: " . $db->connect_error);
/* ============================================
🧾 Insertar productos seleccionados a la cola
============================================ */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['productos'])) {
$productos = $_POST['productos'];
$inserted = 0;
foreach ($productos as $pid) {
$pid = (int)$pid;
$db->query("INSERT IGNORE INTO oc_product_queue (product_id) VALUES ($pid)");
if ($db->affected_rows > 0) {
$inserted++;
}
}
$logEndpoint = 'productos_bulk_update.php?action=log_tail&limit=120';
echo "<div class='container'>
<h3>✅ $inserted productos añadidos a la cola.</h3>
<p>El worker los procesará automáticamente por orden de llegada. Puedes seguir el progreso en tiempo real debajo.</p>
<div class='row'>
<div class='twelve columns'>
<div class='log-panel'>
<div class='log-panel__header'>
<strong>📊 Log del worker (últimas entradas)</strong>
<span id='log-status' class='log-panel__status'>Actualizando...</span>
</div>
<pre id='log-viewer' class='log-panel__body'>Cargando log...</pre>
</div>
</div>
</div>
<div class='row'>
<div class='twelve columns'>
<button class='button button-primary' onclick=\"window.location.href='productos_bulk_update.php'\">← Volver</button>
</div>
</div>
</div>
<script>
(function(){
const viewer = document.getElementById('log-viewer');
const status = document.getElementById('log-status');
const endpoint = '$logEndpoint';
const refresh = () => {
fetch(endpoint + '&_=' + Date.now(), {cache: 'no-store'})
.then(r => r.json())
.then(data => {
if (!data.ok) {
viewer.textContent = data.message || 'Sin datos disponibles.';
status.textContent = 'Sin registros';
return;
}
viewer.textContent = data.log || 'Sin registros recientes.';
status.textContent = 'Actualizado ' + (data.updated || '');
viewer.scrollTop = viewer.scrollHeight;
})
.catch(() => {
status.textContent = 'Error al actualizar';
});
};
refresh();
setInterval(refresh, 5000);
})();
</script>";
include('./inc/footer.php');
exit;
}
/* ============================================
📋 Listado de productos activos no procesados
============================================ */
$per_page = isset($_GET['per_page']) ? max(10, min(200, (int)$_GET['per_page'])) : 50;
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$sql_total = "
SELECT COUNT(*) AS total
FROM oc_product p
WHERE p.status = 1
AND p.product_id NOT IN (SELECT product_id FROM oc_product_queue)";
$res_total = $db->query($sql_total);
$total_rows = $res_total ? (int)$res_total->fetch_assoc()['total'] : 0;
$total_pages = max(1, (int)ceil($total_rows / $per_page));
if ($page > $total_pages) {
$page = $total_pages;
}
$offset = ($page - 1) * $per_page;
$q = "
SELECT p.product_id, p.image, d1.name, LEFT(d1.description, 250) AS descripcion
FROM oc_product p
LEFT JOIN oc_product_description d1
ON p.product_id=d1.product_id AND d1.language_id=$LANG_ES
WHERE p.status = 1
AND p.product_id NOT IN (SELECT product_id FROM oc_product_queue)
ORDER BY p.product_id ASC
LIMIT $per_page OFFSET $offset";
$res = $db->query($q);
?>
<div class="container">
<h3>🧾 Productos pendientes de optimización SEO</h3>
<div class="row">
<div class="eight columns">
<p>Total pendientes: <strong><?php echo $total_rows; ?></strong><br>
Página <strong><?php echo $page; ?></strong> de <strong><?php echo $total_pages; ?></strong></p>
</div>
<div class="four columns">
<form method="GET" class="row u-cf">
<div class="six columns">
<label for="per_page">products</label>
</div>
<div class="six columns">
<select class="u-full-width" name="per_page" id="per_page" onchange="this.form.submit()">
<?php foreach ([25, 50, 100, 150, 200] as $opt): ?>
<option value="<?php echo $opt; ?>" <?php echo $opt==$per_page?'selected':''; ?>><?php echo $opt; ?></option>
<?php endforeach; ?>
</select>
</div>
<input type="hidden" name="page" value="1">
<noscript><div class="four columns"><button type="submit" class="button">Actualizar</button></div></noscript>
</form>
</div>
</div>
<?php if ($total_rows === 0): ?>
<div class="row">
<div class="twelve columns alert-box">
<p>No hay productos pendientes de encolar.</p>
<button type="button" class="button" onclick="window.location.href='productos_bulk_update.php?action=show_log'">
Ver progreso del worker
</button>
</div>
</div>
<?php endif; ?>
<form method="POST">
<table class="u-full-width">
<thead>
<tr><th></th><th>ID</th><th>Imagen</th><th>Nombre</th><th>Descripción</th></tr>
</thead>
<tbody>
<?php while($r=$res->fetch_assoc()){
$img=!empty($r['image'])?$IMG_BASE.htmlspecialchars($r['image']):"https://via.placeholder.com/80x80?text=No+Image"; ?>
<tr>
<td><input type="checkbox" name="productos[]" value="<?php echo $r['product_id']; ?>" checked></td>
<td><?php echo $r['product_id']; ?></td>
<td><img src="<?php echo $img; ?>" alt=""></td>
<td><?php echo htmlspecialchars($r['name']); ?></td>
<td><div class="table-description"><?php echo nl2br(strip_tags($r['descripcion'])); ?></div></td>
</tr>
<?php } ?>
</tbody>
</table>
<div class="row">
<div class="twelve columns">
<button type="submit" class="button button-primary"> Encolar seleccionados</button>
</div>
</div>
<input type="hidden" name="current_page" value="<?php echo $page; ?>">
<input type="hidden" name="current_per_page" value="<?php echo $per_page; ?>">
</form>
<?php
$queryBase = function($targetPage) use ($per_page) {
return 'productos_bulk_update.php?page=' . $targetPage . '&per_page=' . $per_page;
};
?>
<div class="row pagination-row">
<div class="twelve columns">
<?php if($page > 1): ?>
<a class="button" href="<?php echo $queryBase($page-1); ?>">← Página anterior</a>
<?php else: ?>
<span class="button disabled">← Página anterior</span>
<?php endif; ?>
<span class="pagination-meta">Página <?php echo $page; ?> de <?php echo $total_pages; ?></span>
<?php if($page < $total_pages): ?>
<a class="button" href="<?php echo $queryBase($page+1); ?>">Página siguiente →</a>
<?php else: ?>
<span class="button disabled">Página siguiente →</span>
<?php endif; ?>
</div>
</div>
</div>
<?php include('./inc/footer.php'); ?>

View File

@@ -0,0 +1,284 @@
<?php
/* ============================
productos_modificados.php
============================ */
require_once __DIR__ . '/bootstrap.php';
$LANG_ES = (int) legacy_config('store.language_es', 4);
$LANG_EN = (int) legacy_config('store.language_en', 1);
$IMG_BASE = legacy_config('store.image_base_url', 'https://example.local/image/');
$PRODUCT_BASE = legacy_config('store.product_base_url', 'https://example.local/index.php?route=product/product&product_id=');
/* ---- Conexión BD ---- */
$db = legacy_new_mysqli();
if ($db->connect_errno) {
header('Content-Type: text/plain; charset=utf-8');
http_response_code(500);
echo "DB Error: " . $db->connect_error;
exit;
}
/* ---- AJAX ---- */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json; charset=utf-8');
// Toggle individual
if (isset($_POST['pid']) && isset($_POST['toggle'])) {
$pid = (int)$_POST['pid'];
$state = (int)($_POST['toggle'] ? 1 : 0);
$ok = $db->query("UPDATE oc_product_queue SET needs_verify=$state WHERE product_id=$pid");
echo json_encode(['ok'=>$ok, 'state'=>$state]);
exit;
}
// Auto-marcar faltantes
if (isset($_POST['autoMark'])) {
$ok = $db->query("UPDATE oc_product_queue
SET needs_verify=1
WHERE processed=1 AND
(product_id IN (
SELECT p.product_id FROM oc_product p
LEFT JOIN oc_product_description d1 ON p.product_id=d1.product_id AND d1.language_id=$LANG_ES
LEFT JOIN oc_product_description d2 ON p.product_id=d2.product_id AND d2.language_id=$LANG_EN
WHERE (d1.description IS NULL OR d1.description='')
OR (d2.description IS NULL OR d2.description='')
))");
echo json_encode(['ok'=>$ok]);
exit;
}
// 🔁 Reprocesar Needs Verify
if (isset($_POST['reprocessNeeds'])) {
$ok = $db->query("UPDATE oc_product_queue
SET processed=0, log=NULL
WHERE needs_verify=1");
echo json_encode(['ok'=>$ok]);
exit;
}
}
/* ---- Vista ---- */
include('./inc/header.php'); // aquí ya se incluye custom.css
/* ---- Paginación ---- */
$per_page = 50;
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$offset = ($page - 1) * $per_page;
$search = isset($_GET['q']) ? trim($_GET['q']) : '';
$filter_missing = isset($_GET['missing']) ? (int)$_GET['missing'] : 0;
$conditions = ["q.processed=1"];
if ($search !== '') {
$safe = $db->real_escape_string($search);
$conditions[] = "(d1.name LIKE '%$safe%' OR d2.name LIKE '%$safe%')";
}
if ($filter_missing) {
$conditions[] = "(
(d1.description IS NULL OR d1.description='')
OR (d2.description IS NULL OR d2.description='')
OR (d1.meta_description IS NULL OR d1.meta_description='')
OR (d2.meta_description IS NULL OR d2.meta_description='')
)";
}
$where = implode(' AND ', $conditions);
$res_total = $db->query("SELECT COUNT(*) AS t
FROM oc_product_queue q
JOIN oc_product p ON q.product_id=p.product_id
LEFT JOIN oc_product_description d1 ON p.product_id=d1.product_id AND d1.language_id=$LANG_ES
LEFT JOIN oc_product_description d2 ON p.product_id=d2.product_id AND d2.language_id=$LANG_EN
WHERE $where");
$total = $res_total ? (int)$res_total->fetch_assoc()['t'] : 0;
$total_pages = max(1, ceil($total / $per_page));
$sql = "
SELECT p.product_id,p.image,
d1.description AS descripcion_es,
d2.description AS descripcion_en,
d1.name AS nombre_es,
d2.name AS nombre_en,
q.processed_at,q.needs_verify
FROM oc_product_queue q
JOIN oc_product p ON q.product_id=p.product_id
LEFT JOIN oc_product_description d1 ON p.product_id=d1.product_id AND d1.language_id=$LANG_ES
LEFT JOIN oc_product_description d2 ON p.product_id=d2.product_id AND d2.language_id=$LANG_EN
WHERE $where
ORDER BY q.processed_at DESC
LIMIT $per_page OFFSET $offset";
$res = $db->query($sql);
?>
<div class="container">
<h3>🧾 Productos modificados</h3>
<p>Total: <b><?php echo $total; ?></b> — Página <b><?php echo $page; ?></b> de <b><?php echo $total_pages; ?></b></p>
<form method="GET">
<div class="row" style="margin-bottom: 0;">
<div class="nine columns">
<input type="text"
id="search-name"
name="q"
value="<?php echo htmlspecialchars($search); ?>"
placeholder="Filtrar por nombre..."
class="u-full-width">
</div>
<div class="three columns">
<label for="missing" class="u-pull-right" style="margin-top:8px;">
<input type="checkbox"
id="missing"
name="missing"
value="1"
<?php echo $filter_missing ? 'checked' : ''; ?>>
Missing
</label>
</div>
</div>
<div class="row">
<div class="six columns">
<button type="submit" class="button button-primary u-full-width">Aplicar</button>
</div>
<div class="six columns">
<a href="productos_modificados.php" class="button u-full-width">Limpiar</a>
</div>
</div>
</form>
<div class="row toolbar">
<div class="six columns">
<button class="button u-full-width" onclick="autoMark()" type="button">⚙️ Auto-marcar faltantes</button>
</div>
<div class="six columns">
<button class="button u-full-width" onclick="reprocessNeeds()" type="button">🔁 Reprocesar Needs Verify</button>
</div>
</div>
<table class="u-full-width">
<tr><th>Imagen</th><th>Descripciones</th></tr>
<?php while($r=$res->fetch_assoc()):
$img = !empty($r['image']) ? $IMG_BASE.htmlspecialchars($r['image']) : "https://via.placeholder.com/160";
$url = $PRODUCT_BASE.$r['product_id'];
$fecha = $r['processed_at']?date('d/m/Y H:i',strtotime($r['processed_at'])):'-';
$has_en = !empty(trim($r['descripcion_en']));
$has_es = !empty(trim($r['descripcion_es']));
$flag_html = $r['needs_verify']
? '<span class="badge warn">⚠️ Needs verify</span>'
: '<span class="badge ok">✔ Verified</span>';
$btn_text = $r['needs_verify'] ? '✅ Mark OK' : '⚠️ Mark Needs Verify';
$btn_cls = $r['needs_verify'] ? 'toggle-btn active' : 'toggle-btn';
?>
<tr>
<td rowspan="2" style="text-align:center;width:180px;">
<a href="<?php echo $url;?>" target="_blank"><img src="<?php echo $img;?>" alt=""></a>
<div class="idbox">
ID <?php echo $r['product_id'];?><br>
<small><?php echo htmlspecialchars($r['nombre_es'] ?: $r['nombre_en'] ?: ''); ?></small><br>
<small><?php echo $fecha;?></small><br>
<div id="flag-<?php echo $r['product_id'];?>"><?php echo $flag_html;?></div>
<button class="<?php echo $btn_cls;?>" id="btn-<?php echo $r['product_id'];?>"
onclick="toggleVerify(<?php echo $r['product_id'];?>)">
<?php echo $btn_text;?>
</button>
</div>
</td>
<td>
<div class="lang-title">🇬🇧 English
<span class="badge <?php echo $has_en?'ok':'miss';?>"><?php echo $has_en?'OK':'Missing';?></span>
</div>
<div class="lang-section"><?php echo $has_en?$r['descripcion_en']:'<i>No data</i>';?></div>
</td>
</tr>
<tr>
<td>
<div class="lang-title">🇪🇸 Español
<span class="badge <?php echo $has_es?'ok':'miss';?>"><?php echo $has_es?'OK':'Missing';?></span>
</div>
<div class="lang-section"><?php echo $has_es?$r['descripcion_es']:'<i>No data</i>';?></div>
</td>
</tr>
<?php endwhile;?>
</table>
<?php
$queryBase = function($targetPage) use ($search, $filter_missing) {
$params = ['page'=>$targetPage];
if ($search !== '') $params['q'] = $search;
if ($filter_missing) $params['missing'] = 1;
return 'productos_modificados.php?' . http_build_query($params);
};
?>
<div class="pagination">
<?php if($page>1):?>
<a href="<?php echo $queryBase($page-1);?>">← Prev</a>
<?php else:?>
<a class="disabled">← Prev</a>
<?php endif;?>
<span>Página <?php echo $page;?> de <?php echo $total_pages;?></span>
<?php if($page<$total_pages):?>
<a href="<?php echo $queryBase($page+1);?>">Next →</a>
<?php else:?>
<a class="disabled">Next →</a>
<?php endif;?>
</div>
</div>
<script>
function toggleVerify(pid){
const btn = document.getElementById('btn-'+pid);
const flag = document.getElementById('flag-'+pid);
const active = btn.classList.contains('active');
const next = active ? 0 : 1;
const params = new URLSearchParams({pid:pid, toggle:next});
fetch(window.location.pathname+window.location.search,{
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:params.toString()
}).then(r=>r.json()).then(j=>{
if(!j.ok){alert('Error al actualizar');return;}
if(j.state===1){
btn.classList.add('active');
btn.textContent='✅ Mark OK';
flag.innerHTML='<span class="badge warn">⚠️ Needs verify</span>';
}else{
btn.classList.remove('active');
btn.textContent='⚠️ Mark Needs Verify';
flag.innerHTML='<span class="badge ok">✔ Verified</span>';
}
});
}
function autoMark(){
if(!confirm('¿Marcar automáticamente como "Need verification" todos los productos con descripciones faltantes?'))return;
fetch('',{
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:'autoMark=1'
}).then(r=>r.json()).then(j=>{
if(j.ok){alert('Productos con descripciones faltantes marcados como Need Verify.');location.reload();}
else alert('Error al marcar.');
});
}
function reprocessNeeds(){
if(!confirm('¿Reprocesar todos los productos marcados como "Needs Verify"?'))return;
fetch('',{
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:'reprocessNeeds=1'
})
.then(r=>r.json())
.then(j=>{
if(j.ok){
alert('Los productos "Needs Verify" han sido marcados como pendientes para reprocesar. El worker los regenerará.');
location.reload();
} else alert('Error al actualizar.');
})
.catch(()=>alert('Error de red.'));
}
</script>
<?php include('./inc/footer.php'); ?>

View File

@@ -0,0 +1,255 @@
<?php
// ============================================================
// worker_bulk.php — doble prompt: inglés y español independientes
// ============================================================
require_once __DIR__ . '/bootstrap.php';
date_default_timezone_set('Europe/Madrid');
mb_internal_encoding('UTF-8');
@ini_set('max_execution_time', '0');
@set_time_limit(0);
/* === CONFIG === */
$OPENAI_API_KEY = trim((string) legacy_config('openai.api_key', ''));
$OPENAI_MODEL = legacy_config('openai.model', 'gpt-4o-mini');
$OPENAI_ENDPOINT = legacy_config('openai.endpoint', 'https://api.openai.com/v1/chat/completions');
$LANG_ES = (int) legacy_config('store.language_es', 4);
$LANG_EN = (int) legacy_config('store.language_en', 1);
$STORE_NAME = legacy_config('store.name', 'Natural - Mercado de Vida');
$LOG_FILE = legacy_config('paths.worker_log', __DIR__ . '/logs/worker.log');
$PROMPT_EN_FILE = legacy_config('paths.prompt_en', __DIR__ . '/inc/prompt_en.md');
$PROMPT_ES_FILE = legacy_config('paths.prompt_es', __DIR__ . '/inc/prompt_es.md');
$BATCH_SIZE = (int) legacy_config('worker.batch_size', 20);
$MIN_HTML_LENGTH = (int) legacy_config('worker.min_html_length', 500);
$SHARD_TOTAL = 1;
$SHARD_INDEX = 0;
if (PHP_SAPI === 'cli' && isset($argv)) {
foreach ($argv as $arg) {
if (strpos($arg, '--shards=') === 0) {
$value = (int)substr($arg, 9);
if ($value > 0) $SHARD_TOTAL = min($value, 16); // evita saturar en exceso
} elseif (strpos($arg, '--shard=') === 0) {
$value = (int)substr($arg, 8);
if ($value >= 0) $SHARD_INDEX = $value;
}
}
}
if ($SHARD_INDEX >= $SHARD_TOTAL) {
$SHARD_INDEX = $SHARD_TOTAL - 1;
}
if ($SHARD_INDEX < 0) $SHARD_INDEX = 0;
/* === FUNCIONES === */
function log_msg($msg) {
global $LOG_FILE;
$time = date('Y-m-d H:i:s');
file_put_contents($LOG_FILE, "[$time] $msg\n", FILE_APPEND);
}
function obtener_respuesta($prompt, $key, $model, $max_tokens = 2000, $retries = 3) {
$endpoint = legacy_config('openai.endpoint', 'https://api.openai.com/v1/chat/completions');
if ($key === '' || strpos($key, 'CHANGE_ME_') === 0) {
log_msg('❌ Missing openai.api_key in config/local.php');
return '';
}
for ($i = 1; $i <= $retries; $i++) {
$ch = curl_init($endpoint);
$data = [
'model' => $model,
'messages' => [['role' => 'user', 'content' => $prompt]],
'temperature' => 0.6,
'max_tokens' => $max_tokens
];
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . trim($key)
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_TIMEOUT => 180
]);
$result = curl_exec($ch);
$err = curl_error($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($err) { log_msg("⚠️ cURL error ($i/$retries): $err"); sleep(2); continue; }
if ($http !== 200) { log_msg("⚠️ HTTP $http on attempt $i"); sleep(3); continue; }
$json = json_decode($result, true);
$txt = $json['choices'][0]['message']['content'] ?? '';
if ($txt && mb_strlen(trim($txt)) > 50) return trim($txt);
log_msg("⚠️ Empty response attempt $i");
sleep(2);
}
log_msg("❌ No response after $retries attempts");
return '';
}
function limpiar_html($t) {
if (!$t) return '';
// 🔧 Quita fences Markdown (```html ... ```)
$t = preg_replace('/^```[a-zA-Z]*\s*/m', '', $t);
$t = preg_replace('/```$/m', '', $t);
$t = preg_replace('/```[\s\S]*?```/', '', $t);
// Quita h1/h2 pero conserva contenido
$t = preg_replace('/<\/?h1[^>]*>/i', '', $t);
$t = preg_replace('/<\/?h2[^>]*>/i', '', $t);
// Convierte div y section a <p>
$t = preg_replace('/<\s*div[^>]*>/i', '<p>', $t);
$t = preg_replace('/<\s*\/div\s*>/i', '</p>', $t);
$t = preg_replace('/<\s*section[^>]*>/i', '<p>', $t);
$t = preg_replace('/<\s*\/section\s*>/i', '</p>', $t);
// Quita scripts y estilos
$t = preg_replace('/<script.*?<\/script>/is', '', $t);
$t = preg_replace('/<style.*?<\/style>/is', '', $t);
// Quita markdown residual
$t = str_replace('```', '', $t);
// Limpieza de espacios
$t = preg_replace('/[ \t]+/', ' ', $t);
$t = preg_replace('/\n{2,}/', "\n", $t);
return trim($t);
}
/* Elimina emojis y normaliza espacios */
function sanitize_for_db($text) {
if ($text === null || $text === '') return '';
$text = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $text);
$text = preg_replace('/\s+/', ' ', $text);
return trim($text);
}
function sentence_case($text) {
if (empty($text)) return '';
$text = trim(mb_strtolower($text, 'UTF-8'));
$first = mb_strtoupper(mb_substr($text, 0, 1, 'UTF-8'), 'UTF-8');
return $first . mb_substr($text, 1, null, 'UTF-8');
}
/* === DB === */
$db = legacy_new_mysqli();
if ($db->connect_errno) { log_msg('❌ DB: ' . $db->connect_error); exit; }
/* === Prompt base === */
if (!file_exists($PROMPT_EN_FILE) || !file_exists($PROMPT_ES_FILE)) {
log_msg("❌ Missing prompt files.");
exit;
}
$PROMPT_EN = file_get_contents($PROMPT_EN_FILE);
$PROMPT_ES = file_get_contents($PROMPT_ES_FILE);
if (trim($PROMPT_EN) === '' || trim($PROMPT_ES) === '') {
log_msg("❌ Empty prompt files.");
exit;
}
/* === Worker === */
$shardLabel = $SHARD_TOTAL > 1 ? " | shard {$SHARD_INDEX}/{$SHARD_TOTAL}" : '';
log_msg("🚀 Worker iniciado (modo doble prompt, batch={$BATCH_SIZE}{$shardLabel})");
$shardFilter = $SHARD_TOTAL > 1 ? " AND MOD(id, {$SHARD_TOTAL}) = {$SHARD_INDEX}" : '';
$q = $db->query("SELECT * FROM oc_product_queue WHERE processed=0{$shardFilter} ORDER BY id ASC LIMIT $BATCH_SIZE");
if (!$q || $q->num_rows === 0) { log_msg("⏸️ Cola vacía."); exit; }
while ($row = $q->fetch_assoc()) {
$pid = (int)$row['product_id'];
log_msg("🔄 Procesando producto $pid...");
$r = $db->query("
SELECT p.ean, d.name
FROM oc_product p
LEFT JOIN oc_product_description d ON p.product_id=d.product_id AND d.language_id=$LANG_ES
WHERE p.product_id=$pid
");
if (!$r || !$prod = $r->fetch_assoc()) {
log_msg("⚠️ Producto $pid no encontrado");
$db->query("UPDATE oc_product_queue SET processed=1, log='No encontrado' WHERE product_id=$pid");
continue;
}
$producto = $prod['name'];
$ean = $prod['ean'];
// === Prompts personalizados ===
$prompt_en = str_replace(['$producto', '$ean'], [$producto, $ean], $PROMPT_EN);
$prompt_es = str_replace(['$producto', '$ean'], [$producto, $ean], $PROMPT_ES);
// === Generar EN ===
$raw_en = obtener_respuesta($prompt_en, $OPENAI_API_KEY, $OPENAI_MODEL, 2200);
file_put_contents(__DIR__ . "/logs/raw_openai_en_$pid.txt", $raw_en);
$clean_en = limpiar_html($raw_en);
$html_en = sanitize_for_db($clean_en);
$meta_en = sanitize_for_db(mb_substr(strip_tags($clean_en), 0, 255, 'UTF-8'));
// === Generar ES ===
$raw_es = obtener_respuesta($prompt_es, $OPENAI_API_KEY, $OPENAI_MODEL, 2200);
file_put_contents(__DIR__ . "/logs/raw_openai_es_$pid.txt", $raw_es);
$clean_es = limpiar_html($raw_es);
$html_es = sanitize_for_db($clean_es);
$meta_es = sanitize_for_db(mb_substr(strip_tags($clean_es), 0, 255, 'UTF-8'));
// === Longitud de contenido ===
$len_en = mb_strlen($html_en);
$len_es = mb_strlen($html_es);
file_put_contents(__DIR__ . "/logs/html_debug_$pid.txt",
"EN ($len_en):\n$html_en\n\nES ($len_es):\n$html_es"
);
if ($len_en < $MIN_HTML_LENGTH || $len_es < $MIN_HTML_LENGTH) {
log_msg("❌ Texto demasiado corto (EN=$len_en / ES=$len_es) PID $pid");
$db->query("UPDATE oc_product_queue
SET processed=1, processed_at=NOW(), result_en=0, result_es=0, needs_verify=1, log='Texto corto'
WHERE product_id=$pid");
continue;
}
// === Guardar ===
$u_title_en = sentence_case("$producto | $STORE_NAME");
$u_h1_en = $producto;
$u_h2_en = sentence_case("benefits and properties of $producto");
$u_title_es = sentence_case("comprar $producto | $STORE_NAME");
$u_h1_es = $producto;
$u_h2_es = sentence_case("propiedades y beneficios de $producto");
$stmt = $db->prepare("UPDATE oc_product_description
SET description=?, meta_description=?, u_title=?, u_h1=?, u_h2=?
WHERE product_id=? AND language_id=?");
$stmt->bind_param('ssssssi', $html_en, $meta_en, $u_title_en, $u_h1_en, $u_h2_en, $pid, $LANG_EN);
if (!$stmt->execute()) log_msg("❌ Error EN $pid: " . $stmt->error);
$stmt->close();
$stmt = $db->prepare("UPDATE oc_product_description
SET description=?, meta_description=?, u_title=?, u_h1=?, u_h2=?
WHERE product_id=? AND language_id=?");
$stmt->bind_param('ssssssi', $html_es, $meta_es, $u_title_es, $u_h1_es, $u_h2_es, $pid, $LANG_ES);
if (!$stmt->execute()) log_msg("❌ Error ES $pid: " . $stmt->error);
$stmt->close();
$db->query("UPDATE oc_product_queue
SET processed=1, processed_at=NOW(), result_en=1, result_es=1, needs_verify=0, log='OK doble prompt'
WHERE product_id=$pid");
log_msg("$pid completado EN/ES (len EN=$len_en | ES=$len_es)");
usleep(100000);
}
log_msg("🏁 Worker finalizado.");

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
# Template core has no hard runtime deps.
# Add project-specific dependencies after running ./scripts/start.sh

280
scripts/agent_status.py Executable file
View File

@@ -0,0 +1,280 @@
#!/usr/bin/env python3
import argparse
import json
import re
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
STATUS_PATH = ROOT / 'work' / 'runtime-status.json'
MATRIX_PATH = ROOT / 'harness' / 'agents.matrix.yml'
WORKFLOW_PATH = ROOT / 'harness' / 'workflow.stages.yml'
ARTIFACTS_DIR = ROOT / 'work' / 'artifacts'
VALID_RUNTIME_STATES = {'idle', 'waiting', 'running', 'blocked', 'done'}
DEFAULT_EMOJIS = {
'leader': '🧭',
'triager': '🧩',
'architect': '🏗️',
'implementer': '🛠️',
'reviewer': '🔍',
'security': '🔒',
'qa': '🧪',
'documenter': '📚',
}
GATE_FILES = {
'reviewer': 'reviewer.json',
'security': 'security.json',
'qa': 'qa.json',
'documenter': 'documenter.md',
'publish': 'publish.json',
'leader': 'leader-close.json',
}
def now_iso():
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
def load_json(path: Path, default=None):
if not path.exists():
return default
return json.loads(path.read_text(encoding='utf-8'))
def save_json(path: Path, payload):
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
def load_role_emojis():
emojis = dict(DEFAULT_EMOJIS)
if not MATRIX_PATH.exists():
return emojis
current_role = None
for line in MATRIX_PATH.read_text(encoding='utf-8').splitlines():
match_role = re.match(r'^ ([a-z_]+):\s*$', line)
if match_role:
current_role = match_role.group(1)
continue
match_emoji = re.match(r'^\s{4}emoji:\s*["\']?(.*?)["\']?\s*$', line)
if match_emoji and current_role:
emojis[current_role] = match_emoji.group(1)
return emojis
def load_roles():
roles = []
if not MATRIX_PATH.exists():
return roles
for line in MATRIX_PATH.read_text(encoding='utf-8').splitlines():
match_role = re.match(r'^ ([a-z_]+):\s*$', line)
if match_role:
roles.append(match_role.group(1))
return roles
def load_stage_names():
stages = []
if not WORKFLOW_PATH.exists():
return stages
for line in WORKFLOW_PATH.read_text(encoding='utf-8').splitlines():
match_stage = re.match(r'^ - name:\s*([a-z_]+)\s*$', line)
if match_stage:
stages.append(match_stage.group(1))
return stages
def default_status():
return {
'feature_id': None,
'stage': 'idle',
'agent': 'leader',
'action': 'Sin ejecución activa',
'state': 'waiting',
'next_agent': 'leader',
'waiting_for': 'Seleccionar una feature pending y actualizar este estado',
'updated_at': now_iso(),
'timeline': [],
}
def load_status():
status = load_json(STATUS_PATH, default_status())
base = default_status()
for key, value in base.items():
status.setdefault(key, value)
if not isinstance(status.get('timeline'), list):
status['timeline'] = []
return status
def gate_status(feature_id):
gates = {}
if not feature_id:
return gates
feature_dir = ARTIFACTS_DIR / feature_id
for gate, filename in GATE_FILES.items():
path = feature_dir / filename
if not path.exists():
gates[gate] = 'pending'
continue
if gate == 'documenter':
gates[gate] = 'approved'
continue
try:
payload = json.loads(path.read_text(encoding='utf-8'))
wanted = 'PUBLISHED' if gate == 'publish' else 'APPROVED'
gates[gate] = 'approved' if payload.get('verdict') == wanted else 'present'
except Exception:
gates[gate] = 'invalid'
return gates
def render_gate(gate, state, emojis):
icon = {
'approved': '',
'pending': '',
'present': '⚠️',
'invalid': '',
}.get(state, '')
label = {
'leader': 'close',
'documenter': 'docs',
'publish': 'publish',
}.get(gate, gate)
return f"{icon} {emojis.get(gate, '')} {label}: {state.upper()}"
def validate_runtime_args(args):
roles = set(load_roles()) or set(DEFAULT_EMOJIS)
stages = set(load_stage_names()) | {'idle'}
if args.agent is not None and args.agent not in roles:
raise SystemExit(f"Invalid agent: {args.agent}. Allowed: {', '.join(sorted(roles))}")
if args.next_agent is not None and args.next_agent not in roles:
raise SystemExit(f"Invalid next-agent: {args.next_agent}. Allowed: {', '.join(sorted(roles))}")
if args.stage is not None and args.stage not in stages:
raise SystemExit(f"Invalid stage: {args.stage}. Allowed: {', '.join(sorted(stages))}")
if args.state is not None and args.state not in VALID_RUNTIME_STATES:
raise SystemExit(f"Invalid state: {args.state}. Allowed: {', '.join(sorted(VALID_RUNTIME_STATES))}")
def show_status():
status = load_status()
emojis = load_role_emojis()
feature_id = status.get('feature_id') or ''
current_agent = status.get('agent', 'leader')
next_agent = status.get('next_agent') or ''
gates = gate_status(status.get('feature_id'))
print('╔══════════════════════════════════════════════════════════════╗')
print('║ ARNES · Runtime Status ║')
print('╚══════════════════════════════════════════════════════════════╝')
print(f"Feature activa : {feature_id}")
print(f"Stage actual : {status.get('stage', '')}")
print(f"Agente actual : {emojis.get(current_agent, '')} {current_agent}")
print(f"Acción : {status.get('action', '')}")
print(f"Estado : {status.get('state', '')}")
print(f"Siguiente : {emojis.get(next_agent, '')} {next_agent}")
print(f"Esperando : {status.get('waiting_for', '')}")
print(f"Actualizado : {status.get('updated_at', '')}")
print()
print('Gates')
if gates:
for gate in ['reviewer', 'security', 'qa', 'documenter', 'publish', 'leader']:
print(f" {render_gate(gate, gates.get(gate, 'pending'), emojis)}")
else:
print(' — Sin feature activa —')
print()
print('Timeline')
timeline = status.get('timeline', [])[-8:]
if not timeline:
print(' — Sin eventos —')
return
for item in timeline:
agent = item.get('agent', 'leader')
emoji = emojis.get(agent, '')
ts = item.get('ts', '')
stage = item.get('stage', '')
state = item.get('state', '')
message = item.get('message', '')
print(f" - {ts} · {emoji} {agent} · {stage} · {state} · {message}")
def set_status(args):
validate_runtime_args(args)
status = load_status()
if args.feature_id is not None:
status['feature_id'] = args.feature_id or None
if args.stage is not None:
status['stage'] = args.stage
if args.agent is not None:
status['agent'] = args.agent
if args.action is not None:
status['action'] = args.action
if args.state is not None:
status['state'] = args.state
if args.next_agent is not None:
status['next_agent'] = args.next_agent
if args.waiting_for is not None:
status['waiting_for'] = args.waiting_for
status['updated_at'] = now_iso()
event_message = args.note or status.get('action') or 'Estado actualizado'
status['timeline'].append({
'ts': status['updated_at'],
'agent': status.get('agent', 'leader'),
'stage': status.get('stage', ''),
'state': status.get('state', ''),
'message': event_message,
})
status['timeline'] = status['timeline'][-20:]
save_json(STATUS_PATH, status)
show_status()
def reset_status(_args):
status = default_status()
status['updated_at'] = now_iso()
save_json(STATUS_PATH, status)
show_status()
def build_parser():
parser = argparse.ArgumentParser(description='Renderiza y actualiza el estado visible de ARNES.')
sub = parser.add_subparsers(dest='command', required=True)
sub.add_parser('show', help='Muestra el panel visible de estado')
set_parser = sub.add_parser('set', help='Actualiza el estado runtime y añade evento a timeline')
set_parser.add_argument('--feature-id')
set_parser.add_argument('--stage')
set_parser.add_argument('--agent')
set_parser.add_argument('--action')
set_parser.add_argument('--state')
set_parser.add_argument('--next-agent')
set_parser.add_argument('--waiting-for')
set_parser.add_argument('--note')
sub.add_parser('reset', help='Resetea el estado runtime a idle')
return parser
def main():
parser = build_parser()
args = parser.parse_args()
if args.command == 'show':
show_status()
elif args.command == 'set':
set_status(args)
elif args.command == 'reset':
reset_status(args)
else:
parser.print_help()
return 1
return 0
if __name__ == '__main__':
raise SystemExit(main())

66
scripts/install_into_repo.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
SRC_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TARGET_INPUT="${1:-}"
if [ -z "$TARGET_INPUT" ]; then
echo "Usage: ./scripts/install_into_repo.sh /path/to/target-repo"
exit 1
fi
mkdir -p "$TARGET_INPUT"
TARGET_ROOT="$(cd "$TARGET_INPUT" && pwd)"
if [ "$TARGET_ROOT" = "$SRC_ROOT" ]; then
echo "Refusing to install ARNES into its own source repository. Use a different project repo."
exit 1
fi
if ! git -C "$TARGET_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "No git repo detected at target. Initializing git repository..."
git -C "$TARGET_ROOT" init >/dev/null
fi
copy_item() {
local item="$1"
if [ -d "$SRC_ROOT/$item" ]; then
mkdir -p "$TARGET_ROOT/$item"
cp -R "$SRC_ROOT/$item"/. "$TARGET_ROOT/$item"/
else
cp "$SRC_ROOT/$item" "$TARGET_ROOT/$item"
fi
}
ITEMS=(
AGENTS.md
AGENTS.local.md.example
CHECKPOINTS.md
HOWTO.md
HOWTO-FEATURE.md
README.md
TEMPLATE.md
Makefile
requirements.txt
backlog
defaults
docs
features
harness
platforms
project
scripts
spec
starter-pack
work
)
for item in "${ITEMS[@]}"; do
copy_item "$item"
done
echo "Installed ARNES core into: $TARGET_ROOT"
echo "Next steps:"
echo " cd $TARGET_ROOT"
echo " ./scripts/start.sh"
echo " ./scripts/verify.sh"

102
scripts/new_ticket.py Executable file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
import json
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BACKLOG = ROOT / 'backlog' / 'features.json'
TYPE_CHOICES = ('feature', 'fix', 'bug', 'chore')
LEVEL_CHOICES = ('low', 'med', 'high')
def ask(prompt, default=''):
value = input(f"{prompt}{' [' + default + ']' if default else ''}: ").strip()
return value if value else default
def ask_choice(prompt, choices, default):
while True:
value = ask(prompt, default).lower()
if value in choices:
return value
print(f"Invalid value. Use one of: {', '.join(choices)}")
def ask_list(prompt, default_csv=''):
raw = ask(prompt, default_csv)
return [item.strip() for item in raw.split(',') if item.strip()]
def next_id(features):
nums = []
for feature in features:
fid = str(feature.get('id', ''))
if fid.startswith('F-') and fid[2:].isdigit():
nums.append(int(fid[2:]))
return f"F-{(max(nums) + 1) if nums else 1:03d}"
def main():
data = json.loads(BACKLOG.read_text(encoding='utf-8'))
features = data.get('features', [])
print('Create ticket (English caveman style).')
ticket_type = ask_choice('Type (feature/fix/bug/chore)', TYPE_CHOICES, 'feature')
title = ask('Title (short EN)', f'{ticket_type.capitalize()} TODO')
problem = ask('Problem (short EN)', 'Need change')
goal = ask('Goal (short EN)', 'Make flow better')
scope_in = ask_list('Scope IN (comma list EN)', 'Core flow')
scope_out = ask_list('Scope OUT (comma list EN)', 'No redesign')
risk = ask_choice('Risk (low/med/high)', LEVEL_CHOICES, 'low')
priority = ask_choice('Priority (low/med/high)', LEVEL_CHOICES, 'med')
print('Acceptance bullets (EN caveman). Empty line to end.')
acceptance = []
while True:
line = input('- ').strip()
if not line:
break
acceptance.append(line)
if not acceptance:
acceptance = [
'Flow works end to end',
'No break old behavior',
'verify.sh is green',
]
fid = next_id(features)
desc = (
f"Problem: {problem}. "
f"Goal: {goal}. "
f"Scope IN: {', '.join(scope_in) or 'none'}. "
f"Scope OUT: {', '.join(scope_out) or 'none'}. "
f"Type: {ticket_type}. Priority: {priority}. Risk: {risk}."
)
features.append({
'id': fid,
'type': ticket_type,
'title': title,
'problem': problem,
'goal': goal,
'scope_in': scope_in,
'scope_out': scope_out,
'priority': priority,
'risk': risk,
'description': desc,
'acceptance': acceptance,
'status': 'pending',
'created_at': str(date.today()),
'gates': {'review': False, 'security': False, 'qa': False},
})
data['features'] = features
rules = data.setdefault('rules', {})
rules.setdefault('valid_types', list(TYPE_CHOICES))
BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
print(f'Created {fid}: {title}')
if __name__ == '__main__':
main()

133
scripts/publish_ticket.py Executable file
View File

@@ -0,0 +1,133 @@
#!/usr/bin/env python3
import argparse
import json
import subprocess
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BACKLOG = ROOT / 'backlog' / 'features.json'
ARTIFACTS = ROOT / 'work' / 'artifacts'
def now_iso():
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
def run_git(*args, check=True):
result = subprocess.run(
['git', *args],
cwd=ROOT,
text=True,
capture_output=True,
)
if check and result.returncode != 0:
msg = result.stderr.strip() or result.stdout.strip() or f'git command failed: {args}'
raise SystemExit(msg)
return result
def ensure_git_repo():
result = run_git('rev-parse', '--is-inside-work-tree', check=False)
if result.returncode != 0 or result.stdout.strip() != 'true':
raise SystemExit('This project is not inside a git repository. Run git init or clone a repo first.')
def load_backlog():
return json.loads(BACKLOG.read_text(encoding='utf-8'))
def find_feature(feature_id):
data = load_backlog()
for feature in data.get('features', []):
if str(feature.get('id')) == feature_id:
return feature
raise SystemExit(f'Feature not found in backlog: {feature_id}')
def current_branch():
branch = run_git('symbolic-ref', '--quiet', '--short', 'HEAD', check=False).stdout.strip()
if not branch:
branch = run_git('rev-parse', '--abbrev-ref', 'HEAD', check=False).stdout.strip()
if not branch or branch == 'HEAD':
raise SystemExit('Detached HEAD is not supported for publish. Checkout a branch first.')
return branch
def ensure_remote(remote):
remotes = [line.strip() for line in run_git('remote').stdout.splitlines() if line.strip()]
if remote not in remotes:
raise SystemExit(f'Remote not found: {remote}. Add it first with git remote add {remote} <url>.')
def status_porcelain():
return run_git('status', '--porcelain').stdout.strip()
def default_commit_message(feature):
feature_id = feature['id']
ticket_type = feature.get('type')
title = str(feature.get('title', '')).strip()
if ticket_type:
return f'{feature_id} {ticket_type}: {title}'
return f'{feature_id}: {title}'
def write_publish_artifact(feature_id, payload):
feature_dir = ARTIFACTS / feature_id
feature_dir.mkdir(parents=True, exist_ok=True)
path = feature_dir / 'publish.json'
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
return path
def ensure_git_identity():
name = run_git('config', 'user.name', check=False).stdout.strip()
email = run_git('config', 'user.email', check=False).stdout.strip()
if not name or not email:
raise SystemExit('Missing git identity. Configure git user.name and user.email before publish.')
def main():
parser = argparse.ArgumentParser(description='Commit and push one ticket, then write publish.json evidence.')
parser.add_argument('--feature-id', required=True)
parser.add_argument('--remote', default='origin')
parser.add_argument('--branch', default='')
parser.add_argument('--commit-message', default='')
args = parser.parse_args()
ensure_git_repo()
ensure_git_identity()
feature = find_feature(args.feature_id)
remote = args.remote.strip() or 'origin'
branch = args.branch.strip() or current_branch()
ensure_remote(remote)
if not status_porcelain():
raise SystemExit('No git changes to publish. Nothing to commit.')
commit_message = args.commit_message.strip() or default_commit_message(feature)
payload = {
'agent': 'leader',
'verdict': 'PUBLISHED',
'feature_id': args.feature_id,
'branch': branch,
'remote': remote,
'message': commit_message,
'pushed': True,
'published_at': now_iso(),
'note': 'This artifact is committed inside the publish commit for this ticket.'
}
artifact_path = write_publish_artifact(args.feature_id, payload)
run_git('add', '-A')
if not status_porcelain():
raise SystemExit('No staged git changes after git add -A. Nothing to commit.')
run_git('commit', '-m', commit_message)
run_git('push', remote, branch)
print(f'done -> {artifact_path}')
if __name__ == '__main__':
main()

215
scripts/start.sh Executable file
View File

@@ -0,0 +1,215 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
ask() {
local prompt="$1"; local def="${2:-}"; local val
if [ -n "$def" ]; then
read -r -p "$prompt [$def]: " val || true
echo "${val:-$def}"
else
read -r -p "$prompt: " val || true
echo "$val"
fi
}
echo "=== ARNES start wizard ==="
echo "Mode: use this template in a new repo or copy core ARNES into an existing repo."
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "No git repo detected. Initializing local git repository..."
git init >/dev/null
fi
PROJECT_NAME="$(ask 'Project name' 'my-project')"
PROJECT_DESC="$(ask 'Project description' 'Project using ARNES template')"
APP_DIR="$(ask 'App directory (relative)' 'project')"
STACK_CHOICE="$(ask 'Stack preset (1=default Flask+MariaDB+Skeleton, 2=custom)' '1')"
if [ "$STACK_CHOICE" = "2" ]; then
BACKEND="$(ask 'Backend stack' 'python/flask')"
DB="$(ask 'Database' 'mariadb')"
CSSFW="$(ask 'CSS framework' 'skeleton')"
else
BACKEND="python/flask"
DB="mariadb"
CSSFW="skeleton"
fi
DEFAULT_TEST_CMD="echo TODO-set-test-command"
if [ "$BACKEND" = "python/flask" ]; then
DEFAULT_TEST_CMD="python3 -m unittest discover -s $APP_DIR/tests -v"
fi
TEST_CMD="$(ask 'Test command' "$DEFAULT_TEST_CMD")"
LINT_CMD="$(ask 'Lint command (optional)' '')"
MODEL_MODE="$(ask 'Model mode (lean/balanced/power)' 'lean')"
ADD_BOOTSTRAP="$(ask 'Create bootstrap ticket F-001 now? (y/n)' 'y')"
mkdir -p "$APP_DIR"
[ -f "$APP_DIR/README.md" ] || cat > "$APP_DIR/README.md" <<EOF
# Project code
This directory holds the real project code.
Configured by ARNES start wizard.
EOF
if [ "$BACKEND" = "python/flask" ]; then
mkdir -p "$APP_DIR/templates" "$APP_DIR/static/js" "$APP_DIR/static/css" "$APP_DIR/static/images" "$APP_DIR/tests"
[ -f "$APP_DIR/tests/test_bootstrap.py" ] || cat > "$APP_DIR/tests/test_bootstrap.py" <<'PY'
import unittest
class BootstrapSmokeTest(unittest.TestCase):
def test_bootstrap(self):
self.assertTrue(True)
if __name__ == '__main__':
unittest.main()
PY
fi
if [ "$CSSFW" = "skeleton" ]; then
mkdir -p "$APP_DIR/static/css" "$APP_DIR/static/images"
[ -f "$APP_DIR/static/css/normalize.css" ] || cp defaults/flask-skeleton/static/css/normalize.css "$APP_DIR/static/css/normalize.css"
[ -f "$APP_DIR/static/css/skeleton.css" ] || cp defaults/flask-skeleton/static/css/skeleton.css "$APP_DIR/static/css/skeleton.css"
[ -f "$APP_DIR/static/images/favicon.png" ] || cp defaults/flask-skeleton/static/images/favicon.png "$APP_DIR/static/images/favicon.png"
fi
cat > harness/project.config.json <<JSON
{
"project_name": "$PROJECT_NAME",
"project_description": "$PROJECT_DESC",
"app_dir": "$APP_DIR",
"stack": {
"backend": "$BACKEND",
"database": "$DB",
"css": "$CSSFW"
},
"commands": {
"test": "$TEST_CMD",
"lint": "$LINT_CMD"
},
"model_mode": "$MODEL_MODE"
}
JSON
cat > scripts/verify.local.sh <<'SH'
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
if [ ! -f "harness/project.config.json" ]; then
echo "[LOCAL] missing harness/project.config.json"
exit 1
fi
APP_DIR=$(python3 - <<'PY'
import json
from pathlib import Path
cfg=json.loads(Path('harness/project.config.json').read_text())
print(cfg.get('app_dir','project'))
PY
)
TEST_CMD=$(python3 - <<'PY'
import json
from pathlib import Path
cfg=json.loads(Path('harness/project.config.json').read_text())
print(cfg.get('commands',{}).get('test',''))
PY
)
LINT_CMD=$(python3 - <<'PY'
import json
from pathlib import Path
cfg=json.loads(Path('harness/project.config.json').read_text())
print(cfg.get('commands',{}).get('lint',''))
PY
)
if [ ! -d "$APP_DIR" ]; then
echo "[LOCAL] app dir not found: $APP_DIR"
exit 1
fi
echo "[LOCAL] app dir OK: $APP_DIR"
if [ -n "$LINT_CMD" ]; then
echo "[LOCAL] lint: $LINT_CMD"
bash -lc "$LINT_CMD"
fi
if [ -n "$TEST_CMD" ]; then
echo "[LOCAL] test: $TEST_CMD"
bash -lc "$TEST_CMD"
fi
echo "[LOCAL] OK"
SH
chmod +x scripts/verify.local.sh
python3 - <<PY
import json
from pathlib import Path
from datetime import date
b = Path('backlog/features.json')
data = json.loads(b.read_text(encoding='utf-8'))
data['project'] = '$PROJECT_NAME'
data['description'] = '$PROJECT_DESC'
rules = data.setdefault('rules', {})
rules.setdefault('valid_types', ['feature', 'fix', 'bug', 'chore'])
features = data.get('features', [])
if '$ADD_BOOTSTRAP'.lower().startswith('y') and not features:
features.append({
'id': 'F-001',
'type': 'chore',
'title': 'Bootstrap ARNES on project',
'problem': 'Need base workflow and control',
'goal': 'Make ARNES ready on this repo',
'scope_in': ['Harness setup', 'Runtime status', 'First verify cycle'],
'scope_out': ['Business feature work', 'Product redesign'],
'priority': 'med',
'risk': 'low',
'description': 'Problem: Need base workflow and control. Goal: Make ARNES ready on this repo. Scope IN: Harness setup, Runtime status, First verify cycle. Scope OUT: Business feature work, Product redesign. Type: chore. Priority: med. Risk: low.',
'acceptance': ['verify.sh is green', 'runtime status works', 'first feature closes with gates'],
'status': 'pending',
'created_at': str(date.today()),
'gates': {'review': False, 'security': False, 'qa': False}
})
data['features'] = features
b.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
PY
cat > work/current.md <<EOF
# Current session
- Feature in progress: _none_
- Orchestrator: _leader_
## Plan
- Pick one pending feature.
- Run ./scripts/verify.sh
- Set runtime status.
## Next step
- Use python3 scripts/new_ticket.py to create first real ticket.
EOF
python3 scripts/agent_status.py reset >/dev/null || true
echo ""
echo "Done. Project configured."
echo "- Config: harness/project.config.json"
echo "- Local checks: scripts/verify.local.sh"
echo "- Ticket tool: python3 scripts/new_ticket.py"
echo "- Publish tool: python3 scripts/publish_ticket.py --feature-id F-001"
echo "- Verify: ./scripts/verify.sh"
echo "- Runtime: python3 scripts/agent_status.py show"
echo "- Reminder: configure git remote before final publish if missing"

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Ejemplo de overlay local por proyecto.
# Copiar a scripts/verify.local.sh y adaptar.
set -euo pipefail
echo "[LOCAL] checks específicos del proyecto"
# Ejemplos:
# alembic check
# pytest -m smoke -q
# npm run lint
echo "[LOCAL] OK"

View File

@@ -12,23 +12,49 @@ fail() { printf "${RED}[FAIL]${NC} %s\n" "$1"; }
EXIT_CODE=0 EXIT_CODE=0
cd "$(dirname "$0")/.." || exit 1
echo "── 1) Verificando estructura base ─────────────────────" echo "── 1) Verificando estructura base ─────────────────────"
required=( required=(
"AGENTS.md" "AGENTS.md"
"CHECKPOINTS.md" "CHECKPOINTS.md"
"README.md"
"HOWTO.md"
"TEMPLATE.md"
"docs/README.md"
"docs/repository-layout.md"
"docs/scripts-reference.md"
"harness/agents.matrix.yml" "harness/agents.matrix.yml"
"harness/workflow.stages.yml" "harness/workflow.stages.yml"
"harness/policies/governance.md" "harness/policies/governance.md"
"harness/policies/security.md" "harness/policies/security.md"
"harness/policies/quality.md" "harness/policies/quality.md"
"harness/policies/language.md"
"harness/policies/model-routing.md"
"harness/models.profiles.yml"
"harness/contracts/handoff.md" "harness/contracts/handoff.md"
"harness/contracts/evidence.schema.json" "harness/contracts/evidence.schema.json"
"spec/product.md" "spec/product.md"
"spec/tech.md" "spec/tech.md"
"spec/acceptance.md" "spec/acceptance.md"
"spec/bdd/README.md"
"spec/bdd/features/README.md"
"spec/sdd/README.md"
"spec/sdd/components/README.md"
"spec/sdd/decisions/README.md"
"features/README.md"
"project/README.md"
"backlog/features.json" "backlog/features.json"
"work/current.md" "work/current.md"
"work/history.md" "work/history.md"
"work/runtime-status.json"
"scripts/agent_status.py"
"scripts/new_ticket.py"
"scripts/publish_ticket.py"
"scripts/install_into_repo.sh"
"scripts/start.sh"
"platforms/pi/README.md"
"platforms/opencode/README.md"
) )
for f in "${required[@]}"; do for f in "${required[@]}"; do
@@ -40,6 +66,20 @@ for f in "${required[@]}"; do
fi fi
done done
echo ""
echo "── 1.5) Validando git repo ───────────────────────────"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
ok "Git repo detectado"
if git remote | grep -q .; then
ok "Git remote configurado"
else
warn "Sin git remote configurado (publish requerirá remote)"
fi
else
fail "Este proyecto debe vivir dentro de un git repo"
EXIT_CODE=1
fi
echo "" echo ""
echo "── 2) Validando backlog + gates ───────────────────────" echo "── 2) Validando backlog + gates ───────────────────────"
python3 - <<'PY' python3 - <<'PY'
@@ -49,6 +89,7 @@ import sys
root = pathlib.Path('.') root = pathlib.Path('.')
path = root / 'backlog' / 'features.json' path = root / 'backlog' / 'features.json'
level_choices = {'low', 'med', 'high'}
try: try:
data = json.loads(path.read_text(encoding='utf-8')) data = json.loads(path.read_text(encoding='utf-8'))
@@ -56,12 +97,19 @@ except Exception as e:
print(f"[FAIL] backlog/features.json inválido: {e}") print(f"[FAIL] backlog/features.json inválido: {e}")
sys.exit(1) sys.exit(1)
valid = set(data.get('rules', {}).get('valid_status', ["pending", "in_progress", "blocked", "done"])) rules = data.get('rules', {})
valid_status = set(rules.get('valid_status', ["pending", "in_progress", "blocked", "done"]))
valid_types = set(rules.get('valid_types', ["feature", "fix", "bug", "chore"]))
features = data.get('features', []) features = data.get('features', [])
if not isinstance(features, list): if not isinstance(features, list):
print('[FAIL] features debe ser una lista') print('[FAIL] features debe ser una lista')
sys.exit(1) sys.exit(1)
ids = [str(f.get('id', '')).strip() for f in features]
if len(ids) != len(set(ids)):
print('[FAIL] Hay IDs de feature duplicados en backlog/features.json')
sys.exit(1)
in_progress = [f for f in features if f.get('status') == 'in_progress'] in_progress = [f for f in features if f.get('status') == 'in_progress']
if len(in_progress) > 1: if len(in_progress) > 1:
print(f"[FAIL] Hay {len(in_progress)} features in_progress (máximo 1)") print(f"[FAIL] Hay {len(in_progress)} features in_progress (máximo 1)")
@@ -70,25 +118,65 @@ if len(in_progress) > 1:
for f in features: for f in features:
fid = str(f.get('id', '')).strip() fid = str(f.get('id', '')).strip()
status = f.get('status') status = f.get('status')
if status not in valid: title = str(f.get('title', '')).strip()
acceptance = f.get('acceptance')
gates = f.get('gates', {})
if not fid:
print('[FAIL] Hay una feature sin id')
sys.exit(1)
if not title:
print(f"[FAIL] Feature {fid} sin title")
sys.exit(1)
if status not in valid_status:
print(f"[FAIL] Estado inválido en feature {fid}: {status}") print(f"[FAIL] Estado inválido en feature {fid}: {status}")
sys.exit(1) sys.exit(1)
if not isinstance(acceptance, list) or not acceptance or any(not str(item).strip() for item in acceptance):
print(f"[FAIL] Feature {fid} debe tener acceptance como lista no vacía")
sys.exit(1)
ticket_type = f.get('type')
if ticket_type is not None and ticket_type not in valid_types:
print(f"[FAIL] Feature {fid} tiene type inválido: {ticket_type}")
sys.exit(1)
for field in ('priority', 'risk'):
value = f.get(field)
if value is not None and value not in level_choices:
print(f"[FAIL] Feature {fid} tiene {field} inválido: {value}")
sys.exit(1)
for field in ('scope_in', 'scope_out'):
value = f.get(field)
if value is not None:
if not isinstance(value, list) or any(not str(item).strip() for item in value):
print(f"[FAIL] Feature {fid} tiene {field} inválido")
sys.exit(1)
if gates:
for gate_name in ('review', 'security', 'qa'):
gate_value = gates.get(gate_name)
if not isinstance(gate_value, bool):
print(f"[FAIL] Feature {fid} tiene gates.{gate_name} inválido")
sys.exit(1)
if status == 'done': if status == 'done':
d = root / 'work' / 'artifacts' / fid d = root / 'work' / 'artifacts' / fid
req = ['reviewer.json', 'security.json', 'qa.json', 'leader-close.json'] req = ['reviewer.json', 'security.json', 'qa.json', 'leader-close.json', 'documenter.md', 'publish.json']
missing = [name for name in req if not (d / name).is_file()] missing = [name for name in req if not (d / name).is_file()]
if missing: if missing:
print(f"[FAIL] Feature {fid} done sin artefactos: {', '.join(missing)}") print(f"[FAIL] Feature {fid} done sin artefactos: {', '.join(missing)}")
sys.exit(1) sys.exit(1)
expected = { expected = {
'reviewer.json': 'reviewer', 'reviewer.json': ('reviewer', 'APPROVED'),
'security.json': 'security', 'security.json': ('security', 'APPROVED'),
'qa.json': 'qa', 'qa.json': ('qa', 'APPROVED'),
'leader-close.json': 'leader', 'leader-close.json': ('leader', 'APPROVED'),
'publish.json': ('leader', 'PUBLISHED'),
} }
for filename, agent in expected.items(): for filename, rule in expected.items():
agent, verdict = rule
try: try:
obj = json.loads((d / filename).read_text(encoding='utf-8')) obj = json.loads((d / filename).read_text(encoding='utf-8'))
except Exception as e: except Exception as e:
@@ -98,16 +186,43 @@ for f in features:
if obj.get('agent') != agent: if obj.get('agent') != agent:
print(f"[FAIL] {fid}/{filename} agent debe ser '{agent}'") print(f"[FAIL] {fid}/{filename} agent debe ser '{agent}'")
sys.exit(1) sys.exit(1)
if obj.get('verdict') != 'APPROVED': if obj.get('verdict') != verdict:
print(f"[FAIL] {fid}/{filename} no está APPROVED") print(f"[FAIL] {fid}/{filename} no está {verdict}")
sys.exit(1)
if filename == 'publish.json' and obj.get('pushed') is not True:
print(f"[FAIL] {fid}/{filename} debe tener pushed=true")
sys.exit(1) sys.exit(1)
print(f"[OK] backlog válido ({len(features)} features)") print(f"[OK] backlog válido ({len(features)} features)")
PY PY
if [ $? -ne 0 ]; then EXIT_CODE=1; fi if [ $? -ne 0 ]; then EXIT_CODE=1; fi
python3 - <<'PY'
import json
import pathlib
import sys
path = pathlib.Path('work/runtime-status.json')
required = ['feature_id', 'stage', 'agent', 'action', 'state', 'next_agent', 'waiting_for', 'updated_at', 'timeline']
try:
data = json.loads(path.read_text(encoding='utf-8'))
except Exception as e:
print(f"[FAIL] work/runtime-status.json inválido: {e}")
sys.exit(1)
missing = [key for key in required if key not in data]
if missing:
print(f"[FAIL] work/runtime-status.json incompleto: {', '.join(missing)}")
sys.exit(1)
if not isinstance(data.get('timeline'), list):
print('[FAIL] work/runtime-status.json timeline debe ser una lista')
sys.exit(1)
print('[OK] runtime-status válido')
PY
if [ $? -ne 0 ]; then EXIT_CODE=1; fi
echo "" echo ""
echo "── 3) Verificación de tests/build (opcional auto-detect) ─" echo "── 3) Verificación de tests/build (auto-detect) ───────"
if [ -f "Makefile" ] && grep -qE '^test:' Makefile; then if [ -f "Makefile" ] && grep -qE '^test:' Makefile; then
if make test; then ok "make test OK"; else fail "make test falló"; EXIT_CODE=1; fi if make test; then ok "make test OK"; else fail "make test falló"; EXIT_CODE=1; fi
elif [ -f "package.json" ]; then elif [ -f "package.json" ]; then
@@ -127,9 +242,24 @@ else
fi fi
echo "" echo ""
echo "── 4) Resumen ─────────────────────────────────────────" echo "── 4) Overlay local opcional ─────────────────────────"
if [ -x "scripts/verify.local.sh" ]; then
if ./scripts/verify.local.sh; then
ok "verify.local.sh OK"
else
fail "verify.local.sh falló"
EXIT_CODE=1
fi
elif [ -f "scripts/verify.local.sh" ]; then
warn "scripts/verify.local.sh existe pero no es ejecutable"
else
warn "Sin overlay local (scripts/verify.local.sh)"
fi
echo ""
echo "── 5) Resumen ─────────────────────────────────────────"
if [ $EXIT_CODE -eq 0 ]; then if [ $EXIT_CODE -eq 0 ]; then
ok "Harness verificado. Puedes trabajar." ok "Harness verificado. Template listo para adaptar a cualquier proyecto."
else else
fail "Harness NO verificado. Corrige antes de continuar." fail "Harness NO verificado. Corrige antes de continuar."
fi fi

View File

@@ -1,9 +1,54 @@
# Acceptance Criteria # Acceptance Spec
Define criterios verificables por feature. ## F-001 — Document and move legacy PHP app into ARNES project layout
Formato recomendado: ### Acceptance criteria
- Feature ID: - Legacy PHP app structure is documented in SDD files.
- Escenario: - Repo layout decision is recorded in one ADR.
- Given / When / Then: - Legacy code moves from `project/new` to `project/web/index/new` with no file loss.
- Evidencia esperada (test/comando): - SQL dump moves from `project/db-25052026.sql` to `project/sql/db-25052026.sql`.
- `./scripts/verify.sh` stays green after the move.
### Evidence targets
- `spec/sdd/architecture.md`
- `spec/sdd/components/*.md`
- `spec/sdd/decisions/001-store-legacy-app-under-project-web.md`
- `spec/bdd/features/layout/legacy-app-layout.feature`
- `work/artifacts/F-001/architect.md`
## F-002 — Remove secrets and externalize config
### Acceptance criteria
- No hard-coded API or DB secrets stay in versioned PHP files.
- Config values load from one local config source for the legacy module.
- Production URLs and external endpoints are configurable.
- Legacy PHP entry points use config helper keys instead of inline values.
- `./scripts/verify.sh` stays green after the change.
### Evidence targets
- `project/web/index/new/bootstrap.php`
- `project/web/index/new/config/local.example.php`
- `project/web/index/new/config/README.md`
- `spec/sdd/components/legacy-config-loader.md`
- `spec/sdd/decisions/002-use-local-config-loader-for-legacy-module.md`
- `spec/bdd/features/config/legacy-config.feature`
- `work/artifacts/F-002/architect.md`
- `work/artifacts/F-002/implementer.md`
## F-003 — Sanitize SQL dump for safe dev use
### Acceptance criteria
- Repo no longer stores the raw production-like SQL dump as the active development baseline.
- Tracked SQL baseline contains only safe synthetic or non-sensitive data for local module work.
- Safe local data handling is documented.
- Local development remains possible through the sanitized baseline and docs.
- `./scripts/verify.sh` stays green after the change.
### Evidence targets
- `project/sql/db-25052026.sql`
- `project/sql/README.md`
- `spec/sdd/components/development-data-baseline.md`
- `spec/sdd/decisions/003-replace-raw-sql-with-sanitized-dev-baseline.md`
- `spec/bdd/features/data/sanitized-sql-baseline.feature`
- `work/artifacts/F-003/architect.md`
- `work/artifacts/F-003/implementer.md`

110
spec/bdd/README.md Normal file
View File

@@ -0,0 +1,110 @@
# BDD — Behavior Driven Development
## Índice
- [Overview](#overview)
- [Features](#features)
- [Step Definitions](#step-definitions)
---
## Overview
Este directorio contiene las especificaciones BDD fuente en formato Gherkin.
Separación recomendada:
- `spec/bdd/features/` = source-of-truth de escenarios
- `features/` = assets ejecutables del runner (steps, config)
### naming conventions
```text
spec/bdd/features/
├── <domain>/
│ ├── <feature-name>.feature
│ └── <feature-name>.feature
└── common/
└── <common-feature>.feature
```
### tags permitidos
| Tag | Uso |
|-----|-----|
| `@F-XXX` | Link a feature del backlog |
| `@smoke` | Tests críticos (siempre ejecutar) |
| `@regression` | Tests de regresión |
| `@integration` | Tests de integración |
| `@e2e` | End-to-end tests |
| `@unit` | Tests unitarios |
| `@api` | Tests de API |
| `@ui` | Tests de interfaz |
---
## Features
Ver `spec/bdd/features/` para los archivos `.feature`.
---
## Step Definitions
Los step definitions deben estar en:
- Python: `features/steps/*.py`
- JS/TS: `features/step_definitions/*.ts`
- Go: `features/steps/*.go`
### Template (Python/Behave)
```python
"""Steps para login feature."""
from behave import given, when, then
@given('un usuario registrado con email "{email}" y password "{password}"')
def step_registered_user(context, email, password):
"""Crea usuario de prueba."""
pass
@when('el usuario ingresa su email "{email}"')
def step_enter_email(context, email):
"""Ingresa email en el formulario."""
pass
@when('ingresa password "{password}"')
def step_enter_password(context, password):
"""Ingresa password."""
pass
@then('el sistema muestra mensaje de error "{message}"')
def step_show_error(context, message):
"""Verifica mensaje de error."""
pass
```
---
## Ejecutar Tests
### Python (Behave)
```bash
behave features/
behave features/ --tags @smoke
behave features/ --tags ~@slow # exclude
```
### Node.js (Cucumber)
```bash
npx cucumber-js features/
npx cucumber-js features/ --tags "@smoke and @F-001"
```
---
## Checklist de Feature
- [ ] Feature documentado en Gherkin
- [ ] Todos los scenarios tienen Given/When/Then
- [ ] Tags `@F-XXX` presentes
- [ ] Step definitions implementados
- [ ] Tests ejecutables

View File

View File

@@ -0,0 +1,12 @@
# BDD feature files
Put Gherkin `.feature` files here.
Example:
- `spec/bdd/features/checkout/purchase.feature`
- `spec/bdd/features/common/error-handling.feature`
Use tags like:
- `@F-001`
- `@smoke`
- `@regression`

View File

@@ -0,0 +1,20 @@
@F-002 @smoke @security @regression
Feature: Legacy module reads config from one local source
As a maintainer
I want secrets and URLs outside tracked PHP files
So I can run the legacy module without storing sensitive values in source
Scenario: Entry points use shared config helper
Given the legacy PHP module has multiple web and CLI entry points
When feature F-002 is applied
Then tracked PHP files do not contain hard-coded DB credentials
And tracked PHP files do not contain hard-coded OpenAI credentials
And DB and route values are loaded through a shared config helper
Scenario: Local config shape is documented
Given a maintainer needs to set local credentials
When feature F-002 is applied
Then the repo contains a versioned local config example
And the repo ignores the real local config file
And setup notes explain how to create the local config

View File

@@ -0,0 +1,18 @@
@F-003 @smoke @security @regression
Feature: Safe SQL baseline exists for legacy module development
As a maintainer
I want a tracked SQL baseline without sensitive live data
So I can develop locally without keeping a raw production snapshot in git
Scenario: Tracked SQL baseline is sanitized
Given the repo contains one tracked SQL baseline for the legacy module
When feature F-003 is applied
Then the tracked SQL baseline does not contain customer or live order snapshot data
And the baseline contains only safe schema and synthetic seed data needed for local module work
Scenario: Local private data handling is documented
Given a maintainer may still need a private raw dump outside git
When feature F-003 is applied
Then the repo documents where private local data should live
And the tracked SQL baseline remains safe for commit and push

View File

@@ -0,0 +1,25 @@
@F-001 @smoke @regression
Feature: Legacy app lives in stable ARNES layout
As a maintainer
I want the copied legacy PHP app in a stable repo path
So I can trace design and change code safely
Scenario: Web module path is explicit
Given the repo contains legacy PHP product module files
When feature F-001 is applied
Then the module lives under "project/web/index/new"
And the old temporary path "project/new" is removed
Scenario: Development SQL baseline is explicit
Given the repo contains one SQL dump for local development
When feature F-001 is applied
Then the dump lives under "project/sql/db-25052026.sql"
And the dump is referenced by SDD docs as development baseline only
Scenario: Design trace exists for the move
Given feature F-001 is in progress
When the design stage is complete
Then SDD architecture docs exist for the legacy app
And one ADR records the repo layout move
And architect evidence exists under "work/artifacts/F-001/architect.md"

View File

@@ -1,15 +1,72 @@
# Product Spec # Product Spec
## Problema ## Problem
Describe el problema de negocio. Legacy PHP app lives in temporary path `project/new`.
SQL dump lives mixed with app code.
There is no ARNES design record for this code.
This makes next change work risky and hard to trace.
## Objetivo ## Objective
Define el resultado esperado del producto. Put legacy app in stable ARNES project layout.
Keep same code and same behavior for now.
Make next work easy to trace, review, and test.
## Usuarios ## Users
- Usuario principal: - Primary user: maintainer of legacy PHP app
- Usuario secundario: - Secondary user: architect, implementer, reviewer, qa
## Alcance v1 ## Scope v1
- In scope: - In scope:
- document current legacy app structure
- define target repo layout
- move app code to `project/web/index/new`
- move SQL dump to `project/sql/db-25052026.sql`
- Out of scope: - Out of scope:
- auth rewrite
- OpenAI secret cleanup
- production deploy
- feature refactor
## F-002 — Remove secrets and externalize config
### Problem
Legacy PHP files still contain API keys, DB credentials, and production URLs.
This blocks security approval and makes local setup unsafe.
### Objective
Load config from one local source outside versioned code.
Keep page behavior the same while removing hard-coded secrets from tracked PHP files.
### Scope
- In scope:
- one config loader for legacy module
- one local config file shape for DB, OpenAI, URLs, and endpoints
- replace hard-coded values in tracked PHP files
- setup notes for local config
- Out of scope:
- auth redesign
- worker refactor beyond config use
- deploy automation
## F-003 — Sanitize SQL dump for safe dev use
### Problem
Current SQL dump in repo looks like a production snapshot.
It contains sensitive and production-like data.
This is unsafe as a tracked development baseline.
### Objective
Replace the raw dump in the working tree with a safe development baseline.
Keep local development possible for the legacy PHP module.
Document how to handle private data outside git.
### Scope
- In scope:
- define safe SQL baseline strategy
- replace current tracked dump with sanitized development dump
- document private local dump handling
- keep module development possible with synthetic seed data
- Out of scope:
- production database changes
- app logic changes
- full OpenCart dataset preservation

315
spec/sdd-bdd-guide.md Normal file
View File

@@ -0,0 +1,315 @@
# SDD/BDD Guide — System Design Document & Behavior Driven Development
Guía para crear y mantener SDD (System Design Document) y BDD (Behavior Driven Development) specs dentro del framework ARNES.
---
## 📐 Propósito
- **SDD**: Documenta el diseño técnico del sistema (arquitectura, componentes, decisiones).
- **BDD**: Documenta el comportamiento esperado desde la perspectiva del usuario/negocio.
Ambos alimentan el pipeline de agentes y se versionan junto con el código.
---
## 🔗 Relación con ARNES
```
spec/
├── product.md # Qué construir (negocio)
├── tech.md # Stack y decisiones técnicas
├── acceptance.md # Criterios de aceptación (BDD light)
├── sdd/ # System Design Document
│ ├── README.md
│ ├── architecture.md
│ ├── components/
│ └── decisions/
└── bdd/ # Behavior Driven Development source-of-truth
├── README.md
└── features/
features/ # optional executable BDD runner assets
├── behave.ini
└── steps/
```
---
## 📋 SDD — System Design Document
### Objetivos
1. Definir arquitectura del sistema
2. Documentar componentes y sus responsabilidades
3. Registrar decisiones técnicas (ADRs)
4. Servir como fuente de verdad para `architect` y `implementer`
### Estructura de un SDD
```
spec/sdd/
├── README.md # Índice y overview
├── architecture.md # Vista general (contexto, capas)
├── components/ # Componentes individuales
│ ├── component-name.md
│ └── ...
└── decisions/ # Architecture Decision Records
├── 001-decision-title.md
└── ...
```
### Template: component.md
```markdown
# Componente: <Nombre>
## Responsabilidad
Descripción clara de qué hace este componente.
## Interfaces
- **Entrada**: API, eventos, mensajes
- **Salida**: Respuestas, side effects
## Dependencias
- Servicio A (tipo de dependencia)
- Base de datos B
## Límites
- Qué NO hace este componente
- Restricciones conocidas
## Criterios de éxito
- [ ] Mecanismo de verificación
- [ ] Métrica de performance
```
### Template: ADR (Architecture Decision Record)
```markdown
# ADR-XXX: <Título>
## Estado
Aceptado | Deprecado | Propuesto
## Contexto
Problema que motiva esta decisión.
## Decisión
Qué se decidió y por qué.
## Consecuencias
- ✅ Positivos
- ❌ Negativos
## Alternativas consideradas
1. Opción A - razón de descarte
2. Opción B - razón de descarte
## Fecha
YYYY-MM-DD
```
---
## 🎯 BDD — Behavior Driven Development
### Objetivos
1. Definir comportamiento del sistema en lenguaje de negocio
2. Crear trazabilidad entre requisitos y tests
3. Servir como especificación ejecutable para `implementer` y `qa`
### Formato: Gherkin
Usar sintaxis Gherkin para todos los features:
```gherkin
Feature: <Nombre del feature>
Como <actor>
Quiero <acción>
Para <beneficio>
Scenario: <escenario positivo>
Given <contexto inicial>
And <más contexto>
When <acción del usuario>
And <otra acción>
Then <resultado esperado>
And <otro resultado>
Scenario: <escenario negativo>
Given <contexto>
When <acción que falla>
Then <error esperado>
```
### Estructura de un Feature BDD
```
spec/bdd/features/
├── README.md
├── auth/
│ ├── login.feature
│ └── registration.feature
├── checkout/
│ └── purchase.feature
└── common/
└── error-handling.feature
features/
├── behave.ini
└── steps/
└── login_steps.py
```
### Tags para trazabilidad
```gherkin
@F-001 @auth @smoke
Feature: Inicio de sesión
@regression @slow
Scenario: Login con credenciales válidas
...
```
Tags disponibles:
- `@F-XXX` — Link a feature ID del backlog
- `@smoke` — Test crítico (ejecutar siempre)
- `@regression` — Tests de regresión
- `@integration` — Tests de integración
- `@e2e` — End-to-end tests
---
## 🔄 Flujo de trabajo SDD/BDD en ARNES
### Stage: design (architect)
1. **Crear/actualizar SDD**
- Definir componentes nuevos
- Documentar decisiones técnicas
- Crear ADRs cuando haya cambios
2. **Crear/actualizar BDD**
- Traducir requisitos de `spec/product.md` a Gherkin
- Crear scenarios para cada criterio de aceptación
- Asegurar que cada scenario tenga link a `@F-XXX`
3. **Producir artefacto**
- Archivo: `work/artifacts/<feature_id>/architect.md`
- Contenido: resumen de cambios en SDD/BDD
### Stage: build (implementer)
1. **Implementar código** que cumpla los scenarios BDD
2. **Escribir tests** que ejecuten los scenarios
3. **Actualizar SDD** si hay cambios en componentes
### Stage: review_gate (reviewer)
1. **Verificar** que el código implementa lo documentado en SDD
2. **Verificar** que tests cubren los scenarios BDD
3. **Producir** `work/artifacts/<feature_id>/reviewer.json`
### Stage: qa_gate (qa)
1. **Ejecutar** tests BDD (feature files)
2. **Verificar** trazabilidad: todos los `@F-XXX` tienen tests
3. **Producir** `work/artifacts/<feature_id>/qa.json`
---
## 🛠 Herramientas recomendadas
| Propósito | Herramienta | Notas |
|-----------|-------------|-------|
| BDD test runner | Behave (Python) / Cucumber (JS/Java) | Ejecuta .feature files |
| SDD docs | Markdown + Mermaid diagrams | Portable y versionable |
| ADRs |adr-tools o manual | Mantener en `decisions/` |
### Ejemplo: Python Behave
```bash
# Estructura
spec/bdd/features/
└── login.feature
features/
└── steps/
└── login_steps.py
# Ejecutar
behave features/
```
### Ejemplo: Node.js Cucumber
```bash
# Estructura
spec/bdd/features/
└── login.feature
features/
└── step_definitions/
└── login_steps.js
# Ejecutar
npx cucumber-js features/
```
---
## 📊 Checklist de calidad SDD/BDD
### SDD Quality
- [ ] Cada componente tiene responsabilidad clara
- [ ] Interfaces están documentadas
- [ ] ADRs para decisiones importantes
- [ ] Diagramas Mermaid para arquitectura
### BDD Quality
- [ ] Cada feature tiene al menos un scenario
- [ ] Todos los scenarios usan Given/When/Then
- [ ] Tags `@F-XXX` para trazabilidad con backlog
- [ ] Scenarios son atómicos (no dependen de estado previo)
---
## 🚫 Reglas anti-trampa
1. **SDD no es decoration**: debe reflejar la realidad del código
2. **BDD no es documentación de tests**: es especificación executable
3. **Discrepancia = bug**: si SDD dice A pero código hace B, el código está mal
4. **Sin scenario = sin acceptance**: feature sin BDD scenario no puede cerrarse
---
## 📝 Formato de artefacto architect.md
```markdown
# Architect Artefact — Feature: F-XXX
## SDD Changes
- Componentes afectados: [...]
- ADRs creados/actualizados: [...]
## BDD Coverage
- Features/Scenarios nuevos: [...]
- Coverage: X/Y scenarios cubiertos por tests
## Decisiones técnicas
- Decisión 1: razón
- Decisión 2: razón
## Riesgos identificados
- Riesgo 1: mitigación
```
---
## 🔗 Referencias
- [Gherkin Reference](https://cucumber.io/docs/gherkin/)
- [MADR (Markdown Any Decision Records)](https://adr.github.io/madr/)
- [BDD with Behave](https://behave.readthedocs.io/)

67
spec/sdd/README.md Normal file
View File

@@ -0,0 +1,67 @@
# SDD — System Design Document
## Índice
- [Architecture Overview](#architecture-overview)
- [Components](#components)
- [Decisions](#decisions)
---
## Architecture Overview
```mermaid
graph TD
subgraph Frontend
F[Client App]
end
subgraph Backend
A[API Gateway]
S[Services]
D[(Database)]
end
F --> A
A --> S
S --> D
```
### Contexto
_Describir el propósito del sistema y su alcance._
### Restricciones
- _Lista de restricciones técnicas o de negocio_
---
## Components
### Component Template
Ver `spec/sdd/components/.template.md` para el formato.
---
## Decisions
Ver `spec/sdd/decisions/` para ADRs.
---
## Diagrama de secuencia (ejemplo)
```mermaid
sequenceDiagram
actor U as User
participant API
participant SVC as Service
participant DB as Database
U->>API: Request
API->>SVC: Process
SVC->>DB: Query
DB-->>SVC: Result
SVC-->>API: Response
API-->>U: Data
```

47
spec/sdd/architecture.md Normal file
View File

@@ -0,0 +1,47 @@
# Architecture Overview — Legacy PHP Product Module
## Context
This repo holds one legacy PHP module copied from production.
The module helps product staff create products and generate SEO text.
The module also runs one batch worker that updates OpenCart product descriptions.
Current raw source path was `project/new`.
Target stable path is `project/web/index/new`.
SQL baseline path is `project/sql/db-25052026.sql` and now contains sanitized synthetic development data.
## Main flows
1. User opens product form.
2. Form reads OpenCart data from MariaDB.
3. User can open AI helper page for one product text.
4. Bulk page writes product ids into `oc_product_queue`.
5. CLI worker reads queue, calls OpenAI, updates `oc_product_description`.
## Constraints
- Keep legacy behavior unchanged in layout and config features.
- Preserve file contents during move unless config externalization requires value lookup changes.
- Keep evidence in repo for each design change.
- Do not redesign auth or deploy in these features.
## System view
```mermaid
graph TD
U[Backoffice user] --> F[Legacy PHP web module]
F --> DB[(MariaDB / OpenCart)]
F --> Q[oc_product_queue]
W[worker_bulk.php CLI worker] --> Q
W --> AI[OpenAI API]
W --> DB
```
## Files and responsibilities
- `bootstrap.php`: shared local config loader and DB helper
- `config/local.example.php`: versioned config shape
- `config/local.php`: ignored local values file
- `index.php`: manual product create form
- `describe.php`: one-shot AI description helper
- `productos_bulk_update.php`: queue intake and worker log viewer
- `productos_modificados.php`: review processed items
- `worker_bulk.php`: batch generator and DB updater
- `inc/*`: shared layout, prompts, AJAX helpers
- `db/conn.php`: shared DB connection for web pages
- `logs/*`: runtime debug output from worker and AI calls

View File

View File

@@ -0,0 +1,8 @@
# SDD components
Put one markdown file per technical component.
Example:
- `api-gateway.md`
- `order-service.md`
- `cart-repository.md`

View File

@@ -0,0 +1,33 @@
# Component: Bulk SEO worker
## Responsibility
Read product ids from queue.
Call OpenAI with EN and ES prompts.
Clean output.
Update OpenCart product description fields.
Write processing logs.
## Interfaces
- Input:
- CLI run of `worker_bulk.php`
- rows from `oc_product_queue`
- prompt files `inc/prompt_en.md` and `inc/prompt_es.md`
- Output:
- updates in `oc_product_description`
- status fields in `oc_product_queue`
- log files under `logs/`
## Dependencies
- MariaDB/MySQL
- OpenAI Chat Completions API
- local prompt markdown files
## Limits
- No secret management yet.
- No retry queue store outside DB.
- No metrics or structured logs.
## Success criteria
- [ ] Worker path is documented
- [ ] Queue and DB side effects are known
- [ ] Log location is explicit in design docs

View File

@@ -0,0 +1,25 @@
# Component: Development data baseline
## Responsibility
Provide one safe local SQL baseline so maintainers can seed a development database for the legacy PHP module.
## Interfaces
- Input:
- SQL import command run by maintainer
- Output:
- local MariaDB database with the schema and synthetic seed data needed by the module
## Dependencies
- `project/sql/db-25052026.sql`
- `project/sql/README.md`
- local MariaDB/MySQL server
## Limits
- Baseline is intentionally smaller than the former raw snapshot.
- Baseline covers current module needs, not the full production dataset.
- Private raw snapshots must stay outside git.
## Success criteria
- [ ] Dump path is stable and explicit
- [ ] Tracked dump contains only safe synthetic or non-sensitive data
- [ ] Docs explain private local dump handling

View File

@@ -0,0 +1,30 @@
# Component: Legacy config loader
## Responsibility
Load local configuration for the legacy PHP module.
Expose helper access for DB, OpenAI, URLs, endpoints, and path values.
Provide one DB connection factory used by web pages and worker.
## Interfaces
- Input:
- `config/local.php` if present
- fallback `config/local.example.php` for shape and safe defaults
- Output:
- config access helpers
- mysqli connection helper
- normalized path values for logs and routes
## Dependencies
- PHP array config files
- `mysqli`
- module root path
## Limits
- Does not manage secret rotation.
- Does not validate remote credentials.
- Does not redesign auth or downstream business logic.
## Success criteria
- [ ] No tracked PHP file contains hard-coded DB or OpenAI secrets
- [ ] Entry points use shared config helper
- [ ] Local setup path is documented

View File

@@ -0,0 +1,32 @@
# Component: Legacy web module
## Responsibility
Serve old PHP pages for product create and product SEO work.
Render HTML.
Read OpenCart data.
Write queue rows for batch processing.
## Interfaces
- Input:
- browser GET and POST requests
- session state from external login flow
- Output:
- HTML pages
- inserts into `oc_product_queue`
- writes brand rows and URL alias rows
## Dependencies
- `db/conn.php`
- `inc/header.php`, `inc/footer.php`
- OpenCart tables
- external `success.php` and `login.php` outside repo
## Limits
- Does not own authentication.
- Does not own final product creation endpoint.
- Uses hard-coded config today.
## Success criteria
- [ ] Module files live under stable repo path
- [ ] Relative module structure stays intact
- [ ] Pages can still be reviewed as one legacy unit

View File

View File

@@ -0,0 +1,33 @@
# ADR-001: Store legacy app under project web path
## Status
Accepted
## Context
Legacy PHP code was copied into `project/new`.
That path does not explain app role.
SQL dump also sits beside code in `project/` root.
We need stable layout before deeper refactor.
## Decision
Store legacy web code under `project/web/index/new`.
Store SQL dump under `project/sql/db-25052026.sql`.
Keep internal legacy file tree unchanged inside module.
Do not refactor code in same step.
## Consequences
- Good:
- repo layout shows what is web code and what is data
- ARNES design docs can point to stable paths
- future config and secret cleanup gets easier
- Bad:
- move may require path-aware follow-up in later features
- repo still contains legacy secrets until later cleanup
## Alternatives considered
1. Keep code in `project/new` - rejected because path is temporary and vague.
2. Move code to `project/app` - rejected because this is web module, not service code.
3. Refactor layout and code now - rejected because scope would grow too much.
## Date
2026-05-25

View File

@@ -0,0 +1,33 @@
# ADR-002: Use local config loader for legacy module
## Status
Accepted
## Context
Security gate for F-001 failed.
Legacy PHP files still hold DB credentials, OpenAI keys, and production-coupled URLs.
The module needs one small config mechanism without large refactor.
## Decision
Add `bootstrap.php` to the legacy module root.
Load config from `config/local.php` with fallback to `config/local.example.php`.
Expose shared helper functions for config lookup and DB connection.
Update web pages and worker to read DB, OpenAI, route, and URL values through this helper.
Ignore `config/local.php` in git.
## Consequences
- Good:
- secrets leave tracked PHP source files
- one config shape is reused by web pages and worker
- local setup becomes explicit
- Bad:
- module still depends on local file management
- fallback example config can still fail at runtime until maintainer fills real values
## Alternatives considered
1. Use environment variables only - rejected because this legacy module already expects file-based setup.
2. Keep secrets in PHP constants - rejected because tracked source would still hold sensitive values.
3. Full framework migration - rejected because scope is too large for this fix.
## Date
2026-05-25

View File

@@ -0,0 +1,31 @@
# ADR-003: Replace raw SQL snapshot with sanitized dev baseline
## Status
Accepted
## Context
The tracked SQL file under `project/sql/db-25052026.sql` looked like a production snapshot.
It exposed production-like and sensitive data in the working tree.
The legacy PHP module still needs a database baseline for local work.
## Decision
Keep the same tracked SQL path but replace its content with a sanitized development baseline.
The new baseline contains only the schema and synthetic seed data needed by the legacy PHP module.
Document how to keep any private raw dump outside git.
## Consequences
- Good:
- active repo tree stops shipping raw sensitive SQL data
- local setup remains possible with a smaller safe dataset
- module development gets a focused baseline for current pages and worker
- Bad:
- baseline no longer mirrors the full production dataset
- some future work may need extra synthetic fixtures
## Alternatives considered
1. Keep raw dump and add warning only - rejected because data risk remains in tracked files.
2. Remove all SQL baseline files - rejected because local development would become harder.
3. Rewrite full git history now - rejected because scope is too large for this feature.
## Date
2026-05-25

View File

@@ -0,0 +1,7 @@
# SDD decisions
Put ADRs (Architecture Decision Records) here.
Example:
- `001-use-flask.md`
- `002-use-mariadb.md`

View File

@@ -1,19 +1,43 @@
# Technical Spec # Technical Spec
## Stack ## Stack
- Lenguaje: - Language: PHP, JavaScript, CSS
- Framework: - Framework: legacy custom PHP + OpenCart database schema
- Runtime: - Runtime: Apache/Nginx + PHP, MariaDB/MySQL, CLI worker for batch jobs
## Restricciones ## Restrictions
- Seguridad: - Security:
- Rendimiento: - do not expose secrets in new docs
- Compatibilidad: - keep real dump as local dev input only
- Performance:
- file move must not change app code behavior
- Compatibility:
- preserve relative file structure inside legacy module
- preserve SQL dump file content
## Dependencias ## Dependencies
Lista y justificación de dependencias externas. - MariaDB/MySQL dump from `project/sql/db-25052026.sql`
- OpenCart tables like `oc_product`, `oc_product_description`, `oc_product_queue`
- OpenAI API used by legacy scripts
- External login and success endpoints exist outside this repo
## Observabilidad ## Observability
- Logging: - Logging:
- Métricas: - current legacy logs live under module `logs/`
- Alertas: - Metrics:
- none in repo now
- Alerts:
- none in repo now
## F-002 technical notes
- Add `bootstrap.php` in legacy module root.
- Add config files under `project/web/index/new/config/`.
- Versioned file stores example values only.
- Ignored local file stores real local secrets and URLs.
- All PHP entry points must read DB, OpenAI, and route values through config helper.
## F-003 technical notes
- Keep one tracked SQL baseline for safe local development.
- Baseline should contain synthetic or non-sensitive seed data only.
- Baseline should cover the tables needed by the legacy module pages and worker.
- Private raw dumps must stay outside git or in ignored local paths only.

28
starter-pack/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Starter Pack (rápido)
Este pack sirve para arrancar ARNES en 2 escenarios:
## A) Proyecto nuevo (greenfield)
1. Crea repo vacío.
2. Instala el core ARNES desde otro repo fuente (`scripts/install_into_repo.sh`).
3. Ajusta `backlog/features.json` (`project`, `description`).
4. Copia `starter-pack/backlog.features.bootstrap.json` como primera feature (`type=chore`).
5. Ejecuta:
- `./scripts/verify.sh`
- `python3 scripts/agent_status.py show`
## B) Proyecto ya empezado (brownfield)
1. Instala **solo** el core ARNES dentro del repo existente.
2. Mantén tu código actual intacto.
3. Añade checks del dominio en `scripts/verify.local.sh`.
4. Define features reales del proyecto en `backlog/features.json`.
5. Ejecuta:
- `./scripts/verify.sh`
- `python3 scripts/agent_status.py show`
## Reglas mínimas
- 1 sola feature en `in_progress`.
- Tipos válidos: `feature`, `fix`, `bug`, `chore`.
- `done` requiere gates: `review/security/qa`.
- `done` requiere commit+push final del ticket.
- Evidencia en `work/artifacts/<feature_id>/`.

View File

@@ -0,0 +1,31 @@
{
"id": "F-001",
"type": "chore",
"title": "Bootstrap ARNES on project",
"problem": "Need base workflow and control",
"goal": "Make ARNES ready on this repo",
"scope_in": [
"Harness setup",
"Runtime status",
"First verify cycle"
],
"scope_out": [
"Business feature work",
"Product redesign"
],
"priority": "med",
"risk": "low",
"description": "Problem: Need base workflow and control. Goal: Make ARNES ready on this repo. Scope IN: Harness setup, Runtime status, First verify cycle. Scope OUT: Business feature work, Product redesign. Type: chore. Priority: med. Risk: low.",
"acceptance": [
"verify.sh is green",
"runtime status works",
"first feature closes with gates"
],
"status": "pending",
"created_at": "YYYY-MM-DD",
"gates": {
"review": false,
"security": false,
"qa": false
}
}

112
tests/test_arnes_core.py Normal file
View File

@@ -0,0 +1,112 @@
import json
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
INSTALL = ROOT / 'scripts' / 'install_into_repo.sh'
class ArnesCoreTests(unittest.TestCase):
def run_cmd(self, args, cwd=None, input_text=None, check=True):
result = subprocess.run(
args,
cwd=cwd or ROOT,
input=input_text,
text=True,
capture_output=True,
)
if check and result.returncode != 0:
raise AssertionError(
f"Command failed: {args}\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
)
return result
def test_install_refuses_source_repo(self):
result = self.run_cmd([str(INSTALL), str(ROOT)], check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn('Refusing to install ARNES into its own source repository', result.stdout)
def test_install_start_verify_and_publish_in_external_repo(self):
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
target = tmp_path / 'project-repo'
remote = tmp_path / 'remote.git'
self.run_cmd([str(INSTALL), str(target)])
self.assertTrue((target / 'scripts' / 'start.sh').is_file())
self.assertFalse((target / 'tests').exists(), 'source self-tests must not be installed into project repo')
self.run_cmd(['git', 'config', 'user.name', 'ARNES Bot'], cwd=target)
self.run_cmd(['git', 'config', 'user.email', 'arnes@example.com'], cwd=target)
self.run_cmd(['git', 'init', '--bare', str(remote)])
self.run_cmd(['git', 'remote', 'add', 'origin', str(remote)], cwd=target)
wizard_input = '\n'.join([
'demo-project',
'Demo project',
'project',
'1',
'',
'',
'',
'',
]) + '\n'
self.run_cmd(['./scripts/start.sh'], cwd=target, input_text=wizard_input)
self.assertTrue((target / 'project' / 'tests').is_dir())
cfg = json.loads((target / 'harness' / 'project.config.json').read_text(encoding='utf-8'))
self.assertEqual(cfg['app_dir'], 'project')
self.assertEqual(cfg['commands']['test'], 'python3 -m unittest discover -s project/tests -v')
verify_result = self.run_cmd(['./scripts/verify.sh'], cwd=target)
self.assertIn('Harness verificado', verify_result.stdout)
self.run_cmd(['python3', 'scripts/new_ticket.py'], cwd=target, input_text='\n'.join([
'fix',
'Repair docs',
'Need docs repair',
'Make docs clear',
'Docs flow',
'No redesign',
'low',
'med',
'Docs clear',
'Verify green',
'',
]) + '\n')
with (target / 'project' / 'README.md').open('a', encoding='utf-8') as fh:
fh.write('\nchange\n')
publish_result = self.run_cmd(
['python3', 'scripts/publish_ticket.py', '--feature-id', 'F-001'],
cwd=target,
)
self.assertIn('done ->', publish_result.stdout)
publish_path = target / 'work' / 'artifacts' / 'F-001' / 'publish.json'
payload = json.loads(publish_path.read_text(encoding='utf-8'))
self.assertEqual(payload['verdict'], 'PUBLISHED')
self.assertTrue(payload['pushed'])
self.assertEqual(payload['remote'], 'origin')
status_result = self.run_cmd(['git', 'status', '--short'], cwd=target)
self.assertEqual(status_result.stdout.strip(), '')
remote_refs = self.run_cmd(['git', 'ls-remote', 'origin'], cwd=target)
self.assertIn('refs/heads/master', remote_refs.stdout)
def test_agent_status_rejects_invalid_agent(self):
result = self.run_cmd(
['python3', 'scripts/agent_status.py', 'set', '--agent', 'nope'],
check=False,
)
self.assertNotEqual(result.returncode, 0)
combined = (result.stdout + result.stderr)
self.assertIn('Invalid agent: nope', combined)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,31 @@
# Architect Artefact — Feature: F-001
## SDD Changes
- Added `spec/sdd/architecture.md`
- Added component docs:
- `spec/sdd/components/legacy-web-module.md`
- `spec/sdd/components/bulk-seo-worker.md`
- `spec/sdd/components/development-data-baseline.md`
- Added ADR:
- `spec/sdd/decisions/001-store-legacy-app-under-project-web.md`
## BDD Coverage
- Added `spec/bdd/features/layout/legacy-app-layout.feature`
- Coverage target:
- stable web module path
- stable SQL dump path
- design trace exists
## Technical decisions
- Use `project/web/index/new` as stable repo path for copied legacy web code.
- Use `project/sql/db-25052026.sql` as stable path for local dev dump.
- Keep legacy internal module tree unchanged in this feature.
## Risks found
- Legacy code has hard-coded secrets and production URLs.
- External files `login.php` and `success.php` are not in repo.
- SQL dump may contain production-like data.
## Next step
- Move files with no content change.
- Re-run verify after move.

View File

@@ -0,0 +1,24 @@
# Documenter Artefact — Feature: F-001
## What changed
- Added SDD record for legacy PHP product module.
- Added ADR for stable repo layout.
- Added BDD trace for layout move.
- Moved legacy web code to `project/web/index/new`.
- Moved SQL dump to `project/sql/db-25052026.sql`.
## Files to know
- `spec/sdd/architecture.md`
- `spec/sdd/components/legacy-web-module.md`
- `spec/sdd/components/bulk-seo-worker.md`
- `spec/sdd/components/development-data-baseline.md`
- `spec/sdd/decisions/001-store-legacy-app-under-project-web.md`
- `spec/bdd/features/layout/legacy-app-layout.feature`
- `project/web/index/new/`
- `project/sql/db-25052026.sql`
## Notes for next feature
- Remove secrets from PHP files.
- Externalize DB and API config.
- Replace production-coupled URLs and missing external dependencies.
- Move runtime logs out of web tree.

View File

@@ -0,0 +1,38 @@
# Implementer Artefact — Feature: F-001
## Summary
Moved legacy PHP module into stable ARNES project path.
Moved SQL dump into stable project data path.
Kept legacy module internal tree unchanged.
Updated local project path docs.
## Changes
- moved `project/new` -> `project/web/index/new`
- moved `project/db-25052026.sql` -> `project/sql/db-25052026.sql`
- updated `project/README.md`
- updated `spec/tech.md`
- updated `work/current.md`
## Evidence
- before file count in module: `124`
- after file count in module: `124`
- before size of module: `640K`
- after size of module: `640K`
- SQL dump present after move: `project/sql/db-25052026.sql` (`229M`)
- old path removed: `project/new`
- verify result: `./scripts/verify.sh` OK
## Checks run
- `find project/new -type f | wc -l`
- `du -sh project/new project/db-25052026.sql`
- `mv project/new project/web/index/new`
- `mv project/db-25052026.sql project/sql/db-25052026.sql`
- `find project -maxdepth 4 | sort`
- `find project/web/index/new -type f | wc -l`
- `du -sh project/web/index/new project/sql/db-25052026.sql`
- `./scripts/verify.sh`
## Notes
- No functional refactor done.
- No secret cleanup done.
- External dependencies `login.php` and `success.php` still live outside repo.

View File

@@ -0,0 +1,22 @@
{
"feature_id": "F-001",
"agent": "qa",
"verdict": "APPROVED",
"summary": "Acceptance criteria for layout move are satisfied. Design trace exists, target paths exist, old path is removed, and core harness verification is green.",
"traceability": [
"AC: SDD docs exist and explain current legacy app structure -> spec/sdd/architecture.md and component docs created",
"AC: ADR records why code moves under project/web and SQL under project/sql -> spec/sdd/decisions/001-store-legacy-app-under-project-web.md",
"AC: Legacy code is moved with same contents and no file loss -> implementer evidence shows same file count and size before/after",
"AC: SQL dump is kept as local development baseline in project/sql -> project/sql/db-25052026.sql exists and is referenced in docs",
"AC: verify.sh is green -> ./scripts/verify.sh passed after move"
],
"evidence": [
"Reviewed spec/bdd/features/layout/legacy-app-layout.feature",
"Reviewed work/artifacts/F-001/implementer.md",
"Checked project/web/index/new exists",
"Checked project/sql/db-25052026.sql exists",
"Checked project/new is removed",
"Checked ./scripts/verify.sh output is OK"
],
"timestamp": "2026-05-25T05:45:00Z"
}

View File

@@ -0,0 +1,14 @@
{
"feature_id": "F-001",
"agent": "reviewer",
"verdict": "APPROVED",
"summary": "Layout move is correct. SDD and BDD trace exist. Legacy module and SQL dump now live in explicit stable paths. No file loss was found in move evidence.",
"evidence": [
"Reviewed work/artifacts/F-001/architect.md",
"Reviewed work/artifacts/F-001/implementer.md",
"Checked project tree under project/web/index/new and project/sql/db-25052026.sql",
"Confirmed old path project/new is removed",
"Confirmed ./scripts/verify.sh is green"
],
"timestamp": "2026-05-25T05:45:00Z"
}

View File

@@ -0,0 +1,52 @@
{
"feature_id": "F-001",
"agent": "security",
"verdict": "CHANGES_REQUESTED",
"summary": "Legacy code still contains hard-coded API credentials, database credentials, and production-coupled endpoints inside versioned files. Feature cannot pass security gate until secrets are removed or externalized.",
"checks": [
"secret scan",
"input and config review",
"repo path review"
],
"findings": [
{
"severity": "high",
"title": "Hard-coded API credential in legacy PHP files",
"status": "open",
"paths": [
"project/web/index/new/describe.php",
"project/web/index/new/worker_bulk.php",
"project/web/index/new/productos_bulk_update.php"
]
},
{
"severity": "high",
"title": "Hard-coded database credentials in versioned PHP files",
"status": "open",
"paths": [
"project/web/index/new/worker_bulk.php",
"project/web/index/new/productos_modificados.php",
"project/web/index/new/productos_bulk_update.php",
"project/web/index/new/db/conn.php"
]
},
{
"severity": "medium",
"title": "Code is coupled to production URLs and external auth/success endpoints",
"status": "open",
"paths": [
"project/web/index/new/index.php",
"project/web/index/new/inc/header.php",
"project/web/index/new/productos_modificados.php",
"project/web/index/new/productos_bulk_update.php"
]
}
],
"evidence": [
"Ran secret scan on project/web/index/new excluding logs",
"Found hard-coded API and DB credentials in PHP source files",
"Found production URL coupling and external endpoint references",
"Reviewed ADR risk note that secrets remain in repo"
],
"timestamp": "2026-05-25T05:45:00Z"
}

View File

@@ -0,0 +1,28 @@
# Architect Artefact — Feature: F-002
## SDD Changes
- Added `spec/sdd/components/legacy-config-loader.md`
- Added `spec/sdd/decisions/002-use-local-config-loader-for-legacy-module.md`
- Extended product, tech, and acceptance specs for F-002
## BDD Coverage
- Added `spec/bdd/features/config/legacy-config.feature`
- Coverage target:
- no tracked secrets in PHP files
- one shared config helper
- documented local config setup
## Technical decisions
- Use module-root `bootstrap.php` as one config entry point.
- Use ignored `config/local.php` for real local values.
- Use versioned `config/local.example.php` for safe shape and defaults.
- Share one mysqli helper across web pages and worker.
## Risks found
- Example config will not make app fully runnable until maintainer fills local values.
- Legacy logs remain under web tree for now.
## Next step
- Implement bootstrap and config files.
- Replace inline secrets and URLs in tracked PHP files.
- Run verify and secret scan.

View File

@@ -0,0 +1,27 @@
# Documenter Artefact — Feature: F-002
## What changed
- Added one shared config loader at `project/web/index/new/bootstrap.php`.
- Added versioned config template at `project/web/index/new/config/local.example.php`.
- Added local setup notes at `project/web/index/new/config/README.md`.
- Ignored real local config file `project/web/index/new/config/local.php`.
- Updated legacy PHP entry points to use shared config lookups.
## Important files
- `project/web/index/new/bootstrap.php`
- `project/web/index/new/config/local.example.php`
- `project/web/index/new/config/README.md`
- `project/web/index/new/db/conn.php`
- `project/web/index/new/inc/header.php`
- `project/web/index/new/index.php`
- `project/web/index/new/describe.php`
- `project/web/index/new/productos_bulk_update.php`
- `project/web/index/new/productos_modificados.php`
- `project/web/index/new/worker_bulk.php`
## Local setup note
Copy or edit `project/web/index/new/config/local.php` with real local values before running the module.
## Follow-up
- Review the SQL dump for sensitive data and retention policy.
- Consider moving runtime logs out of the web tree.

View File

@@ -0,0 +1,35 @@
# Implementer Artefact — Feature: F-002
## Summary
Added one shared config loader for the legacy PHP module.
Moved DB, OpenAI, and route values out of tracked PHP source files.
Added versioned config template and ignored local config file path.
Updated docs and specs for local setup.
## Code changes
- added `project/web/index/new/bootstrap.php`
- added `project/web/index/new/config/local.example.php`
- added `project/web/index/new/config/README.md`
- added `project/web/index/new/README.md`
- updated `.gitignore` to ignore `project/web/index/new/config/local.php`
- updated PHP entry points to use `legacy_config()` and `legacy_new_mysqli()`
- removed inline DB and OpenAI secrets from tracked PHP files
- replaced inline production URLs in tracked PHP files with config keys
## Evidence
- `./scripts/verify.sh` -> OK
- secret scan on tracked PHP files -> no hard-coded DB or OpenAI secrets found
- route scan on tracked PHP files -> no hard-coded production URLs found
- ignore check -> `project/web/index/new/config/local.php` is ignored by git
- local config docs exist -> `project/web/index/new/config/README.md`
- config template exists -> `project/web/index/new/config/local.example.php`
## Checks run
- `./scripts/verify.sh`
- `rg -n "(sk-proj-|admin_natural|oo6478022A)" project/web/index/new --glob '!logs/*' --glob '!config/local.php' --glob '!config/local.example.php' --glob '*.php'`
- `rg -n "https://mercadodevida\.es|https://www\.mercadodevida\.es" project/web/index/new --glob '!logs/*' --glob '!config/local.php' --glob '!config/local.example.php' --glob '*.php'`
- `git check-ignore -v project/web/index/new/config/local.php`
## Notes
- Real local values must be filled in `config/local.php`.
- Existing SQL dump remains in repo and should be handled by separate data-security work.

View File

@@ -0,0 +1,14 @@
{
"feature_id": "F-002",
"agent": "leader",
"verdict": "APPROVED",
"summary": "All required non-leader gates are approved for F-002. Feature is accepted for final publish with shared config loader, tracked secret removal, and green harness verification.",
"evidence": [
"Reviewed work/artifacts/F-002/reviewer.json -> APPROVED",
"Reviewed work/artifacts/F-002/security.json -> APPROVED",
"Reviewed work/artifacts/F-002/qa.json -> APPROVED",
"Reviewed work/artifacts/F-002/documenter.md",
"Ran ./scripts/verify.sh -> OK"
],
"timestamp": "2026-05-25T06:00:00Z"
}

Some files were not shown because too many files have changed in this diff Show More