diff --git a/docs/audit/tracker-gap-reference-catalog.md b/docs/audit/tracker-gap-reference-catalog.md index de9a4d44..d48b2287 100644 --- a/docs/audit/tracker-gap-reference-catalog.md +++ b/docs/audit/tracker-gap-reference-catalog.md @@ -286,7 +286,15 @@ No se reutilizó `CoreEvaluationDeposit`: es la forma de la INGESTA y exige viol - **Criticality:** P1 · **Complexity:** M - **Proposed fix:** Añadir un robot `core-sdlc-parity` que ejecute una iniciativa multi-fase, consuma formatos vivos, produzca artefactos, sincronice/evalúe con Core y afirme decisiones tenant. - **Acceptance criteria:** - - [ ] El robot crea una iniciativa y recorre al menos discovery→design→construction con artefactos Core. **Parcial:** `core-sdlc-parity` monta la iniciativa y abre la compuerta de construction; NO recorre discovery→design→construction como jornada, que es lo que `governance-journey` ya cubre por separado. + - [x] El robot crea una iniciativa y recorre al menos discovery→design→construction con artefactos Core — repartido entre dos robots que ya existían y uno nuevo: `governance-journey` recorre las cinco fases, `core-sdlc-parity` ejercita la decisión de compuerta con un depósito adjunto, y **`phase-artifact-catalog`** (nuevo) afirma la mitad que faltaba: los artefactos **del catálogo derivado del Core**. + +**El hueco que destapó buscar esto:** el catálogo por fase es lo que CP-02 construyó y lo leen ya CUATRO superficies (el endpoint, el contraste del expediente, el payload al Core y el scorecard) — y **ningún robot lo tocaba**. La pieza más compartida del producto no tenía cobertura de extremo a extremo. + +`phase-artifact-catalog` afirma, contra el despliegue vivo: que el catálogo se sirve por fase y **declara su procedencia** (`core-sync` o `core-standin` — un consumidor tiene que poder distinguir una respuesta sincronizada de una espejada leyendo el payload); que marcar `not-applicable` **no borra** el artefacto sino que lo sirve marcado con `standardRequired` al lado; que **retirar el overlay devuelve el estándar intacto** —la aserción que sostiene «sin modificar el estándar base», porque si el overlay hubiera editado el catálogo del Core esto seguiría en falso—; y que una aplicabilidad inválida se rechaza al guardar sin dejar rastro. + +**SÍ entra en la lista de CI**, a diferencia de `core-sdlc-parity`: no necesita clave de máquina ni el Core desplegado. Añadido a `ROBOSOFT_ONLY` en `local-test.sh`. + +**Verificado local:** 18 checks propios, 0 fallos; con la lista completa de CI, **229 checks, 0 fallos**. - [ ] Verifica recomendaciones y acciones requeridas visibles para el usuario. **Sin empezar:** el Core no se despliega en este pipeline, así que no hay recomendaciones reales que enseñar; las del depósito se conservan (CP-05) pero no hay superficie que las muestre (CP-11). - [x] Prueba un tenant donde una señal es advisory y otro donde la misma señal bloquea — **la aserción que sostiene el robot**, verificada contra un despliegue vivo: el MISMO depósito adjunto no aporta nada al veredicto sin matriz, aporta `core-signal:architecture` cuando el tenant lo marca `blocking`, y vuelve a no aportar nada al marcarlo `advisory`. Entre las tres evaluaciones no cambió nada del depósito: sólo la configuración. - [x] Emite evidencia JSON enlazable — `robosoft/.evidence/robosoft-*.json`, como todos los robots. diff --git a/docs/audit/tracker-gap-tracking.md b/docs/audit/tracker-gap-tracking.md index 24072e19..5571b0db 100644 --- a/docs/audit/tracker-gap-tracking.md +++ b/docs/audit/tracker-gap-tracking.md @@ -24,7 +24,7 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor | [`CP-04`](./tracker-gap-reference-catalog.md#cp-04) | El contexto que el Tracker envía al Core es demasiado pobre para evaluar artefactos, evidencia, autoría y revisión de repositorio | El Core recibe un gate casi sin insumos tipados y sólo puede emitir un veredicto básico, no inteligencia útil sobre el paquete de fase | Ya viajan tipados `schemaVersion`, `requester` y `repositoryRevision` — este último sale del `passthrough` que el Core no interpreta; y los artefactos de la fase viajan tipados con lo exigido y lo presentado, armados por el servidor; la evidencia de gate sigue siendo otro agregado y no se manda | `Integration` | Cross | P1 | M | `DONE` | | [`CP-05`](./tracker-gap-reference-catalog.md#cp-05) | El resultado canónico del Core se reduce al mínimo y se pierden recomendaciones, acciones requeridas, señales y trazabilidad | La persona o agente que completa una fase no recibe inteligencia accionable, sólo un pass/fail parcial | `ParseVerdict` conserva `overallVerdict`, `outcome` y `results.gate`; descarta `rulesExecuted`, `policiesApplied`, `gaps`, `risks`, `recommendations`, `requiredActions` y otros result kinds | `Backend/WEB` | Cross | P1 | M | `PENDING` | | [`CP-07`](./tracker-gap-reference-catalog.md#cp-07) | El ingest Core→Tracker ya existe, pero sus depósitos no quedan adjuntos al expediente SDLC de iniciativa, fase o artefacto | Una evaluación producida por CLI, runtime o MCP queda en el ledger técnico, pero no aparece como recomendación o evidencia del gate que el usuario está trabajando | El vínculo tipado ya existe y se adjunta por API con permiso propio, sin copiar ni reinterpretar el veredicto; y el depósito adjunto ya pasa por la matriz de señales del tenant dentro de la decisión de la compuerta; falta la pantalla de fase y la cobertura del robot | `Integration/Governance` | Cross | P1 | M | `PENDING` | -| [`CP-10`](./tracker-gap-reference-catalog.md#cp-10) | No hay robot de paridad SDLC que conduzca una iniciativa y pruebe formatos, evaluación Core, recomendaciones y reglas tenant por fase | Podemos cerrar piezas individuales sin demostrar que juntas sostienen el flujo principal del producto | El robot core-sdlc-parity prueba contra un despliegue vivo que la MISMA señal del Core bloquea o no según la matriz del tenant; falta la jornada multi-fase y las recomendaciones visibles, que dependen de desplegar el Core | `Infra/Quality` | Cross | P1 | M | `PENDING` | +| [`CP-10`](./tracker-gap-reference-catalog.md#cp-10) | No hay robot de paridad SDLC que conduzca una iniciativa y pruebe formatos, evaluación Core, recomendaciones y reglas tenant por fase | Podemos cerrar piezas individuales sin demostrar que juntas sostienen el flujo principal del producto | El robot core-sdlc-parity prueba contra un despliegue vivo que la MISMA señal del Core bloquea o no según la matriz del tenant; y un robot nuevo cubre el catálogo de artefactos por fase, que no tocaba ninguno pese a leerlo cuatro superficies; solo faltan las recomendaciones visibles, que dependen de desplegar el Core | `Infra/Quality` | Cross | P1 | M | `PENDING` | | [`CP-11`](./tracker-gap-reference-catalog.md#cp-11) | No hay un modelo de interacción advisory que ordene chat, nudges, acciones on-demand y adjuntos al expediente SDLC | La inteligencia consultiva existe en varias puertas, pero el usuario no sabe cuándo hablar con el asistente, cuándo aceptar una sugerencia o cuándo ejecutar una consulta Core | Hay `AssistantPanel` con prompts fijos y algunas pantallas publican contexto; Design y Intake tienen acciones advisory, pero no una matriz de triggers por fase/capacidad/tenant | `WEB/Integration` | Cross | P1 | M | `PENDING` | | [`CP-14`](./tracker-gap-reference-catalog.md#cp-14) | Los artefactos SDLC no tienen versionado documental con comparación, restauración y línea base aprobada | Se puede actualizar un artefacto, pero no queda claro qué cambió, qué versión fue evaluada o cuál quedó aprobada | El historial por artefacto ya existe con autor, fecha, checksum, comparación y restauración, y lo aprobado no se modifica en sitio; y la aprobación de la compuerta sella qué versiones aprobó, con el vínculo del depósito nombrando la versión evaluada | `Backend/Artifacts` | Cross | P1 | M | `DONE` | | [`CP-16`](./tracker-gap-reference-catalog.md#cp-16) | Falta una bitácora SDLC por iniciativa que una artefactos, ediciones, aprobaciones, agentes, evaluaciones Core y decisiones | El seguimiento queda repartido entre pantallas y logs técnicos, sin una línea de tiempo entendible para auditoría o gestión | La bitácora existe como PROYECCIÓN de auditoría, entregas de compuerta y depósitos adjuntos, filtrable por fase y artefacto y clasificando advisory, cumplido, bloqueo y excepción; con las versiones de artefacto ya enlazadas; sólo faltan los exports, que aún no se registran en ningún sitio | `Governance/Audit` | Cross | P1 | M | `PENDING` | diff --git a/product/infra/helm/local-test.sh b/product/infra/helm/local-test.sh index f83d9ab6..ecc13afa 100755 --- a/product/infra/helm/local-test.sh +++ b/product/infra/helm/local-test.sh @@ -451,7 +451,7 @@ robosoft() { # `_pf` returns the node exit code (0 pass / 1 fail), so this MORDS in CI. _pf tracker-api-evolith-tracker-api 5100 \ env ROBOSOFT_API_BASE=http://localhost:5100 \ - ROBOSOFT_ONLY=governance-journey,provider-connections,tenant-isolation,gate-enforcement,exception-governance,audit-trail,intake,qa-quality-gate,scorecard \ + ROBOSOFT_ONLY=governance-journey,provider-connections,tenant-isolation,gate-enforcement,exception-governance,audit-trail,intake,qa-quality-gate,scorecard,phase-artifact-catalog \ node "$ROOT/robosoft/run.mjs" } diff --git a/robosoft/README.md b/robosoft/README.md index ccf56cc9..472b1664 100644 --- a/robosoft/README.md +++ b/robosoft/README.md @@ -60,7 +60,7 @@ fails the run; a failed **soft** check is a warning only. ## Robots -**14 robots.** Derived from `robosoft/robots/*.robot.mjs` — the same discovery `robosoft/run.mjs` performs, so this table cannot disagree with what a run executes. +**15 robots.** Derived from `robosoft/robots/*.robot.mjs` — the same discovery `robosoft/run.mjs` performs, so this table cannot disagree with what a run executes. | Robot | Proves | |-------|--------| @@ -73,6 +73,7 @@ fails the run; a failed **soft** check is a warning only. | `gate-enforcement` | Prove the discovery gate REFUSES to approve until evidence AND approval are present. | | `governance-journey` | Drive one tenant initiative through the full SDLC funnel, asserting every gate. | | `intake` | Drive the pre-discovery intake funnel (handshake → feasibility → promote) and assert each transition. | +| `phase-artifact-catalog` | Assert the phase-artifact catalogue is served per phase, declares its provenance, and honours the tenant overlay without touching the base standard (CP-02). | | `provider-connections` | Drive the ProviderConnection lifecycle (register→secret→acl→connect→disconnect) and assert its state machine. | | `qa-quality-gate` | Prove the QA gate stops authorizing while a blocking defect is open, and clears once it is closed. | | `runtime-approvals` | Drive the CD-23 HITL flow: machine submits, only an authorized human may approve, rejection is recorded. | diff --git a/robosoft/robots/phase-artifact-catalog.robot.mjs b/robosoft/robots/phase-artifact-catalog.robot.mjs new file mode 100644 index 00000000..abc02da3 --- /dev/null +++ b/robosoft/robots/phase-artifact-catalog.robot.mjs @@ -0,0 +1,145 @@ +// ----------------------------------------------------------------------------- +// RoboSoft agent · phase-artifact-catalog (CP-02, y el criterio 1 de CP-10) +// +// El catálogo de artefactos por fase es lo que CP-02 construyó y lo que leen ya +// CUATRO superficies: este endpoint, el contraste del expediente, el payload que +// va al Core y el scorecard. No lo tocaba NINGÚN robot — o sea que la pieza más +// compartida del producto no tenía cobertura de extremo a extremo. +// +// Lo que se afirma aquí: +// · el catálogo se sirve por fase y DECLARA su procedencia (`core-sync` cuando +// hubo sincronización, `core-standin` mientras se sirve el espejo); +// · el overlay del tenant cambia lo exigido SIN tocar el estándar base; +// · marcar `not-applicable` NO borra el artefacto: lo sirve marcado, con lo que +// decía el estándar al lado; +// · una aplicabilidad inválida se rechaza al guardar. +// +// NO necesita clave de máquina ni el Core desplegado, así que ENTRA en la lista de +// CI (a diferencia de `core-sdlc-parity`). Con el Core caído el catálogo se sirve +// como `core-standin`, y esa es justamente una de las cosas que se comprueban: +// un consumidor tiene que poder distinguir una respuesta sincronizada de una +// espejada leyendo el payload, no confiando en que hubo sincronización. +// ----------------------------------------------------------------------------- + +const RUN = Date.now().toString(36); + +export default { + name: 'phase-artifact-catalog', + description: + 'Assert the phase-artifact catalogue is served per phase, declares its provenance, and honours the tenant overlay without touching the base standard (CP-02).', + + async run(ctx) { + const { api, step, check, info } = ctx; + + const need = async (label, p) => { + const r = await p; + if (!check(label, r.ok, { detail: r.error ? r.error : `status=${r.status}` })) { + throw new Error(`precondition failed: ${label} (status=${r.status})`); + } + return r; + }; + + const perfiles = async () => + (await need('GET /phase-artifact-profiles', api.get('/phase-artifact-profiles'))).body; + + const deFase = (lista, fase) => + (Array.isArray(lista) ? lista : []).find((p) => p?.phase === fase); + + // ── 1. El catálogo existe, por fase, y dice de dónde viene ─────────────── + step('The catalogue is served per phase and declares its provenance'); + + const base = await perfiles(); + const fases = (base || []).map((p) => p.phase); + check('it covers the SDLC phases', ['discovery', 'design', 'construction'].every((f) => fases.includes(f)), { + detail: `fases=${JSON.stringify(fases)}`, + }); + + const construccion = deFase(base, 'construction'); + if (!check('construction has a profile', Boolean(construccion), { detail: `fases=${fases.length}` })) { + throw new Error('no construction profile'); + } + + // La procedencia es un HECHO del payload, no una suposición. `core-standin` + // es una respuesta legítima —el Core es otro producto y puede no estar + // desplegado—; lo que no puede es callarse cuál de las dos es. + check('the profile declares its source', ['core-sync', 'core-standin'].includes(construccion.source), { + detail: `source=${construccion.source}`, + }); + info(`catálogo servido como ${construccion.source} con ${construccion.artifacts?.length ?? 0} artefacto(s) en construction`); + + const exigidosBase = (construccion.artifacts || []).filter((a) => a.required).map((a) => a.artifactKind); + if (!check('construction requires at least one artifact', exigidosBase.length > 0, { + detail: `exigidos=${JSON.stringify(exigidosBase)}`, + })) { + throw new Error('an empty required set would make every assertion below vacuous'); + } + + const objetivo = exigidosBase[0]; + const politicaBase = { + mode: 'SIMPLE', requiresCoreVerdict: false, strategy: 'SINGLE', stages: 'parallel', + requiredEvidence: [], approvers: [], + }; + + // ── 2. El tenant lo marca no aplicable — y NO desaparece ───────────────── + step(`The tenant marks \`${objetivo}\` not-applicable — it is served marked, not deleted`); + { + await need('PUT /gate-policies/construction (overlay)', api.put('/gate-policies/construction', { + ...politicaBase, + artifactApplicability: [{ artifactKind: objetivo, applicability: 'not-applicable' }], + })); + + const conOverlay = deFase(await perfiles(), 'construction'); + const art = (conOverlay.artifacts || []).find((a) => a.artifactKind === objetivo); + + check('the artifact is STILL served', Boolean(art), { detail: `objetivo=${objetivo}` }); + check('it is no longer required', art?.required === false, { detail: `required=${art?.required}` }); + check('the tenant decision is declared', art?.tenantApplicability === 'not-applicable', { + detail: `tenantApplicability=${art?.tenantApplicability}`, + }); + // La mitad que hace auditable el overlay: quien lee tiene que poder separar + // «el Core lo exige» de «este tenant lo exige». + check('what the STANDARD said travels alongside', art?.standardRequired === true, { + detail: `standardRequired=${art?.standardRequired}`, + }); + } + + // ── 3. Retirar el overlay devuelve el estándar intacto ─────────────────── + step('Removing the overlay returns the base standard untouched'); + { + await need('PUT /gate-policies/construction (sin overlay)', api.put('/gate-policies/construction', { + ...politicaBase, artifactApplicability: [], + })); + + const limpio = deFase(await perfiles(), 'construction'); + const art = (limpio.artifacts || []).find((a) => a.artifactKind === objetivo); + + // ES LA ASERCIÓN QUE SOSTIENE «sin modificar el estándar base»: si el overlay + // hubiera editado el catálogo del Core, esto seguiría en false. + check('the artifact is required again', art?.required === true, { detail: `required=${art?.required}` }); + check('no tenant mark remains', art?.tenantApplicability == null, { + detail: `tenantApplicability=${art?.tenantApplicability}`, + }); + } + + // ── 4. Una aplicabilidad inválida se rechaza al guardar ────────────────── + step('An unknown applicability is refused on write'); + { + const r = await api.put('/gate-policies/construction', { + ...politicaBase, + artifactApplicability: [{ artifactKind: objetivo, applicability: `invento-${RUN}` }], + }); + // Guardarla e ignorarla luego daría un tenant que cree haber marcado un + // artefacto y un catálogo que se lo sigue exigiendo. + check('the write is refused', !r.ok, { detail: `status=${r.status}` }); + + const tras = deFase(await perfiles(), 'construction'); + const art = (tras.artifacts || []).find((a) => a.artifactKind === objetivo); + check('and nothing was stored', art?.tenantApplicability == null, { + detail: `tenantApplicability=${art?.tenantApplicability}`, + }); + } + + step('catalogue verified'); + info(`el overlay cambió y revirtió «${objetivo}» sin tocar el estándar base`); + }, +};