# Cabuya place feed (v0.1)

Los esquemas JSON que el validador aplica. Cada campo con su tipo, restricciones, un valor de ejemplo y los identificadores de verificación que se disparan en él.

Canonical: https://cabuya.org/es/developers/schemas/0.1/place-feed
Language: es

Identificador del esquema: https://cabuya.org/schemas/0.1/place-feed.schema.json

> Esta página es la traducción al español, y es informativa: donde las dos difieran, rige el texto en inglés. Está versionada junto a la sección que traduce, así que cambiar una sin la otra se ve en la revisión.

## Campos

| Campo | Tipo | Obligatorio | Perfil | Ejemplo | Verificaciones | Descripción |
|---|---|---|---|---|---|---|
| `last_updated` | `string` | Sí | Núcleo | `"2026-08-16T04:05:00Z"` | ENV001, ENV002 | Marca de tiempo de generación del feed, obligatoria (RFC 3339 en UTC). MUST NOT calcularse por petición (el antipatrón de siempre-ahora). |
| `ttl` | `integer` | Sí | Núcleo | `300` | ENV001, ENV006 | Segundos que un consumidor puede cachear antes de volver a consultar. El contrato de caché. |
| `version` | `string` | Sí | Núcleo | `"0.1.0"` | ENV001, ENV008 |  |
| `publisher_id` | `string` | Sí | Núcleo | `"acopios-demo"` | ENV001, ENV009 |  |
| `license` | `string` | Sí | Núcleo | `"ODbL-1.0"` | ENV001, ENV003, ENV004 |  |
| `permitted_use` | `array` | No | Extendido | `[…3]` | ENV005 |  |
| `data` | `object` | Sí | Núcleo | `{…}` | — |  |
| `data.places` | `array` | Sí | Núcleo | `[…3]` | ENV001 |  |
| `data.places[].id` | `string` | Sí | Núcleo | `"shel-0007"` | REC002, REC003 | Identificador local estable del publicador (entero, uuid o hexadecimal, todos aceptables, R3). Opaco; MUST NOT incrustar datos personales (R6). |
| `data.places[].publisher_id` | `string` | Sí | Núcleo | `"acopios-demo"` | REC003 |  |
| `data.places[].name` | `any` | Sí | Núcleo | `[…2]` | REC010, REC012 | CR-2: MUST NOT codificar estado operativo («(cerrado ahora)» va en service_status, no en el nombre). |
| `data.places[].place_kind` | `string` | Sí | Núcleo | `"shelter"` | REC004 |  |
| `data.places[].place_kind_secondary` | `array` | No | Extendido | `[…1]` | — |  |
| `data.places[].place_kind_ext` | `string` | No | Extendido | — | REC005 |  |
| `data.places[].origin_category` | `string` | No | Extendido | `"albergue_temporal"` | — | El valor de categoría propio del publicador, literal y sin traducir. Mantiene auditable toda la tabla de equivalencias. |
| `data.places[].description` | `any` | No | Extendido | — | PII003, PII004 | Los publicadores MUST retirar los datos personales antes de publicar (el texto libre es el tercer canal de fuga). |
| `data.places[].municipality_code` | `string | null` | Sí | Núcleo | `"66001"` | REC006 | DIVIPOLA. null se permite SOLO con municipality_text presente (advertencia del validador, no error — Q3). |
| `data.places[].municipality_text` | `string` | No | Extendido | — | — |  |
| `data.places[].address_text` | `string` | No | Extendido | `"Av. Las Américas con Calle 37, Villa Olímpica"` | PII004 |  |
| `data.places[].neighborhood_text` | `string` | No | Extendido | — | — |  |
| `data.places[].lat` | `number` | No | Extendido | `4.815091` | REC007 |  |
| `data.places[].lon` | `number` | No | Extendido | `-75.735` | REC007 |  |
| `data.places[].geo_precision` | `string` | No | Extendido | `"exact"` | — |  |
| `data.places[].lifecycle_status` | `string` | Sí | Núcleo | `"active"` | REC008 |  |
| `data.places[].service_status` | `string` | No | Extendido | `"full"` | REC009, REC011 |  |
| `data.places[].closed_at` | `string` | No | Extendido | — | — |  |
| `data.places[].expires_at` | `string` | No | Extendido | — | — |  |
| `data.places[].last_confirmed_at` | `string | null` | Sí | Núcleo | `"2026-08-16T02:30:00Z"` | REC001, REC014, BEH002 | La clave MUST estar presente. null = nunca se confirmó (honesto). Omitirla = no cumple. |
| `data.places[].confirmed_by` | `string` | No | Extendido | `"volunteer"` | REC015, PII005 | Token de ROL o actor, acotado al publicador (team, volunteer, official_source, partner:{publisher_id}). MUST NOT ser el nombre de una persona. |
| `data.places[].confirmation_method` | `string` | No | Extendido | `"in_person"` | — |  |
| `data.places[].confirmations_24h` | `integer` | No | Extendido | `3` | — |  |
| `data.places[].contradictions_active` | `integer` | No | Extendido | `0` | — | La confirmación negativa es una señal de primera clase. |
| `data.places[].last_reported_absent_at` | `string` | No | Extendido | — | — |  |
| `data.places[].updated_at` | `string` | No | Extendido | `"2026-08-16T02:31:12Z"` | REC013 | CR-1: nunca sustituye a last_confirmed_at — editar no es confirmar. |
| `data.places[].published_at` | `string` | No | Extendido | — | — |  |
| `data.places[].source` | `object` | Sí | Núcleo | `{…}` | REC016 | Procedencia estructurada, nunca prosa (se ha observado que el campo libre «fuente» filtra nombres de personas). Los agregadores MUST preservar la cadena original. |
| `data.places[].source.source_id` | `string` | Sí | Núcleo | `"acopios-demo"` | — |  |
| `data.places[].source.source_url` | `string` | No | Extendido | — | — |  |
| `data.places[].source.retrieved_at` | `string` | No | Extendido | — | — |  |
| `data.places[].source.source_kind` | `string` | No | Extendido | `"first_party"` | — |  |
| `data.places[].source_authority` | `string` | No | Extendido | `"community"` | — |  |
| `data.places[].attribution_required` | `boolean` | No | Extendido | `true` | — |  |
| `data.places[].public_url` | `string` | Sí | Núcleo | `"https://acopios-demo.invalid/p/shel-0007"` | REC017 | REQUIRED. El enlace saliente es la forma en que el contacto, los medios y el detalle llegan a las personas SIN viajar en el feed. |
| `data.places[].contact_available` | `boolean` | No | Extendido | `true` | PII001, PII002 | Lleva el hecho, nunca el valor. |
| `data.places[].same_as` | `array` | No | Extendido | `[…2]` | — | Afirmaciones {publisher_id}:{id} absolutas. Un solo salto, no transitivas, nunca autoridad (Q5). |
| `data.places[].merged_into` | `string` | No | Extendido | — | — | Puntero de sustitución dentro del mismo publicador (precedente: alluda ciudades.fusionada_en). |
| `next_cursor` | `string | null` | No | Extendido | — | — | Solo en respuestas de la API de lectura. Cursor opaco de secuencia del servidor — nunca una marca de tiempo de registro. |

