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
10 changes: 9 additions & 1 deletion docs/audit/tracker-gap-reference-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/audit/tracker-gap-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
2 changes: 1 addition & 1 deletion product/infra/helm/local-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down
3 changes: 2 additions & 1 deletion robosoft/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ fails the run; a failed **soft** check is a warning only.
## Robots

<!-- BEGIN GENERATED: robot-roster — derivado por .harness/scripts/doc-inventory.mjs; NO editar a mano -->
**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 |
|-------|--------|
Expand All @@ -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. |
Expand Down
145 changes: 145 additions & 0 deletions robosoft/robots/phase-artifact-catalog.robot.mjs
Original file line number Diff line number Diff line change
@@ -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`);
},
};
Loading