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), + ]} + /> + )}