## Ejemplos

Dos que cumplen y tres que no. Los que no cumplen declaran, en su propio archivo, qué están demostrando — y las pruebas del validador se comparan contra esas cadenas exactas.

### valid-minimal-core.json — Cumple

> **Qué demuestra este ejemplo:** VALID — minimal Core feed. The L2 floor: what an afternoon of work produces. Note last_confirmed_at: null on the second record — 'never confirmed' stated honestly (the silence test: a publisher with zero freshness data can still conform).

```json
{
  "$comment": "VALID — minimal Core feed. The L2 floor: what an afternoon of work produces. Note last_confirmed_at: null on the second record — 'never confirmed' stated honestly (the silence test: a publisher with zero freshness data can still conform).",
  "last_updated": "2026-08-16T04:00:00Z",
  "ttl": 300,
  "version": "0.1.0",
  "publisher_id": "example-app",
  "license": "CC-BY-4.0",
  "permitted_use": ["display", "aggregate", "ai_answer"],
  "data": {
    "places": [
      {
        "id": "184",
        "publisher_id": "example-app",
        "name": "Coliseo Mayor",
        "place_kind": "shelter",
        "municipality_code": "66001",
        "address_text": "Av. Las Américas con Calle 37, Villa Olímpica",
        "lifecycle_status": "active",
        "service_status": "full",
        "last_confirmed_at": "2026-08-15T21:00:00Z",
        "confirmation_method": "in_person",
        "source": { "source_id": "example-app", "source_kind": "first_party" },
        "public_url": "https://example-app.invalid/lugares/184"
      },
      {
        "id": "b3f2c9e1-7a44-4a1c-9d02-1f6e8a5c0d11",
        "publisher_id": "example-app",
        "name": "Punto de acopio Kennedy",
        "place_kind": "collection_center",
        "municipality_code": "66001",
        "address_text": "Parque principal de Kennedy",
        "lifecycle_status": "active",
        "last_confirmed_at": null,
        "source": { "source_id": "example-app", "source_kind": "user_report" },
        "public_url": "https://example-app.invalid/lugares/b3f2c9e1-7a44-4a1c-9d02-1f6e8a5c0d11"
      }
    ]
  }
}
```

