diff --git a/reference/core/control-center/gaps/gap-reference-catalog.es.md b/reference/core/control-center/gaps/gap-reference-catalog.es.md index 04322330f..29c2c2859 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -346,10 +346,10 @@ Este catálogo explica cada gap: problema, propósito, evidencia, criterios de c - **Criticality:** P1 · **Complexity:** L - **Proposed fix:** Contrato ACL con linaje al origen + salvaguardas de transición (la evidencia de Jira alimenta pero no autoriza gates) + una referencia de integración documentada. - **Acceptance criteria:** - - [ ] Elementos de Jira se mapean a artefactos Evolith preservando origen/identidad/timestamps/linaje. - - [ ] Completar un workflow de Jira no autoriza por sí solo una transición de fase. + - [x] Elementos de Jira se mapean a artefactos Evolith preservando origen/identidad/timestamps/linaje. _(`parseJiraIssue`→`CanonicalWorkItem` con `WorkItemProvenance`; rechaza sin id)_ + - [x] Completar un workflow de Jira no autoriza por sí solo una transición de fase. _(`authorizesPhaseTransition:false` por contrato + `externalWorkAuthorizesTransition`⇒false)_ - **Dependencies:** none. -- **Status:** `PENDING` +- **Status:** `IN-PROGRESS` #### GT-530 diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index 1d95cb9a1..7ae560f6d 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -346,10 +346,10 @@ This catalog explains each gap: problem, purpose, evidence, closure criteria, an - **Criticality:** P1 · **Complexity:** L - **Proposed fix:** ACL contract with lineage to origin + transition safeguards (Jira evidence feeds but does not authorize gates) + a documented integration reference. - **Acceptance criteria:** - - [ ] Jira items map to Evolith artifacts preserving origin/identity/timestamps/lineage. - - [ ] Completing a Jira workflow does not by itself authorize a phase transition. + - [x] Jira items map to Evolith artifacts preserving origin/identity/timestamps/lineage. _(`parseJiraIssue`→`CanonicalWorkItem` with `WorkItemProvenance`; rejects a missing id)_ + - [x] Completing a Jira workflow does not by itself authorize a phase transition. _(`authorizesPhaseTransition:false` by contract + `externalWorkAuthorizesTransition`⇒false)_ - **Dependencies:** none. -- **Status:** `PENDING` +- **Status:** `IN-PROGRESS` #### GT-530 diff --git a/reference/core/control-center/gaps/gap-tracking.es.md b/reference/core/control-center/gaps/gap-tracking.es.md index bfa96ea54..ac89b358e 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -33,7 +33,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-526`](./gap-reference-catalog.es.md#gt-526) | **[Superficie de control · eje 2] Enforcement `edit-time` — hook cross-agente.** La 3ª de las tres superficies READ→CONTROL (§14.1): pre-generación (MCP, parcial GT-520) y PR/CI (GT-518) existen, pero falta el hook que bloquea el cambio infractor al vuelo mientras el agente escribe. Sin incumbente en el mercado. Fix: hook cross-agente (Claude Code/Cursor/Copilot) que consulta el contrato de arquitectura y rechaza el edit no conforme. Análisis §14. **EN-PROGRESO (`edit-gate.ts`):** aterrizó el núcleo verificable — `evaluateEdit(edit, rules)` decide **allow/block al vuelo** con un chequeo rápido de un solo archivo (sin toolchain; el PR/CI de GT-518 sigue siendo el gate autoritativo): `extractImports` (TS/JS `import`/`export from`/`require` + C# `using`) + reglas de frontera `EditBoundaryRule` (`appliesTo`/`forbiddenImports`), emite `Violation` canónicas (tool `edit-gate`, reusables por evidencia/compliance), `allow=false` solo ante severidad `error`. Agent-neutral (función pura). Verificado: core-domain 917/917 (+7), tsc limpio. **Pendiente (integración):** el adapter por-agente (hook PreToolUse de Claude Code / extensión Cursor) que alimenta el edit y aplica la decisión. | `Evolith CLI` | Cross | P1 | M | `EN-PROGRESO` | | [`GT-527`](./gap-reference-catalog.es.md#gt-527) | **[Wedge · eje 2] Conectores de ingesta de ownership sin lock-in: Port / Cortex / OpsLevel + Backstage.** Ingerir blueprints de IDP y `catalog-info.yaml` como fuente de ownership/servicios para enriquecer violaciones y ADRs sin depender del proveedor; Evolith bloquea donde ellos solo miden (§13.2). Fix: conectores read-only vía ACL que normalizan a la forma canónica de ownership. **EN-PROGRESO (`domain/ownership.ts`):** aterrizó el núcleo puro de normalización — `OwnershipEntry` canónico, `parseBackstageCatalog` (`catalog-info.yaml` kind Component → owner + pathPrefix vía anotación `evolith.io/path`/`source-location` relativo; descarta no-Components/incompletos), `parseBlueprintOwnership` (genérico Port/Cortex/OpsLevel: `component`\|`identifier` + `owner`\|`team`), `resolveOwner` (file→owner por longest-prefix) y `enrichViolationsWithOwner` (puebla `owner` sin sobrescribir, fuera del fingerprint). **Completa la cadena violación→owner→compliance** (compone con GT-525). Verificado: core-domain 925/925 (+8), tsc limpio. **Pendiente (conector/infra):** leer el `catalog-info.yaml` del repo (vía el port de config-parser) y el fetch read-only de las APIs Port/Cortex. | `Evolith Core` | Cross | P2 | L | `EN-PROGRESO` | | [`GT-528`](./gap-reference-catalog.es.md#gt-528) | **[Wedge · eje 2] Ingesta de DSL Structurizr / C4 → ADR ejecutable.** Convertir modelos Structurizr/C4 (intención de arquitectura, hoy prosa/diagrama) en reglas `enforce:` verificables contra el código real (§13.2). Fix: parser del DSL + mapeo a `NormalizedRule.enforce`; complementa GT-516. **EN-PROGRESO (`c4-compiler.ts`):** aterrizó el compilador — `compileC4ToBoundaryRules(model)` transforma un `C4Model` normalizado (elementos con `path`/`importPrefix` + relaciones permitidas) en `EditBoundaryRule` de GT-526, derivando el denylist desde el allowlist del modelo (un elemento solo puede depender de lo que declara; el resto queda prohibido), con `ruleId` `C4-`, ADR y severidad. **El diagrama C4 se vuelve ejecutable** y se enchufa al gate edit-time (GT-526) y al PR/CI. Verificado end-to-end: un edit de `src/domain` que importa `src/infrastructure` → bloqueado; core-domain 933/933 (+5), tsc limpio. **Pendiente (ingesta):** parsear el `.dsl` crudo de Structurizr (o su export JSON) al `C4Model` normalizado. | `Evolith Core` | Cross | P2 | M | `EN-PROGRESO` | -| [`GT-529`](./gap-reference-catalog.es.md#gt-529) | **[Surround · eje 1] Contrato ACL + referencia de integración Jira Enterprise.** Mapear ideas/epics/stories/aprobaciones/releases de Jira a artefactos Evolith preservando origen/identidad/timestamps/linaje, con salvaguardas de transición (completar un workflow de Jira no autoriza una transición de fase). §8.3 / §12. Fix: ACL de sistemas de trabajo externos + guía de integración. | `Evolith Core` | Cross | P1 | L | `PENDIENTE` | +| [`GT-529`](./gap-reference-catalog.es.md#gt-529) | **[Surround · eje 1] Contrato ACL + referencia de integración Jira Enterprise.** Mapear ideas/epics/stories/aprobaciones/releases de Jira a artefactos Evolith preservando origen/identidad/timestamps/linaje, con salvaguardas de transición (completar un workflow de Jira no autoriza una transición de fase). §8.3 / §12. Fix: ACL de sistemas de trabajo externos + guía de integración. **EN-PROGRESO (`domain/external-work-acl.ts`):** aterrizó el ACL puro — `CanonicalWorkItem` con `WorkItemProvenance` (source/externalId/externalKey/url/created/updated), `parseJiraIssue`/`parseJiraIssues` (mapea issues de Jira preservando procedencia; rechaza sin `id` en vez de fabricar identidad), `mapJiraIssueType` (Epic/Story/Task/Version→kind canónico). **Salvaguarda de transición (§9-6):** `authorizesPhaseTransition: false` por contrato + `externalWorkAuthorizesTransition`⇒`false` (completar un workflow de Jira NO autoriza un phase gate). Verificado: core-domain 939/939 (+6), tsc limpio. **Pendiente (conector/infra + doc):** fetch read-only de la Jira REST API + la guía de integración documentada. | `Evolith Core` | Cross | P1 | L | `EN-PROGRESO` | | [`GT-530`](./gap-reference-catalog.es.md#gt-530) | **[Surround · eje 1] Adaptador Langfuse → evidencia canónica.** Mapear traces/evaluaciones/costo/latencia/versión-de-prompt/tool-calls de Langfuse al modelo de evidencia de Evolith, para no reconstruir una plataforma de telemetría LLM. §8.1 / §12. Fix: `LangfuseEvidenceAdapter` vía puerto de observabilidad. | `Evolith Core` | Cross | P2 | L | `PENDIENTE` | | [`GT-531`](./gap-reference-catalog.es.md#gt-531) | **[Surround · eje 1] Adaptador Cowork/Claude como ejecutor gobernado acotado.** Ejecutor de actividades con permisos/planes/aprobaciones/captura de evidencia, tratando a Claude como uno de varios ejecutores reemplazables (§8.2, §9). Extiende el épico agent-runtime GT-383…394 (HITL GT-441 / adapters GT-438). | `agent-runtime` | Cross | P2 | M | `PENDIENTE` | | [`GT-532`](./gap-reference-catalog.es.md#gt-532) | **[Surround · eje 1] Vistas ejecutivas de portafolio + adaptadores marketplace + paquetes de gobernanza por tenant.** Mejora de adopción empresarial y escala de ecosistema (§12 P2); mayormente Tracker (plano de captura de valor enterprise). | `Tracker` | Cross | P3 | XL | `PENDIENTE` | @@ -547,7 +547,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-246`](./gap-reference-catalog.es.md#gt-246) | Implementar experimentos Chaos Mesh/Litmus | `QA` | Cross | P3 | L | `COMPLETADO` | -**Progreso:** 496 / 532 completados · 17 en progreso · 17 pendientes · 2 diferido +**Progreso:** 496 / 532 completados · 18 en progreso · 16 pendientes · 2 diferido **Oleada 2026-06-23 (auditoría profunda de Winston III):** Añadidos 14 gaps nuevos `GT-212`…`GT-225` del Winston Audit Playbook que cubren: higiene de estado ADR (GT-212), metadata + presupuestos operativos + corpus de guías por topología (GT-213, GT-217, GT-219), observabilidad + OpenAPI en controladores REST (GT-214, GT-215), paridad de input-schemas OPA + densidad de tests por topología (GT-216, GT-222), plantillas de rollback + on-call de Fase 05 (GT-218), cobertura de ramas CLI + paridad de envelope --format + limpieza de skip-list (GT-220, GT-224, GT-225), audit logging HTTP de MCP (GT-221), y tests e2e de paridad cross-surface (GT-223). diff --git a/reference/core/control-center/gaps/gap-tracking.md b/reference/core/control-center/gaps/gap-tracking.md index 2684802f4..da0d52600 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -33,7 +33,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-526`](./gap-reference-catalog.md#gt-526) | **[Control surface · axis 2] `edit-time` enforcement — cross-agent hook.** The 3rd of the three READ→CONTROL surfaces (§14.1): pre-generation (MCP, partial GT-520) and PR/CI (GT-518) exist, but the hook that blocks the offending change in-flight as the agent writes is missing. No market incumbent. Fix: cross-agent hook (Claude Code/Cursor/Copilot) that queries the architecture contract and rejects the non-conforming edit. Analysis §14. **IN-PROGRESS (`edit-gate.ts`):** landed the verifiable core — `evaluateEdit(edit, rules)` decides **allow/block in-flight** with a fast single-file check (no toolchain; GT-518's PR/CI stays the authoritative gate): `extractImports` (TS/JS `import`/`export from`/`require` + C# `using`) + `EditBoundaryRule` boundary rules (`appliesTo`/`forbiddenImports`), emits canonical `Violation`s (tool `edit-gate`, reusable by evidence/compliance), `allow=false` only on `error` severity. Agent-neutral (pure function). Verified: core-domain 917/917 (+7), tsc clean. **Remaining (integration):** the per-agent adapter (Claude Code PreToolUse hook / Cursor extension) that feeds the edit and enforces the decision. | `Evolith CLI` | Cross | P1 | M | `IN-PROGRESS` | | [`GT-527`](./gap-reference-catalog.md#gt-527) | **[Wedge · axis 2] Lock-in-free ownership ingestion connectors: Port / Cortex / OpsLevel + Backstage.** Ingest IDP blueprints and `catalog-info.yaml` as an ownership/service source to enrich violations and ADRs without vendor lock-in; Evolith blocks where they only measure (§13.2). Fix: read-only connectors via ACL normalizing to the canonical ownership shape. **IN-PROGRESS (`domain/ownership.ts`):** landed the pure normalization core — canonical `OwnershipEntry`, `parseBackstageCatalog` (`catalog-info.yaml` kind Component → owner + pathPrefix via `evolith.io/path`/relative `source-location`; drops non-Components/incomplete), `parseBlueprintOwnership` (generic Port/Cortex/OpsLevel: `component`\|`identifier` + `owner`\|`team`), `resolveOwner` (file→owner longest-prefix) and `enrichViolationsWithOwner` (fills `owner` without overwriting, excluded from the fingerprint). **Completes the violation→owner→compliance chain** (composes with GT-525). Verified: core-domain 925/925 (+8), tsc clean. **Remaining (connector/infra):** reading the repo's `catalog-info.yaml` (via the config-parser port) and the read-only Port/Cortex API fetch. | `Evolith Core` | Cross | P2 | L | `IN-PROGRESS` | | [`GT-528`](./gap-reference-catalog.md#gt-528) | **[Wedge · axis 2] Structurizr / C4 DSL ingestion → executable ADR.** Turn Structurizr/C4 models (architecture intent, today prose/diagram) into `enforce:` rules verifiable against real code (§13.2). Fix: DSL parser + mapping to `NormalizedRule.enforce`; complements GT-516. **IN-PROGRESS (`c4-compiler.ts`):** landed the compiler — `compileC4ToBoundaryRules(model)` turns a normalized `C4Model` (elements with `path`/`importPrefix` + allowed relationships) into GT-526 `EditBoundaryRule`s, deriving the denylist from the model's allowlist (an element may only depend on what it declares; everything else is forbidden), with `ruleId` `C4-`, ADR and severity. **The C4 diagram becomes executable** and feeds the edit-time gate (GT-526) and PR/CI. Verified end-to-end: a `src/domain` edit importing `src/infrastructure` → blocked; core-domain 933/933 (+5), tsc clean. **Remaining (ingestion):** parse the raw Structurizr `.dsl` (or its JSON export) into the normalized `C4Model`. | `Evolith Core` | Cross | P2 | M | `IN-PROGRESS` | -| [`GT-529`](./gap-reference-catalog.md#gt-529) | **[Surround · axis 1] ACL contract + Jira Enterprise integration reference.** Map Jira ideas/epics/stories/approvals/releases to Evolith artifacts preserving origin/identity/timestamps/lineage, with transition safeguards (completing a Jira workflow does not authorize a phase transition). §8.3 / §12. Fix: external work-system ACL + integration guide. | `Evolith Core` | Cross | P1 | L | `PENDING` | +| [`GT-529`](./gap-reference-catalog.md#gt-529) | **[Surround · axis 1] ACL contract + Jira Enterprise integration reference.** Map Jira ideas/epics/stories/approvals/releases to Evolith artifacts preserving origin/identity/timestamps/lineage, with transition safeguards (completing a Jira workflow does not authorize a phase transition). §8.3 / §12. Fix: external work-system ACL + integration guide. **IN-PROGRESS (`domain/external-work-acl.ts`):** landed the pure ACL — `CanonicalWorkItem` with `WorkItemProvenance` (source/externalId/externalKey/url/created/updated), `parseJiraIssue`/`parseJiraIssues` (maps Jira issues preserving provenance; rejects a missing `id` rather than fabricating identity), `mapJiraIssueType` (Epic/Story/Task/Version→canonical kind). **Transition safeguard (§9-6):** `authorizesPhaseTransition: false` by contract + `externalWorkAuthorizesTransition`⇒`false` (completing a Jira workflow does NOT authorize a phase gate). Verified: core-domain 939/939 (+6), tsc clean. **Remaining (connector/infra + doc):** the read-only Jira REST API fetch + the documented integration guide. | `Evolith Core` | Cross | P1 | L | `IN-PROGRESS` | | [`GT-530`](./gap-reference-catalog.md#gt-530) | **[Surround · axis 1] Langfuse adapter → canonical evidence.** Map Langfuse traces/evaluations/cost/latency/prompt-version/tool-calls to Evolith's evidence model, to avoid rebuilding an LLM telemetry platform. §8.1 / §12. Fix: `LangfuseEvidenceAdapter` via the observability port. | `Evolith Core` | Cross | P2 | L | `PENDING` | | [`GT-531`](./gap-reference-catalog.md#gt-531) | **[Surround · axis 1] Cowork/Claude adapter as a bounded governed executor.** Activity executor with permissions/plans/approvals/evidence capture, treating Claude as one of several replaceable executors (§8.2, §9). Extends the agent-runtime epic GT-383…394 (HITL GT-441 / adapters GT-438). | `agent-runtime` | Cross | P2 | M | `PENDING` | | [`GT-532`](./gap-reference-catalog.md#gt-532) | **[Surround · axis 1] Executive portfolio views + marketplace adapters + per-tenant governance packages.** Enterprise-adoption and ecosystem-scale improvement (§12 P2); mostly Tracker (enterprise value-capture plane). | `Tracker` | Cross | P3 | XL | `PENDING` | @@ -547,7 +547,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-246`](./gap-reference-catalog.md#gt-246) | Implement Chaos Mesh/Litmus experiments | `QA` | Cross | P3 | L | `DONE` | -**Progress:** 496 / 532 done · 17 in progress · 17 pending · 2 deferred +**Progress:** 496 / 532 done · 18 in progress · 16 pending · 2 deferred **Wave 2026-06-23 (Winston deep audit III):** Added 14 new gaps `GT-212`…`GT-225` from the Winston Audit Playbook covering: ADR status hygiene (GT-212), topology manifest metadata + operational budgets + guidance corpus (GT-213, GT-217, GT-219), REST controller observability + OpenAPI (GT-214, GT-215), OPA input-schema parity + per-topology test density (GT-216, GT-222), SDLC Phase 05 rollback + on-call templates (GT-218), CLI branch coverage + envelope format coverage + skip-list cleanup (GT-220, GT-224, GT-225), MCP HTTP audit logging (GT-221), and cross-surface parity e2e tests (GT-223). diff --git a/src/packages/core-domain/src/domain/external-work-acl.spec.ts b/src/packages/core-domain/src/domain/external-work-acl.spec.ts new file mode 100644 index 000000000..219d535f5 --- /dev/null +++ b/src/packages/core-domain/src/domain/external-work-acl.spec.ts @@ -0,0 +1,72 @@ +import { + externalWorkAuthorizesTransition, + mapJiraIssueType, + parseJiraIssue, + parseJiraIssues, + type JiraIssue, +} from './external-work-acl'; + +const issue = (over: Partial & { fields?: Partial> } = {}): JiraIssue => ({ + id: '10001', + key: 'PROJ-123', + self: 'https://acme.atlassian.net/rest/api/3/issue/10001', + fields: { + summary: 'Add checkout flow', + issuetype: { name: 'Story' }, + status: { name: 'In Progress' }, + created: '2026-07-01T10:00:00.000Z', + updated: '2026-07-10T12:00:00.000Z', + ...over.fields, + }, + ...over, +}); + +describe('mapJiraIssueType (GT-529 — Jira type → canonical kind)', () => { + it('maps the common Jira types case-insensitively, unknown otherwise', () => { + expect(mapJiraIssueType('Epic')).toBe('epic'); + expect(mapJiraIssueType('user story')).toBe('story'); + expect(mapJiraIssueType('Sub-task')).toBe('task'); + expect(mapJiraIssueType('Version')).toBe('release'); + expect(mapJiraIssueType('Spike')).toBe('unknown'); + expect(mapJiraIssueType(undefined)).toBe('unknown'); + }); +}); + +describe('parseJiraIssue (ACL — preserve provenance, reject non-conforming)', () => { + it('preserves origin, identity, key, url and timestamps', () => { + const item = parseJiraIssue(issue())!; + expect(item.kind).toBe('story'); + expect(item.title).toBe('Add checkout flow'); + expect(item.status).toBe('In Progress'); + expect(item.provenance).toEqual({ + source: 'jira', + externalId: '10001', + externalKey: 'PROJ-123', + url: 'https://acme.atlassian.net/rest/api/3/issue/10001', + createdAt: '2026-07-01T10:00:00.000Z', + updatedAt: '2026-07-10T12:00:00.000Z', + }); + }); + + it('rejects an issue with no id (no identity → no lineage), never fabricating one', () => { + expect(parseJiraIssue({ key: 'PROJ-1', fields: { summary: 's' } })).toBeNull(); + }); + + it('falls back to key then id for the title when summary is absent', () => { + expect(parseJiraIssue({ id: '9', key: 'PROJ-9' })!.title).toBe('PROJ-9'); + expect(parseJiraIssue({ id: '9' })!.title).toBe('9'); + }); + + it('parseJiraIssues drops non-conforming items', () => { + const out = parseJiraIssues([issue({ id: '1' }), { key: 'no-id' }, issue({ id: '2' })]); + expect(out.map((i) => i.provenance.externalId)).toEqual(['1', '2']); + }); +}); + +describe('transition safeguard (§9-6 — external work never authorizes a gate)', () => { + it('authorizesPhaseTransition is false regardless of Jira status', () => { + const done = parseJiraIssue(issue({ fields: { status: { name: 'Done' } } }))!; + expect(done.authorizesPhaseTransition).toBe(false); + expect(externalWorkAuthorizesTransition(done)).toBe(false); + }); +}); diff --git a/src/packages/core-domain/src/domain/external-work-acl.ts b/src/packages/core-domain/src/domain/external-work-acl.ts new file mode 100644 index 000000000..0d51a39f3 --- /dev/null +++ b/src/packages/core-domain/src/domain/external-work-acl.ts @@ -0,0 +1,120 @@ +/** + * External work-system Anti-Corruption Layer (GT-529 · axis 1 — positioning §8.3 / §9-6). + * + * Lets Evolith INTEGRATE an external work system (Jira first) WITHOUT ceding governance + * authority. It maps external work items (ideas/epics/stories/tasks/releases) to ONE canonical + * shape that PRESERVES provenance — origin, identity, timestamps, lineage — and it encodes the + * transition safeguard: external work INFORMS but never AUTHORIZES a phase transition (completing + * a Jira workflow does not drive an Evolith gate). Non-conforming items (no identity) are rejected, + * not silently trusted. + * + * Layering: PURE. Fetching from the Jira REST API is the read-only connector/infra step (follow-on); + * this takes already-fetched objects and normalizes them across sources. + */ + +/** Where an imported work item came from — the lineage Evolith must preserve (§8.3). */ +export interface WorkItemProvenance { + readonly source: string; // e.g. 'jira' + /** Stable external id (never shown as an internal id). */ + readonly externalId: string; + /** Human external key, e.g. `PROJ-123`. */ + readonly externalKey?: string; + /** Link back to the origin record. */ + readonly url?: string; + readonly createdAt?: string; + readonly updatedAt?: string; +} + +export type WorkItemKind = 'idea' | 'epic' | 'story' | 'task' | 'release' | 'unknown'; + +/** An external work item, normalized behind the ACL. */ +export interface CanonicalWorkItem { + readonly kind: WorkItemKind; + readonly title: string; + readonly status?: string; + readonly provenance: WorkItemProvenance; + /** + * By CONTRACT always `false`: external work never authorizes a phase transition on its own + * (§9-6). Kept in the model so a consumer cannot forget the safeguard. + */ + readonly authorizesPhaseTransition: false; +} + +/** + * The transition safeguard (§9-6). External work-item state — however "done" in Jira — never + * authorizes an Evolith phase transition by itself. Always `false`, by design. + */ +export function externalWorkAuthorizesTransition(_item: CanonicalWorkItem): boolean { + return false; +} + +// --------------------------------------------------------------------------- +// Jira mapper (the first external work system) +// --------------------------------------------------------------------------- + +/** Partial Jira issue shape (Jira REST v3) — only the fields the ACL reads. */ +export interface JiraIssue { + readonly id?: string; + readonly key?: string; + readonly self?: string; // canonical url + readonly fields?: { + readonly summary?: string; + readonly issuetype?: { readonly name?: string }; + readonly status?: { readonly name?: string }; + readonly created?: string; + readonly updated?: string; + }; +} + +const JIRA_TYPE_MAP: Readonly> = { + idea: 'idea', + epic: 'epic', + story: 'story', + 'user story': 'story', + task: 'task', + 'sub-task': 'task', + subtask: 'task', + bug: 'task', + release: 'release', + version: 'release', +}; + +/** Map a Jira issue-type name to a canonical {@link WorkItemKind} (`unknown` when unmapped). */ +export function mapJiraIssueType(name: string | undefined): WorkItemKind { + if (!name) return 'unknown'; + return JIRA_TYPE_MAP[name.trim().toLowerCase()] ?? 'unknown'; +} + +/** + * Map a Jira issue to a {@link CanonicalWorkItem}, preserving provenance. Returns `null` for a + * non-conforming issue (no `id` → no identity/lineage) — the ACL rejects rather than fabricates. + */ +export function parseJiraIssue(issue: JiraIssue): CanonicalWorkItem | null { + const externalId = issue.id; + if (!externalId) return null; + const f = issue.fields ?? {}; + return { + kind: mapJiraIssueType(f.issuetype?.name), + title: f.summary ?? issue.key ?? externalId, + status: f.status?.name, + provenance: { + source: 'jira', + externalId, + externalKey: issue.key, + url: issue.self, + createdAt: f.created, + updatedAt: f.updated, + }, + authorizesPhaseTransition: false, + }; +} + +/** Map a batch of Jira issues, dropping non-conforming ones (read-only, no fabrication). */ +export function parseJiraIssues(issues: readonly JiraIssue[]): CanonicalWorkItem[] { + const out: CanonicalWorkItem[] = []; + for (const issue of issues) { + const item = parseJiraIssue(issue); + if (item) out.push(item); + } + return out; +}