From e7a259aadf86e3898e305b88c3df053f71494407 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Sat, 22 Aug 2026 21:19:45 -0500 Subject: [PATCH] feat(okf): el corpus firma su procedencia como autorado por humano MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un consumidor OKF v0.2 que leia nuestro bundle no encontraba `generated`, asi que caia al `timestamp` legado (§13.1) — una fecha sin ninguna procedencia. Nuestro corpus se autora a mano en canonical/*.yaml, pero el bundle no lo decia, de modo que no habia forma de distinguirlo de uno escupido por una maquina. Los 10 conceptos proyectados pasan a firmar `generated: { by: human:@winston }`. Dos decisiones, ambas con razon: `human:` y no `process:`. §7 avisa de que los consumidores que clasifican confianza (§5.3) se apoyan en ese prefijo, y obliga a los productores a usarlo para contenido autorado o confirmado por humanos. El proyector TRANSCRIBE el corpus, no lo escribe; firmar con el script degradaria el corpus a "generado por maquina" ante cualquier consumidor v0.2, que es justo lo contrario de lo que es. `at` se OMITE, y conviene que siga omitido. §5.2 lo define como "the content's last meaningful change" y solo marca `by` como REQUIRED, asi que omitirlo es conforme. Las dos fuentes posibles no sirven: - `asOf` es la fecha de PROYECCION. Emitirla haria que cada re-proyeccion afirme que todo el corpus acaba de cambiar — lo contrario del uso que §5.2 le da. - La fecha del commit del fichero canonico seria la correcta, pero es inalcanzable: el bundle se commitea EN EL MISMO commit que el cambio canonico y lo gatea `--verify`, asi que `at` necesitaria la fecha de un commit que aun no existe al generar. Cualquier edicion canonica dejaria el gate en rojo. Un dato ausente es conforme; uno inventado engaña al consumidor. El bundle sigue siendo v0.1-conforme: v0.1 tolera claves extra, el §11 de v0.2 exige las mismas tres reglas (frontmatter parseable, `type` no vacio, reservados) y ya las cumpliamos. Por eso NO se toca la version declarada ni el ADR-0105 — eso seria otra decision, no un efecto colateral de esta. El validador gana dientes: si `generated` esta presente, `generated.by` es REQUERIDO (§5.2) y debe seguir la convencion de actor (§7). Sin eso el campo podria emitirse a medias y un consumidor lo clasificaria mal en silencio, que es peor que no emitirlo. Verificacion: el diff del bundle son EXACTAMENTE 10 adiciones de `generated`, nada mas. --check 0 violaciones, --verify al dia (corrido como en CI, sin --as-of; la linea 527 reproduce con el as-of del log.md publicado), guard CI 38 exit 0, guard pre-commit exit 0. Tests del proyector 14 -> 17, incluido uno que fija la AUSENCIA de `at` para que no se cuele mas adelante. Co-Authored-By: Claude Opus 5 Signed-off-by: aarroyo --- .harness/scripts/knowledge-okf-project.mjs | 63 ++++++++++++++++++- .../scripts/knowledge-okf-project.test.mjs | 36 +++++++++++ .../okf/concepts/glossary-knowledge.md | 2 + .../okf/packs/knowledge-and-corpus.md | 2 + reference/knowledge/okf/product.md | 2 + reference/knowledge/okf/refs/adr-0069.md | 2 + reference/knowledge/okf/refs/adr-0074.md | 2 + reference/knowledge/okf/refs/adr-0080.md | 2 + reference/knowledge/okf/refs/adr-0090.md | 2 + reference/knowledge/okf/refs/adr-0101.md | 2 + .../okf/refs/knowledge-intake-schema.md | 2 + .../okf/refs/knowledge-projection-schema.md | 2 + 12 files changed, 118 insertions(+), 1 deletion(-) diff --git a/.harness/scripts/knowledge-okf-project.mjs b/.harness/scripts/knowledge-okf-project.mjs index cb7cd068e..b83c30abf 100644 --- a/.harness/scripts/knowledge-okf-project.mjs +++ b/.harness/scripts/knowledge-okf-project.mjs @@ -26,6 +26,13 @@ * - Reservados: index.md (listado, sin frontmatter), log.md (historial, fechas ISO 8601). * - Todo otro .md es un "concepto": frontmatter YAML parseable con `type` NO vacío (único obligatorio). * - Recomendados: title, description, resource, tags, timestamp. Claves extra permitidas. + * + * Extensión hacia OKF v0.2 (revisada en el ADR-0105, ver okf-spec.lock.json): + * v0.2 supersede `timestamp` por `generated: { by, at }` (§5.2). Emitimos AMBOS: el + * bundle sigue siendo v0.1-conforme (v0.1 tolera claves extra) y un consumidor v0.2 + * obtiene la procedencia real en vez de caer al `timestamp` legado, que no dice quién + * autoró. `generated.by` usa el prefijo `human:` a propósito — ver GENERATED_BY; + * `generated.at` se omite a propósito y está razonado junto a esa constante. * - Cross-links: absolutos desde la raíz del bundle (empiezan con '/') por estabilidad. */ import fs from 'node:fs'; @@ -45,6 +52,37 @@ const DEFAULT_OUT = path.join(KDIR, 'okf'); // Tipo OKF por bucket autoral (spec: "short string identifying the kind of concept"). const AUTHORED_TYPE = { domain: 'Domain Model', glossary: 'Glossary', prompts: 'Prompt' }; +/** + * Actor OKF (§7) que firma `generated.by` en todo concepto proyectado. + * + * Por qué `human:` y no `process:`: el corpus se AUTORA a mano en canonical/*.yaml y + * este proyector solo lo transcribe — no lo escribe. §7 avisa de que los consumidores + * que clasifican confianza (§5.3) se apoyan en el prefijo `human:`, y obliga a los + * productores a usarlo para contenido autorado o confirmado por humanos. Firmar con el + * script degradaría nuestro corpus a "generado por máquina" ante cualquier consumidor + * v0.2, que es justo lo contrario de lo que es. + */ +const GENERATED_BY = 'human:@winston'; + +/** + * `at` se OMITE a propósito, y conviene que siga omitido. + * + * §5.2 define `generated.at` como "the content's last meaningful change", y solo marca + * `by` como REQUIRED dentro de `generated` — así que omitirlo es conforme. Las dos + * fuentes posibles no sirven: + * + * - `asOf` es la fecha de PROYECCIÓN. Emitirla haría que cada re-proyección afirme + * que todo el corpus acaba de cambiar, que es exactamente lo contrario del uso que + * §5.2 le da al campo ("tell a recent edit from a stale fact"). + * - La fecha del commit del fichero canónico sería la correcta, pero es inalcanzable: + * el bundle se commitea EN EL MISMO commit que el cambio canónico y lo gatea + * `--verify`, de modo que `at` necesitaría la fecha de un commit que todavía no + * existe al generar. Cualquier edición canónica dejaría el gate en rojo. + * + * Un dato ausente es conforme; uno inventado engaña al consumidor. Si algún día el + * corpus canónico lleva su propia fecha de último cambio autoral, esa es la fuente. + */ + /** * Sello de procedencia estampado en TODO documento que escribe este proyector. * @@ -135,6 +173,19 @@ export function okfConformance(files) { if (!data) violations.push({ path: f.path, error: 'sin bloque de frontmatter' }); else if (!data.type || String(data.type).trim() === '') violations.push({ path: f.path, error: 'campo `type` ausente o vacío' }); + // `generated` es opcional, pero si está, `generated.by` es REQUERIDO (§5.2) y debe + // ser un actor (§7). Sin esta comprobación el campo podría emitirse a medias y un + // consumidor v0.2 lo clasificaría mal en silencio, que es peor que no emitirlo. + else if (data.generated !== undefined) { + const by = data.generated?.by; + if (!by || String(by).trim() === '') + violations.push({ path: f.path, error: '`generated` presente sin `generated.by` (OKF §5.2)' }); + else if (!/^(human:|process:)|\//.test(String(by))) + violations.push({ + path: f.path, + error: `\`generated.by\` no sigue la convención de actor (OKF §7): ${by}`, + }); + } } return violations; } @@ -206,6 +257,7 @@ export function buildBundle({ index, loadYaml, readText, asOf }) { resource: resourceOf(index.spec.product), tags: ['product', p.role].filter(Boolean), timestamp: asOf, + generated: { by: GENERATED_BY }, owner: product.metadata?.owner, reviewBy: product.metadata?.reviewBy, }, @@ -245,6 +297,7 @@ export function buildBundle({ index, loadYaml, readText, asOf }) { resource: resourceOf(srcRel), tags: [bucket, ps.boundedContext].filter(Boolean), timestamp: asOf, + generated: { by: GENERATED_BY }, owner: src.data?.owner, reviewBy: src.data?.reviewBy, partOf: `/packs/${packSlug}.md`, @@ -314,6 +367,7 @@ export function buildBundle({ index, loadYaml, readText, asOf }) { resource: resourceOf(entry.manifest), tags: [entry.layer, ps.boundedContext, pack.metadata?.status].filter(Boolean), timestamp: asOf, + generated: { by: GENERATED_BY }, owner: pack.metadata?.owner, reviewBy: pack.metadata?.reviewBy, version: pack.metadata?.version, @@ -344,7 +398,14 @@ export function buildBundle({ index, loadYaml, readText, asOf }) { return { path: `refs/${rSlug}.md`, content: renderConcept( - { type: n.type, title: n.title, resource: n.resource, tags: ['reference'], timestamp: asOf }, + { + type: n.type, + title: n.title, + resource: n.resource, + tags: ['reference'], + timestamp: asOf, + generated: { by: GENERATED_BY }, + }, body, ), }; diff --git a/.harness/scripts/knowledge-okf-project.test.mjs b/.harness/scripts/knowledge-okf-project.test.mjs index a178aad46..d45b20fab 100644 --- a/.harness/scripts/knowledge-okf-project.test.mjs +++ b/.harness/scripts/knowledge-okf-project.test.mjs @@ -112,6 +112,42 @@ test('todo concepto no reservado tiene `type` no vacío', () => { } }); +test('todo concepto firma `generated.by` con el actor humano (OKF §5.2/§7)', () => { + for (const f of build()) { + const base = f.path.split('/').pop(); + if (base === 'index.md' || base === 'log.md') continue; + const { data } = parseFrontmatter(f.content); + assert.equal(data.generated?.by, 'human:@winston', `${f.path} sin generated.by`); + // El prefijo `human:` es el que hace que un consumidor v0.2 clasifique el corpus + // como autorado por humano (§5.3); perderlo lo degrada a "generado por máquina". + assert.ok(String(data.generated.by).startsWith('human:'), `${f.path} no usa prefijo human:`); + } +}); + +test('`generated.at` se omite: no hay fuente veraz para "last meaningful change"', () => { + for (const f of build()) { + const base = f.path.split('/').pop(); + if (base === 'index.md' || base === 'log.md') continue; + const { data } = parseFrontmatter(f.content); + // Emitir `asOf` aqui afirmaria que el contenido cambio en cada re-proyeccion, que es + // lo contrario del uso que §5.2 le da al campo. Ausente es conforme; inventado, no. + assert.equal(data.generated.at, undefined, `${f.path} emite un generated.at inventado`); + } +}); + +test('okfConformance rechaza `generated` sin `by` y con actor no convencional', () => { + const withoutBy = [{ path: 'x.md', content: '---\ntype: Concept\ngenerated:\n at: 2026-01-01T00:00:00Z\n---\n\n# X\n' }]; + assert.equal(okfConformance(withoutBy).length, 1); + assert.match(okfConformance(withoutBy)[0].error, /generated\.by/); + + const badActor = [{ path: 'y.md', content: '---\ntype: Concept\ngenerated:\n by: winston\n---\n\n# Y\n' }]; + assert.equal(okfConformance(badActor).length, 1); + assert.match(okfConformance(badActor)[0].error, /convención de actor/); + + const ok = [{ path: 'z.md', content: '---\ntype: Concept\ngenerated:\n by: human:@winston\n---\n\n# Z\n' }]; + assert.deepEqual(okfConformance(ok), []); +}); + test('el concepto rehidrata el cuerpo desde la fuente y preserva provenance', () => { const concept = build().find((f) => f.path === 'concepts/glossary-knowledge.md'); const { data, body } = parseFrontmatter(concept.content); diff --git a/reference/knowledge/okf/concepts/glossary-knowledge.md b/reference/knowledge/okf/concepts/glossary-knowledge.md index 8b7e9da8a..acbbba730 100644 --- a/reference/knowledge/okf/concepts/glossary-knowledge.md +++ b/reference/knowledge/okf/concepts/glossary-knowledge.md @@ -6,6 +6,8 @@ tags: - glossary - ctx.knowledge timestamp: '2026-07-28' +generated: + by: human:@winston owner: '@winston' reviewBy: '2026-10-06' partOf: /packs/knowledge-and-corpus.md diff --git a/reference/knowledge/okf/packs/knowledge-and-corpus.md b/reference/knowledge/okf/packs/knowledge-and-corpus.md index 18c61731b..97603d038 100644 --- a/reference/knowledge/okf/packs/knowledge-and-corpus.md +++ b/reference/knowledge/okf/packs/knowledge-and-corpus.md @@ -8,6 +8,8 @@ tags: - ctx.knowledge - draft timestamp: '2026-07-28' +generated: + by: human:@winston owner: '@winston' reviewBy: '2026-10-06' version: 0.2.0 diff --git a/reference/knowledge/okf/product.md b/reference/knowledge/okf/product.md index 918344bc6..af7da0e88 100644 --- a/reference/knowledge/okf/product.md +++ b/reference/knowledge/okf/product.md @@ -7,6 +7,8 @@ tags: - product - root-authority timestamp: '2026-07-28' +generated: + by: human:@winston owner: '@winston' reviewBy: '2027-01-06' --- diff --git a/reference/knowledge/okf/refs/adr-0069.md b/reference/knowledge/okf/refs/adr-0069.md index a60ae3538..902397d3e 100644 --- a/reference/knowledge/okf/refs/adr-0069.md +++ b/reference/knowledge/okf/refs/adr-0069.md @@ -5,6 +5,8 @@ resource: evolith://adr/ADR-0069 tags: - reference timestamp: '2026-07-28' +generated: + by: human:@winston ---