From c4911dd077f791a978e367581cfadebdcffeddf8 Mon Sep 17 00:00:00 2001 From: Alberto Arroyo Raygada Date: Wed, 8 Jul 2026 15:50:01 -0500 Subject: [PATCH] =?UTF-8?q?feat(gates):=20T-036=20phase=203=20=E2=80=94=20?= =?UTF-8?q?criteria=20artifactType/fieldPath=20from=20Core=20catalog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Criteria editor no longer accepts arbitrary artifactType/fieldPath: - artifactType is phase-aware: intake→opportunity (locked, single option), phase gates→the phase's Core deliverable artifacts (PhaseArtifactProfile). - fieldPath is a Select over baseFields ∪ custom. from ArtifactFieldSchema (no free text); empty-state hint when the artifact has no catalog fields. - reset fieldPath on artifactType change; addCriterion phase-gate default = first phase artifact instead of hardcoded 'initiative'. - removed hardcoded CRITERION_ARTIFACTS constant. Implements T-036 §2.2/§5 (design point 5). Save contract unchanged. Co-Authored-By: Claude Opus 4.8 --- .../src/components/gate/GatePolicyEditor.tsx | 79 ++++++++++++++----- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/src/apps/tracker-web/src/components/gate/GatePolicyEditor.tsx b/src/apps/tracker-web/src/components/gate/GatePolicyEditor.tsx index 80e530e..6cb03fd 100644 --- a/src/apps/tracker-web/src/components/gate/GatePolicyEditor.tsx +++ b/src/apps/tracker-web/src/components/gate/GatePolicyEditor.tsx @@ -8,7 +8,7 @@ is unchanged. ========================================================================= */ import React from 'react'; -import { ApiError, usePhaseArtifactProfiles, useUpsertGatePolicy } from '../../api'; +import { ApiError, useArtifactFieldSchemas, usePhaseArtifactProfiles, useUpsertGatePolicy } from '../../api'; import type { ApprovalStages, ApprovalStrategy, @@ -37,12 +37,6 @@ const CRITERION_OPERATORS = [ 'gte', 'lte', 'between', 'regex', 'min-length', 'count-gte', ] as const; -/** Artifact the criteria evaluate against — opportunity at the entry gate, initiative at phase gates. */ -const CRITERION_ARTIFACTS = [ - { value: 'opportunity', label: 'Opportunity' }, - { value: 'initiative', label: 'Initiative' }, -] as const; - /** A blank default policy so a matrix renders a row per phase even with no data. */ export function defaultGatePolicy(phase: string): GatePolicyDto { return { @@ -88,6 +82,36 @@ const EditPolicyBody: React.FC<{ (kind: string) => phaseArtifacts.find((a) => a.artifactKind === kind)?.label ?? kind, [phaseArtifacts], ); + // T-036 §3.5: criteria evaluate an artifact's fields; the artifact source is + // phase-dependent (intake → the opportunity; phase gates → a Core deliverable + // of the phase). The `fieldPath` is picked from the artifact's field catalog + // (base ∪ custom.) — never free text. + const schemas = useArtifactFieldSchemas(); + + /** The artifact types criteria may evaluate for this phase. */ + const artifactOptions = React.useMemo( + () => + phase === 'intake' + ? [{ value: 'opportunity', label: t('Opportunity', 'Oportunidad') }] + : phaseArtifacts.map((a) => ({ value: a.artifactKind, label: a.label })), + [phase, phaseArtifacts, t], + ); + + /** Field-path options for an artifact type = base fields ∪ custom.. */ + const fieldOptionsFor = React.useCallback( + (artifactType: string): { value: string; label: string }[] => { + const schema = schemas.data?.find((s) => s.artifactType === artifactType); + if (!schema) return []; + return [ + ...schema.baseFields.map((f) => ({ value: f, label: f })), + ...schema.customFields.map((cf) => ({ + value: `custom.${cf.name}`, + label: `custom.${cf.name} · ${cf.label}`, + })), + ]; + }, + [schemas.data], + ); const [st, setSt] = React.useState(() => ({ mode: (policy.mode as GateMode) ?? 'SIMPLE', requiredEvidence: policy.requiredEvidence.map((e) => ({ ...e })), @@ -108,7 +132,7 @@ const EditPolicyBody: React.FC<{ { id: crypto.randomUUID(), label: '', - artifactType: phase === 'intake' ? 'opportunity' : 'initiative', + artifactType: phase === 'intake' ? 'opportunity' : (phaseArtifacts[0]?.artifactKind ?? ''), fieldPath: '', operator: 'non-empty', expected: [], @@ -444,10 +468,10 @@ const EditPolicyBody: React.FC<{

- {t('Field-level checks on the ', 'Chequeos a nivel de campo sobre ')} + {t('Field-level checks on ', 'Chequeos a nivel de campo sobre ')} {phase === 'intake' - ? t('opportunity', 'la opportunity') - : t('initiative', 'la initiative')} + ? t('the opportunity', 'la oportunidad') + : t('the selected artifact', 'el artefacto seleccionado')} {t('. A mandatory ', '. Un criterio obligatorio ')} {t('blocking', 'blocking')} {t(' criterion left unmet returns the gate; ', ' sin cumplir devuelve el gate; los ')} @@ -516,9 +540,14 @@ const EditPolicyBody: React.FC<{

updateCriterion(i, { fieldPath: e.target.value })} + options={[ + { value: '', label: t('Select a field…', 'Selecciona un campo…') }, + ...fieldOptionsFor(c.artifactType), + ]} + /> + )}