Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion .harness/scripts/knowledge-okf-project.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
*
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
),
};
Expand Down
36 changes: 36 additions & 0 deletions .harness/scripts/knowledge-okf-project.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions reference/knowledge/okf/concepts/glossary-knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions reference/knowledge/okf/packs/knowledge-and-corpus.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions reference/knowledge/okf/product.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ tags:
- product
- root-authority
timestamp: '2026-07-28'
generated:
by: human:@winston
owner: '@winston'
reviewBy: '2027-01-06'
---
Expand Down
2 changes: 2 additions & 0 deletions reference/knowledge/okf/refs/adr-0069.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ resource: evolith://adr/ADR-0069
tags:
- reference
timestamp: '2026-07-28'
generated:
by: human:@winston
---

<!-- GENERATED by .harness/scripts/knowledge-okf-project.mjs — do not edit by hand.
Expand Down
2 changes: 2 additions & 0 deletions reference/knowledge/okf/refs/adr-0074.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ resource: evolith://adr/ADR-0074
tags:
- reference
timestamp: '2026-07-28'
generated:
by: human:@winston
---

<!-- GENERATED by .harness/scripts/knowledge-okf-project.mjs — do not edit by hand.
Expand Down
2 changes: 2 additions & 0 deletions reference/knowledge/okf/refs/adr-0080.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ resource: evolith://adr/ADR-0080
tags:
- reference
timestamp: '2026-07-28'
generated:
by: human:@winston
---

<!-- GENERATED by .harness/scripts/knowledge-okf-project.mjs — do not edit by hand.
Expand Down
2 changes: 2 additions & 0 deletions reference/knowledge/okf/refs/adr-0090.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ resource: evolith://adr/ADR-0090
tags:
- reference
timestamp: '2026-07-28'
generated:
by: human:@winston
---

<!-- GENERATED by .harness/scripts/knowledge-okf-project.mjs — do not edit by hand.
Expand Down
2 changes: 2 additions & 0 deletions reference/knowledge/okf/refs/adr-0101.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ resource: evolith://adr/ADR-0101
tags:
- reference
timestamp: '2026-07-28'
generated:
by: human:@winston
---

<!-- GENERATED by .harness/scripts/knowledge-okf-project.mjs — do not edit by hand.
Expand Down
2 changes: 2 additions & 0 deletions reference/knowledge/okf/refs/knowledge-intake-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ resource: src/rulesets/schema/knowledge-intake.schema.json
tags:
- reference
timestamp: '2026-07-28'
generated:
by: human:@winston
---

<!-- GENERATED by .harness/scripts/knowledge-okf-project.mjs — do not edit by hand.
Expand Down
2 changes: 2 additions & 0 deletions reference/knowledge/okf/refs/knowledge-projection-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ resource: src/rulesets/schema/knowledge-projection.schema.json
tags:
- reference
timestamp: '2026-07-28'
generated:
by: human:@winston
---

<!-- GENERATED by .harness/scripts/knowledge-okf-project.mjs — do not edit by hand.
Expand Down
Loading