### valid-rich-extended.json — Cumple

> **Qué demuestra este ejemplo:** VALID — Extended feed exercising the worked duplicate cases. Record 1 = the Coliseo Mayor cluster (Manizales test: municipality_code disambiguates; same_as claims the two peers' records, one-hop, no authority). Record 2 = the Colegio María Auxiliadora case: this publisher says 'paused' while a peer says active — the record carries its OWN status with its OWN age plus a same_as claim; discrepancies are preserved, not resolved. Record 3 shows negative confirmation and merged_into.

```json
{
  "$comment": "VALID — Extended feed exercising the worked duplicate cases. Record 1 = the Coliseo Mayor cluster (Manizales test: municipality_code disambiguates; same_as claims the two peers' records, one-hop, no authority). Record 2 = the Colegio María Auxiliadora case: this publisher says 'paused' while a peer says active — the record carries its OWN status with its OWN age plus a same_as claim; discrepancies are preserved, not resolved. Record 3 shows negative confirmation and merged_into.",
  "last_updated": "2026-08-16T04:05:00Z",
  "ttl": 300,
  "version": "0.1.0",
  "publisher_id": "acopios-demo",
  "license": "ODbL-1.0",
  "permitted_use": ["display", "aggregate", "redistribute"],
  "data": {
    "places": [
      {
        "id": "shel-0007",
        "publisher_id": "acopios-demo",
        "name": [{ "text": "Coliseo Mayor", "language": "es" }, { "text": "Main Coliseum shelter", "language": "en" }],
        "place_kind": "shelter",
        "place_kind_secondary": ["collection_center"],
        "origin_category": "albergue_temporal",
        "municipality_code": "66001",
        "address_text": "Av. Las Américas con Calle 37, Villa Olímpica",
        "lat": 4.815091,
        "lon": -75.735,
        "geo_precision": "exact",
        "lifecycle_status": "active",
        "service_status": "full",
        "last_confirmed_at": "2026-08-16T02:30:00Z",
        "confirmed_by": "volunteer",
        "confirmation_method": "in_person",
        "confirmations_24h": 3,
        "contradictions_active": 0,
        "updated_at": "2026-08-16T02:31:12Z",
        "source": { "source_id": "acopios-demo", "source_kind": "first_party" },
        "source_authority": "community",
        "attribution_required": true,
        "public_url": "https://acopios-demo.invalid/p/shel-0007",
        "contact_available": true,
        "same_as": ["pereira-ayuda:coliseo-mayor", "unidos-demo:c32653bdd8"]
      },
      {
        "id": "acop-0119",
        "publisher_id": "acopios-demo",
        "name": "IE María Auxiliadora",
        "place_kind": "collection_center",
        "origin_category": "acopio",
        "municipality_code": "66170",
        "address_text": "Cl. 43 #13-74, Dosquebradas",
        "lifecycle_status": "active",
        "service_status": "paused",
        "last_confirmed_at": "2026-08-15T18:00:00Z",
        "confirmation_method": "phone",
        "source": { "source_id": "acopios-demo", "source_kind": "first_party" },
        "public_url": "https://acopios-demo.invalid/p/acop-0119",
        "same_as": ["unidos-demo:76f30c68a4"]
      },
      {
        "id": "acop-0042",
        "publisher_id": "acopios-demo",
        "name": "Acopio UTP — Parque del Estudiante",
        "place_kind": "collection_center",
        "municipality_code": "66001",
        "address_text": "Carrera 27 #10-02, Álamos (frente al edificio Favi)",
        "lat": 4.79105,
        "lon": -75.690601,
        "geo_precision": "approximate",
        "lifecycle_status": "active",
        "last_confirmed_at": "2026-08-14T09:00:00Z",
        "contradictions_active": 2,
        "last_reported_absent_at": "2026-08-15T20:15:00Z",
        "merged_into": "acop-0040",
        "source": { "source_id": "partner-feed-demo", "source_url": "https://partner.invalid/feed.json", "retrieved_at": "2026-08-16T03:00:00Z", "source_kind": "partner_feed" },
        "public_url": "https://acopios-demo.invalid/p/acop-0042"
      }
    ]
  }
}
```

### invalid-1-missing-confirmation-key.json — No cumple

> **Qué demuestra este ejemplo:** INVALID — WHY: the first record OMITS the last_confirmed_at key entirely. Omission is non-conforming; the honest form is last_confirmed_at: null ('never confirmed'). The distinction is the trust core's whole point: a consumer must always be able to render an age or 'sin confirmar' — silence is not an answer. VALIDATOR ERROR: data.places[0]: required property 'last_confirmed_at' is missing (did you mean to publish last_confirmed_at: null?).

```json
{
  "$comment": "INVALID — WHY: the first record OMITS the last_confirmed_at key entirely. Omission is non-conforming; the honest form is last_confirmed_at: null ('never confirmed'). The distinction is the trust core's whole point: a consumer must always be able to render an age or 'sin confirmar' — silence is not an answer. VALIDATOR ERROR: data.places[0]: required property 'last_confirmed_at' is missing (did you mean to publish last_confirmed_at: null?).",
  "last_updated": "2026-08-16T04:00:00Z",
  "ttl": 300,
  "version": "0.1.0",
  "publisher_id": "example-app",
  "license": "CC-BY-4.0",
  "data": {
    "places": [
      {
        "id": "300",
        "publisher_id": "example-app",
        "name": "Punto de acopio El Remanso",
        "place_kind": "collection_center",
        "municipality_code": "66001",
        "address_text": "Av. principal El Remanso, junto al centro de salud",
        "lifecycle_status": "active",
        "source": { "source_id": "example-app" },
        "public_url": "https://example-app.invalid/lugares/300"
      }
    ]
  }
}
```

### invalid-2-contact-and-personal-data.json — No cumple

> **Qué demuestra este ejemplo:** INVALID — WHY (three violations of §7): (1) a phone number travels in the feed ('x_example_phone' — namespaced extensions do NOT exempt contact data; contact is public_url + link-out, fetch-on-demand from origin); (2) confirmed_by carries a person's NAME instead of a role token; (3) description embeds a personal name and phone (free text is the third leak channel — publishers MUST strip before publishing). Schema alone passes some of these; the validator's deny-pattern pass catches them all. VALIDATOR ERRORS: data.places[0].x_example_phone: contact values MUST NOT travel in feeds (use contact_available + public_url) · data.places[0].confirmed_by: must be a role token (team|volunteer|official_source|partner:{id}), never a personal name · data.places[0].description: possible personal data detected (name+phone pattern) — strip before publishing.

```json
{
  "$comment": "INVALID — WHY (three violations of §7): (1) a phone number travels in the feed ('x_example_phone' — namespaced extensions do NOT exempt contact data; contact is public_url + link-out, fetch-on-demand from origin); (2) confirmed_by carries a person's NAME instead of a role token; (3) description embeds a personal name and phone (free text is the third leak channel — publishers MUST strip before publishing). Schema alone passes some of these; the validator's deny-pattern pass catches them all. VALIDATOR ERRORS: data.places[0].x_example_phone: contact values MUST NOT travel in feeds (use contact_available + public_url) · data.places[0].confirmed_by: must be a role token (team|volunteer|official_source|partner:{id}), never a personal name · data.places[0].description: possible personal data detected (name+phone pattern) — strip before publishing.",
  "last_updated": "2026-08-16T04:00:00Z",
  "ttl": 300,
  "version": "0.1.0",
  "publisher_id": "example-app",
  "license": "CC-BY-4.0",
  "data": {
    "places": [
      {
        "id": "77",
        "publisher_id": "example-app",
        "name": "Acopio Consota",
        "place_kind": "collection_center",
        "municipality_code": "66001",
        "address_text": "Mz 7 y 8, Villa Consota, Cuba",
        "lifecycle_status": "active",
        "last_confirmed_at": "2026-08-15T10:00:00Z",
        "confirmed_by": "María Ejemplo Pérez",
        "description": "Recibe alimentos. Preguntar por María Ejemplo Pérez, cel 3000000000.",
        "x_example_phone": "+57 300 000 0000",
        "source": { "source_id": "example-app" },
        "public_url": "https://example-app.invalid/lugares/77"
      }
    ]
  }
}
```

### invalid-3-status-in-name-and-always-now.json — No cumple

> **Qué demuestra este ejemplo:** INVALID — WHY (two production-observed anti-patterns): (1) CR-2 violation: the name encodes operational state ('(cerrado ahora)') — observed verbatim in the wild; state belongs in lifecycle_status/service_status so it can change without breaking name-based reconciliation, and so two publishers' opposite statuses are COMPARABLE fields, not prose. (2) The always-now anti-pattern: this feed's last_updated is regenerated per request (validator detects it by probing twice and seeing the value advance with the clock while content is unchanged) — worse than no signal, consumers can never detect change. Also note the record contradicts itself: name says closed, lifecycle_status says active. VALIDATOR ERRORS: data.places[0].name: operational state token detected in name ('cerrado') — move to service_status/lifecycle_status (CR-2) · envelope.last_updated: value advanced with the probe clock on identical content — generate at build/publish time, not per request.

```json
{
  "$comment": "INVALID — WHY (two production-observed anti-patterns): (1) CR-2 violation: the name encodes operational state ('(cerrado ahora)') — observed verbatim in the wild; state belongs in lifecycle_status/service_status so it can change without breaking name-based reconciliation, and so two publishers' opposite statuses are COMPARABLE fields, not prose. (2) The always-now anti-pattern: this feed's last_updated is regenerated per request (validator detects it by probing twice and seeing the value advance with the clock while content is unchanged) — worse than no signal, consumers can never detect change. Also note the record contradicts itself: name says closed, lifecycle_status says active. VALIDATOR ERRORS: data.places[0].name: operational state token detected in name ('cerrado') — move to service_status/lifecycle_status (CR-2) · envelope.last_updated: value advanced with the probe clock on identical content — generate at build/publish time, not per request.",
  "last_updated": "2026-08-16T04:59:59Z",
  "ttl": 300,
  "version": "0.1.0",
  "publisher_id": "example-app",
  "license": "CC-BY-4.0",
  "data": {
    "places": [
      {
        "id": "912",
        "publisher_id": "example-app",
        "name": "Centro de acopio IE María Auxiliadora (cerrado ahora)",
        "place_kind": "collection_center",
        "municipality_code": "66170",
        "address_text": "Cl. 43 #13-74, Dosquebradas",
        "lifecycle_status": "active",
        "last_confirmed_at": "2026-08-15T18:00:00Z",
        "source": { "source_id": "example-app" },
        "public_url": "https://example-app.invalid/lugares/912"
      }
    ]
  }
}
```

## Navegación del Sitio

- [Inicio](https://cabuya.org/es)
- [Especificación](https://cabuya.org/es/developers/spec)
- [Esquemas](https://cabuya.org/es/developers/schemas)
- [RFCs](https://cabuya.org/es/rfcs)
- [Cambios](https://cabuya.org/es/changelog)
- [Desarrolladores](https://cabuya.org/es/developers)
- [Registro](https://cabuya.org/es/registry)
- [Gobernanza](https://cabuya.org/es/governance)
- [Participar](https://cabuya.org/es/join)
- [GitHub](https://github.com/Cabuya)
- [Repositorio de la skill](https://github.com/Cabuya/cabuya-skill)
- [Registro fundacional](https://github.com/Cabuya/cabuya.org/tree/main/docs/context)
