feat(F-091): completed feature
This commit is contained in:
@@ -4128,6 +4128,40 @@
|
|||||||
"close": true
|
"close": true
|
||||||
},
|
},
|
||||||
"completed_at": "2026-08-20T19:40:41Z"
|
"completed_at": "2026-08-20T19:40:41Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "F-091",
|
||||||
|
"type": "fix",
|
||||||
|
"title": "Explain and prevent duplicate SKU EAN update conflicts",
|
||||||
|
"problem": "Variant updates return an opaque 409 when the requested SKU or EAN already belongs to another variant, and Enter plus blur can submit the same edit twice. The admin only shows a generic Error, so operators cannot tell which value conflicts.",
|
||||||
|
"goal": "Keep SKU and EAN uniqueness, prevent duplicate in-flight submissions, and return/display a field-specific conflict message for SKU or EAN.",
|
||||||
|
"scope_in": [
|
||||||
|
"variant repository error mapping and admin InventorySection"
|
||||||
|
],
|
||||||
|
"scope_out": [
|
||||||
|
"No relaxation of uniqueness",
|
||||||
|
"no schema change",
|
||||||
|
"no duplicate codes allowed"
|
||||||
|
],
|
||||||
|
"priority": "med",
|
||||||
|
"risk": "low",
|
||||||
|
"description": "Problem: Variant updates return an opaque 409 when the requested SKU or EAN already belongs to another variant, and Enter plus blur can submit the same edit twice. The admin only shows a generic Error, so operators cannot tell which value conflicts.. Goal: Keep SKU and EAN uniqueness, prevent duplicate in-flight submissions, and return/display a field-specific conflict message for SKU or EAN.. Scope IN: variant repository error mapping and admin InventorySection. Scope OUT: No relaxation of uniqueness, no schema change, no duplicate codes allowed. Type: fix. Priority: med. Risk: low.",
|
||||||
|
"acceptance": [
|
||||||
|
"- Repeated Enter/blur interaction sends at most one in-flight PATCH per SKU or EAN field",
|
||||||
|
"- Duplicate SKU conflict returns a clear SKU-specific message",
|
||||||
|
"- Duplicate EAN conflict returns a clear EAN-specific message",
|
||||||
|
"- Existing unique values continue to save",
|
||||||
|
"- Typecheck, tests, and verify.sh pass"
|
||||||
|
],
|
||||||
|
"status": "done",
|
||||||
|
"created_at": "2026-08-20",
|
||||||
|
"gates": {
|
||||||
|
"reviewer": true,
|
||||||
|
"security": true,
|
||||||
|
"qa": true,
|
||||||
|
"close": true
|
||||||
|
},
|
||||||
|
"completed_at": "2026-08-20T19:47:31Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { productsApi, inventoryApi, pricingApi } from '@/lib/api-client';
|
import { productsApi, inventoryApi, pricingApi } from '@/lib/api-client';
|
||||||
import type { ProductVariant, VariantPrice, StockAvailability } from '@/types';
|
import { ApiError, type ProductVariant, type VariantPrice, type StockAvailability } from '@/types';
|
||||||
|
|
||||||
interface VariantRow {
|
interface VariantRow {
|
||||||
variant: ProductVariant;
|
variant: ProductVariant;
|
||||||
@@ -75,6 +75,7 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
|||||||
const [rows, setRows] = useState<Record<string, VariantRow>>({});
|
const [rows, setRows] = useState<Record<string, VariantRow>>({});
|
||||||
const [savingVariant, setSavingVariant] = useState<string | null>(null);
|
const [savingVariant, setSavingVariant] = useState<string | null>(null);
|
||||||
const [saveMsg, setSaveMsg] = useState<Record<string, string>>({});
|
const [saveMsg, setSaveMsg] = useState<Record<string, string>>({});
|
||||||
|
const savingCodes = useRef(new Set<string>());
|
||||||
|
|
||||||
// Load variants
|
// Load variants
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -307,6 +308,9 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
|||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const saveKey = `sku:${variantId}`;
|
||||||
|
if (savingCodes.current.has(saveKey)) return;
|
||||||
|
savingCodes.current.add(saveKey);
|
||||||
setRows((prev) => ({ ...prev, [variantId]: { ...r, savingSku: true } }));
|
setRows((prev) => ({ ...prev, [variantId]: { ...r, savingSku: true } }));
|
||||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '' }));
|
setSaveMsg((prev) => ({ ...prev, [variantId]: '' }));
|
||||||
try {
|
try {
|
||||||
@@ -323,12 +327,17 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
|||||||
}));
|
}));
|
||||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '✓ Guardado' }));
|
setSaveMsg((prev) => ({ ...prev, [variantId]: '✓ Guardado' }));
|
||||||
setTimeout(() => setSaveMsg((prev) => ({ ...prev, [variantId]: '' })), 3000);
|
setTimeout(() => setSaveMsg((prev) => ({ ...prev, [variantId]: '' })), 3000);
|
||||||
} catch {
|
} catch (error) {
|
||||||
setSaveMsg((prev) => ({ ...prev, [variantId]: 'Error' }));
|
const message = error instanceof ApiError && error.statusCode === 409
|
||||||
|
? 'El SKU ya existe en otra variante'
|
||||||
|
: 'No se pudo guardar el SKU';
|
||||||
|
setSaveMsg((prev) => ({ ...prev, [variantId]: message }));
|
||||||
setRows((prev) => ({
|
setRows((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[variantId]: { ...prev[variantId], editingSku: false, savingSku: false, skuValue: prev[variantId].variant.sku },
|
[variantId]: { ...prev[variantId], editingSku: false, savingSku: false, skuValue: prev[variantId].variant.sku },
|
||||||
}));
|
}));
|
||||||
|
} finally {
|
||||||
|
savingCodes.current.delete(saveKey);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -342,6 +351,9 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
|||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const saveKey = `ean:${variantId}`;
|
||||||
|
if (savingCodes.current.has(saveKey)) return;
|
||||||
|
savingCodes.current.add(saveKey);
|
||||||
setRows((prev) => ({ ...prev, [variantId]: { ...r, savingEan: true } }));
|
setRows((prev) => ({ ...prev, [variantId]: { ...r, savingEan: true } }));
|
||||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '' }));
|
setSaveMsg((prev) => ({ ...prev, [variantId]: '' }));
|
||||||
try {
|
try {
|
||||||
@@ -358,12 +370,17 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
|||||||
}));
|
}));
|
||||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '✓ Guardado' }));
|
setSaveMsg((prev) => ({ ...prev, [variantId]: '✓ Guardado' }));
|
||||||
setTimeout(() => setSaveMsg((prev) => ({ ...prev, [variantId]: '' })), 3000);
|
setTimeout(() => setSaveMsg((prev) => ({ ...prev, [variantId]: '' })), 3000);
|
||||||
} catch {
|
} catch (error) {
|
||||||
setSaveMsg((prev) => ({ ...prev, [variantId]: 'Error' }));
|
const message = error instanceof ApiError && error.statusCode === 409
|
||||||
|
? 'El EAN ya existe en otra variante'
|
||||||
|
: 'No se pudo guardar el EAN';
|
||||||
|
setSaveMsg((prev) => ({ ...prev, [variantId]: message }));
|
||||||
setRows((prev) => ({
|
setRows((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[variantId]: { ...prev[variantId], editingEan: false, savingEan: false, eanValue: prev[variantId].variant.ean ?? '' },
|
[variantId]: { ...prev[variantId], editingEan: false, savingEan: false, eanValue: prev[variantId].variant.ean ?? '' },
|
||||||
}));
|
}));
|
||||||
|
} finally {
|
||||||
|
savingCodes.current.delete(saveKey);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -19,9 +19,11 @@ export class ProductBrandNotFoundError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProductVariantCode = 'sku' | 'ean';
|
||||||
|
|
||||||
export class ProductVariantCodeAlreadyExistsError extends Error {
|
export class ProductVariantCodeAlreadyExistsError extends Error {
|
||||||
constructor() {
|
constructor(public readonly field?: ProductVariantCode) {
|
||||||
super('Product variant SKU or EAN already exists');
|
super(field ? `Product variant ${field.toUpperCase()} already exists` : 'Product variant SKU or EAN already exists');
|
||||||
this.name = 'ProductVariantCodeAlreadyExistsError';
|
this.name = 'ProductVariantCodeAlreadyExistsError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type pg from 'pg';
|
import type pg from 'pg';
|
||||||
import { ProductVariantCodeAlreadyExistsError } from '../domain/errors.js';
|
import {
|
||||||
|
ProductVariantCodeAlreadyExistsError,
|
||||||
|
type ProductVariantCode,
|
||||||
|
} from '../domain/errors.js';
|
||||||
import type { ProductVariantRepository } from '../domain/ports.js';
|
import type { ProductVariantRepository } from '../domain/ports.js';
|
||||||
import type { NewProductVariant, ProductVariant, ProductVariantPatch } from '../domain/variant.js';
|
import type { NewProductVariant, ProductVariant, ProductVariantPatch } from '../domain/variant.js';
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ export class PgProductVariantRepository implements ProductVariantRepository {
|
|||||||
return toVariant(row);
|
return toVariant(row);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isPgError(error, UNIQUE_VIOLATION)) {
|
if (isPgError(error, UNIQUE_VIOLATION)) {
|
||||||
throw new ProductVariantCodeAlreadyExistsError();
|
throw new ProductVariantCodeAlreadyExistsError(uniqueConstraintField(error));
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -82,7 +85,7 @@ export class PgProductVariantRepository implements ProductVariantRepository {
|
|||||||
return row ? toVariant(row) : undefined;
|
return row ? toVariant(row) : undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isPgError(error, UNIQUE_VIOLATION)) {
|
if (isPgError(error, UNIQUE_VIOLATION)) {
|
||||||
throw new ProductVariantCodeAlreadyExistsError();
|
throw new ProductVariantCodeAlreadyExistsError(uniqueConstraintField(error));
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -116,3 +119,14 @@ function toVariant(row: VariantRow): ProductVariant {
|
|||||||
function isPgError(error: unknown, code: string): boolean {
|
function isPgError(error: unknown, code: string): boolean {
|
||||||
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uniqueConstraintField(error: unknown): ProductVariantCode | undefined {
|
||||||
|
if (typeof error !== 'object' || error === null || !('constraint' in error)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const constraint = error.constraint;
|
||||||
|
if (typeof constraint !== 'string') return undefined;
|
||||||
|
if (constraint.endsWith('_sku_key')) return 'sku';
|
||||||
|
if (constraint.endsWith('_ean_key')) return 'ean';
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|||||||
17
work/artifacts/F-091/implementer.md
Normal file
17
work/artifacts/F-091/implementer.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# F-091 — Implementer evidence
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- `project/src/modules/catalog/domain/errors.ts` now records whether a duplicate variant code is `sku` or `ean`.
|
||||||
|
- `project/src/modules/catalog/infrastructure/pg-variant-repository.ts` maps PostgreSQL unique constraints (`*_sku_key`, `*_ean_key`) to that field while preserving the 409 uniqueness rule.
|
||||||
|
- `project/apps/admin/src/features/products/components/sections/InventorySection.tsx`
|
||||||
|
- Guards each SKU/EAN variant save with an in-flight key so Enter followed by blur cannot submit duplicate PATCH requests.
|
||||||
|
- Shows `El SKU ya existe en otra variante` or `El EAN ya existe en otra variante` for 409 conflicts instead of generic `Error`.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Root `npm run typecheck` → exit 0
|
||||||
|
- Root `npm test -- --run` → 133 passed, 56 skipped
|
||||||
|
- Admin `npx tsc --noEmit` → exit 0
|
||||||
|
- Admin ESLint on `InventorySection.tsx` → exit 0
|
||||||
|
- SKU/EAN uniqueness remains enforced by the existing database constraints.
|
||||||
15
work/artifacts/F-091/leader-close.json
Normal file
15
work/artifacts/F-091/leader-close.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-091",
|
||||||
|
"agent": "leader",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "F-091 keeps SKU/EAN uniqueness, prevents duplicate in-flight updates, and explains 409 conflicts by field.",
|
||||||
|
"evidence": [
|
||||||
|
"reviewer.json verdict=APPROVED",
|
||||||
|
"security.json verdict=APPROVED",
|
||||||
|
"qa.json verdict=APPROVED",
|
||||||
|
"scripts/verify.sh exit 0",
|
||||||
|
"Root tests: 133 passed, 56 skipped",
|
||||||
|
"Root and admin typechecks exit 0"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-20T19:47:30Z"
|
||||||
|
}
|
||||||
14
work/artifacts/F-091/qa.json
Normal file
14
work/artifacts/F-091/qa.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-091",
|
||||||
|
"agent": "qa",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "Variant conflict handling and duplicate-submit protection pass the complete available checks.",
|
||||||
|
"evidence": [
|
||||||
|
"Root npm run typecheck exit 0",
|
||||||
|
"Root npm test: 133 passed, 56 skipped",
|
||||||
|
"Admin npx tsc --noEmit exit 0",
|
||||||
|
"Admin ESLint on InventorySection exit 0",
|
||||||
|
"scripts/verify.sh exit 0"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-20T19:47:20Z"
|
||||||
|
}
|
||||||
13
work/artifacts/F-091/reviewer.json
Normal file
13
work/artifacts/F-091/reviewer.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-091",
|
||||||
|
"agent": "reviewer",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "The API keeps SKU/EAN uniqueness, identifies the conflicting field from the database constraint, and the UI prevents duplicate in-flight saves while presenting actionable conflict feedback.",
|
||||||
|
"evidence": [
|
||||||
|
"Existing 409 uniqueness behavior is preserved",
|
||||||
|
"SKU and EAN constraint names map to field-specific domain messages",
|
||||||
|
"Enter plus blur is guarded by a per-field in-flight key",
|
||||||
|
"Root and admin typechecks pass"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-20T19:47:00Z"
|
||||||
|
}
|
||||||
13
work/artifacts/F-091/security.json
Normal file
13
work/artifacts/F-091/security.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"feature_id": "F-091",
|
||||||
|
"agent": "security",
|
||||||
|
"verdict": "APPROVED",
|
||||||
|
"summary": "The fix does not relax uniqueness or alter authorization. Conflict messages reveal only that the submitted SKU/EAN is already in use, without exposing another variant's identity or data.",
|
||||||
|
"evidence": [
|
||||||
|
"Database UNIQUE constraints remain authoritative for SKU and EAN",
|
||||||
|
"No SQL interpolation or new input path added",
|
||||||
|
"Only field type (SKU/EAN) is returned, not conflicting records",
|
||||||
|
"No dependencies, secrets, auth, or permission changes"
|
||||||
|
],
|
||||||
|
"timestamp": "2026-08-20T19:47:10Z"
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
# Feature actual
|
# Feature actual
|
||||||
|
|
||||||
## Feature activa: ninguna — F-090 cerrada
|
## Feature activa: F-091 (in_progress) — Duplicate SKU/EAN update conflicts are opaque and can be submitted twice
|
||||||
|
|
||||||
Backlog: 158 features (149 done, 9 pending, 0 in_progress).
|
Backlog: 159 features (149 done, 9 pending, 1 in_progress).
|
||||||
|
|
||||||
Últimas features cerradas: **F-080**, **F-081**, **F-082**, **F-083**, **F-084**, **F-085**, **F-086**, **F-087**.
|
Últimas features cerradas: **F-080**, **F-081**, **F-082**, **F-083**, **F-084**, **F-085**, **F-086**, **F-087**.
|
||||||
|
|
||||||
|
## Incidencia actual (2026-08-20)
|
||||||
|
|
||||||
|
El PATCH de variantes devuelve `409 PRODUCT_VARIANT_CODE_EXISTS` cuando el SKU o EAN ya está usado por otra variante. El editor muestra solo `Error` y la combinación Enter + blur puede intentar enviar dos veces. F-091 conserva la unicidad, evita envíos simultáneos y muestra el campo en conflicto.
|
||||||
|
|
||||||
## Última incidencia resuelta (2026-08-20)
|
## Última incidencia resuelta (2026-08-20)
|
||||||
|
|
||||||
F-090 cerrada con todos los gates aprobados. El listado ya recibe marca/caducidad y los valores SKU/EAN guardados permanecen visibles tras salir del campo.
|
F-090 cerrada con todos los gates aprobados. El listado ya recibe marca/caducidad y los valores SKU/EAN guardados permanecen visibles tras salir del campo.
|
||||||
|
|||||||
@@ -1,55 +1,13 @@
|
|||||||
{
|
{
|
||||||
"feature_id": "F-090",
|
"feature_id": "F-091",
|
||||||
"stage": "close",
|
"stage": "close",
|
||||||
"agent": "leader",
|
"agent": "leader",
|
||||||
"action": "F-090 cerrado: marca/caducidad serializadas y SKU/EAN reflejan guardado",
|
"action": "Validate F-091 gates and close SKU/EAN conflict fix",
|
||||||
"state": "done",
|
"state": "running",
|
||||||
"next_agent": "leader",
|
"next_agent": "leader",
|
||||||
"waiting_for": "Seleccionar la siguiente feature pending",
|
"waiting_for": "verify.sh green",
|
||||||
"updated_at": "2026-08-20T19:40:49Z",
|
"updated_at": "2026-08-20T19:47:22Z",
|
||||||
"timeline": [
|
"timeline": [
|
||||||
{
|
|
||||||
"ts": "2026-08-20T19:16:31Z",
|
|
||||||
"agent": "implementer",
|
|
||||||
"stage": "build",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Extend taxApi.update signature to accept appliesTo enum; rebuild admin; respawn admin so port 3004 serves 200"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-20T19:28:23Z",
|
|
||||||
"agent": "reviewer",
|
|
||||||
"stage": "review_gate",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Review F-088 type-only fix and build evidence"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-20T19:28:31Z",
|
|
||||||
"agent": "security",
|
|
||||||
"stage": "security_gate",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Check F-088 enum validation, scope, and dependency impact"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-20T19:28:41Z",
|
|
||||||
"agent": "qa",
|
|
||||||
"stage": "qa_gate",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Run F-088 regression and endpoint smoke checks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-20T19:28:58Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "close",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Validate gates and repair legacy evidence metadata before closing F-088"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-20T19:29:27Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "close",
|
|
||||||
"state": "done",
|
|
||||||
"message": "F-088 cerrado: admin :3004 verificado HTTP 200 y verify.sh verde"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"ts": "2026-08-20T19:32:56Z",
|
"ts": "2026-08-20T19:32:56Z",
|
||||||
"agent": "leader",
|
"agent": "leader",
|
||||||
@@ -147,6 +105,48 @@
|
|||||||
"stage": "close",
|
"stage": "close",
|
||||||
"state": "done",
|
"state": "done",
|
||||||
"message": "F-090 cerrado: marca/caducidad serializadas y SKU/EAN reflejan guardado"
|
"message": "F-090 cerrado: marca/caducidad serializadas y SKU/EAN reflejan guardado"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-20T19:44:59Z",
|
||||||
|
"agent": "leader",
|
||||||
|
"stage": "intake",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Triage repeated variant PATCH 409 and opaque SKU/EAN conflict feedback"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-20T19:45:13Z",
|
||||||
|
"agent": "implementer",
|
||||||
|
"stage": "build",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Add field-specific SKU/EAN conflict messages and guard duplicate in-flight saves"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-20T19:46:54Z",
|
||||||
|
"agent": "reviewer",
|
||||||
|
"stage": "review_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Review duplicate variant conflict mapping and in-flight save guard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-20T19:47:03Z",
|
||||||
|
"agent": "security",
|
||||||
|
"stage": "security_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Check F-091 uniqueness enforcement and error disclosure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-20T19:47:11Z",
|
||||||
|
"agent": "qa",
|
||||||
|
"stage": "qa_gate",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Run F-091 typecheck, tests, lint, and verify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-08-20T19:47:22Z",
|
||||||
|
"agent": "leader",
|
||||||
|
"stage": "close",
|
||||||
|
"state": "running",
|
||||||
|
"message": "Validate F-091 gates and close SKU/EAN conflict fix"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user