Cabuya place feed (v0.1)
Identificador del esquema: https://cabuya.org/schemas/0.1/place-feed.schema.json
Campos
| Campo | Tipo | Perfil | Ejemplo | Verificaciones |
|---|---|---|---|---|
last_updatedObligatorioMarca de tiempo de generación del feed, obligatoria (RFC 3339 en UTC). MUST NOT calcularse por petición (el antipatrón de siempre-ahora). | stringdate-time | Núcleo | "2026-08-16T04:05:00Z" | ENV001ENV002 |
ttlObligatorioSegundos que un consumidor puede cachear antes de volver a consultar. El contrato de caché. Mínimo 0. | integer | Núcleo | 300 | ENV001ENV006 |
versionObligatorio | string | Núcleo | "0.1.0" | ENV001ENV008 |
publisher_idObligatorio | string | Núcleo | "acopios-demo" | ENV001ENV009 |
licenseObligatorio | string | Núcleo | "ODbL-1.0" | ENV001ENV003ENV004 |
permitted_use | array | Extendido | […3] | ENV005 |
dataObligatorio | object | Núcleo | {…} | — |
placesObligatorio | array | Núcleo | […3] | ENV001 |
idObligatorioIdentificador local estable del publicador (entero, uuid o hexadecimal, todos aceptables, R3). Opaco; MUST NOT incrustar datos personales (R6). | string | Núcleo | "shel-0007" | REC002REC003 |
publisher_idObligatorio | string | Núcleo | "acopios-demo" | REC003 |
nameObligatorioCR-2: MUST NOT codificar estado operativo («(cerrado ahora)» va en service_status, no en el nombre). Una de varias formas — ver la fuente del esquema. | any | Núcleo | […2] | REC010REC012 |
place_kindObligatorio
| string | Núcleo | "shelter" | REC004 |
place_kind_secondary | array | Extendido | […1] | — |
place_kind_ext | string | Extendido | — | REC005 |
origin_categoryEl valor de categoría propio del publicador, literal y sin traducir. Mantiene auditable toda la tabla de equivalencias. | string | Extendido | "albergue_temporal" | — |
descriptionLos publicadores MUST retirar los datos personales antes de publicar (el texto libre es el tercer canal de fuga). Una de varias formas — ver la fuente del esquema. | any | Extendido | — | PII003PII004 |
municipality_codeObligatorioDIVIPOLA. null se permite SOLO con municipality_text presente (advertencia del validador, no error — Q3). | string | null | Núcleo | "66001" | REC006 |
municipality_text | string | Extendido | — | — |
address_text | string | Extendido | "Av. Las Américas con Calle 37, Villa Olímpica" | PII004 |
neighborhood_text | string | Extendido | — | — |
latMínimo -90. | number | Extendido | 4.815091 | REC007 |
lonMínimo -180. | number | Extendido | -75.735 | REC007 |
geo_precision
| string | Extendido | "exact" | — |
lifecycle_statusObligatorio
| string | Núcleo | "active" | REC008 |
service_status
| string | Extendido | "full" | REC009REC011 |
closed_at | stringdate-time | Extendido | — | — |
expires_at | stringdate-time | Extendido | — | — |
last_confirmed_atObligatorioLa clave MUST estar presente. null = nunca se confirmó (honesto). Omitirla = no cumple. | string | nulldate-time | Núcleo | "2026-08-16T02:30:00Z" | REC001REC014BEH002 |
confirmed_byToken de ROL o actor, acotado al publicador (team, volunteer, official_source, partner:{publisher_id}). MUST NOT ser el nombre de una persona. | string | Extendido | "volunteer" | REC015PII005 |
confirmation_method
| string | Extendido | "in_person" | — |
confirmations_24hMínimo 0. | integer | Extendido | 3 | — |
contradictions_activeLa confirmación negativa es una señal de primera clase. Mínimo 0. | integer | Extendido | 0 | — |
last_reported_absent_at | stringdate-time | Extendido | — | — |
updated_atCR-1: nunca sustituye a last_confirmed_at — editar no es confirmar. | stringdate-time | Extendido | "2026-08-16T02:31:12Z" | REC013 |
published_at | stringdate-time | Extendido | — | — |
sourceObligatorioProcedencia estructurada, nunca prosa (se ha observado que el campo libre «fuente» filtra nombres de personas). Los agregadores MUST preservar la cadena original. | object | Núcleo | {…} | REC016 |
source_idObligatorio | string | Núcleo | "acopios-demo" | — |
source_url | stringuri | Extendido | — | — |
retrieved_at | stringdate-time | Extendido | — | — |
source_kind
| string | Extendido | "first_party" | — |
source_authority
| string | Extendido | "community" | — |
attribution_required | boolean | Extendido | true | — |
public_urlObligatorioREQUIRED. El enlace saliente es la forma en que el contacto, los medios y el detalle llegan a las personas SIN viajar en el feed. | stringuri | Núcleo | "https://acopios-demo.invalid/p/shel-0007" | REC017 |
contact_availableLleva el hecho, nunca el valor. | boolean | Extendido | true | PII001PII002 |
same_asAfirmaciones {publisher_id}:{id} absolutas. Un solo salto, no transitivas, nunca autoridad (Q5). | array | Extendido | […2] | — |
merged_intoPuntero de sustitución dentro del mismo publicador (precedente: alluda ciudades.fusionada_en). | string | Extendido | — | — |
next_cursorSolo en respuestas de la API de lectura. Cursor opaco de secuencia del servidor — nunca una marca de tiempo de registro. | string | null | Extendido | — | — |
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.
Cumplevalid-minimal-core.json
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).
{
"$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"
}
]
}
}Cumplevalid-rich-extended.json
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.
{
"$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"
}
]
}
}No cumpleinvalid-1-missing-confirmation-key.json
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?).
{
"$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"
}
]
}
}No cumpleinvalid-2-contact-and-personal-data.json
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.
{
"$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"
}
]
}
}No cumpleinvalid-3-status-in-name-and-always-now.json
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.
{
"$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"
}
]
}
}