Whitepaper · 11
Guía de implementación
Desde un clon limpio hasta un token de lote desplegado, con hooks y atestaciones, paso a paso. Cada bloque de JavaScript de este capítulo está copiado de test/standard/ImplementationGuide.test.js y se ejecuta en CI; si un bloque de aquí fuera incorrecto, la compilación fallaría.
Cómo usar esta guía
Los pasos se construyen unos sobre otros y comparten variables: token, attestations, los firmantes y los ids creados por el camino. Léanse en orden la primera vez. Para ejecutarlos por cuenta propia:
npx hardhat test test/standard/ImplementationGuide.test.js
| Paso | Qué se obtiene | Quién actúa | Llamadas principales |
|---|---|---|---|
| 1 · Preparación | una copia de trabajo compilada y probada | desarrollador | npm ci, npm test |
| 2 · Despliegue | proxy del token + registro de atestaciones | desplegador | upgrades.deployProxy, ARexAttestations |
| 3 · Roles | operadores con privilegios mínimos | administrador | grantRole |
| 4 · Creación | un lote con compromiso de metadatos | emisor | createBatch |
| 5 · Lectura | el registro del lote y el suministro | cualquiera | batchOf, totalSupply(uint256), parentsOf |
| 6 · Transferencia | una transferencia comprobada | titular | transferCheck, safeTransferFrom |
| 7 · Linaje | división, fusión, procesamiento | titular / transformador | transform |
| 8 · Estado | actualizaciones del ciclo de vida y de los metadatos | rol de estado | setBatchStatus, setBatchMetadata |
| 9 · Hooks | una canalización ordenada de reglas | administrador de hooks | registerHook, getHooks |
| 10 · Atestación | evidencia que desbloquea transferencias | emisor de confianza | setTrustedIssuer, attest |
| 11 · Sanciones | elegibilidad con fuente y TTL | gestor de elegibilidad | setSanctioned |
| 12 · Ejecución forzosa | congelación, pausa, transferencia forzosa | ejecutor forzoso / pausador | setFrozenTokens, forcedTransfer |
| 13 · Retiro | fin de la vida útil de un lote | rol de estado, titular | setBatchStatus(…, 255, …), burn |
| 14 · Indexación | el grafo de linaje a partir de los eventos | indexador | queryFilter(BatchTransformed) |
| 15 · Actualización | una actualización validada de la implementación | responsable de actualizaciones | validateUpgrade, upgradeProxy |
1 · Preparación
Node.js 20 o 22 LTS y npm. Las dependencias están fijadas en package-lock.json (OpenZeppelin 5.3.0); úsese npm ci para que la compilación propia sea la que construye CI.
git clone https://github.com/Arex-Corp/A-REX.git && cd A-REX
npm ci
npm run compile
npm test # 46 tests: reference implementation, hooks, extensions, invariants, this guide
npm run interface-ids # prints the ERC-165 ids; they must match the specification
Los fragmentos siguientes usan tres pequeñas funciones auxiliares y nombran diez cuentas. explain convierte un código de rechazo de nuevo en su nombre; la lista es el conjunto de códigos definidos en ARexCodes.
const commodity = (scheme, code) => ethers.keccak256(ethers.toUtf8Bytes(`${scheme}:${code}`));
const unit = (u) => ethers.encodeBytes32String(u);
const ref = (text) => ethers.keccak256(ethers.toUtf8Bytes(text));
const REASONS = Object.fromEntries(
["PAUSED", "FROZEN", "SENDER_INELIGIBLE", "RECEIVER_INELIGIBLE", "SANCTIONED", "BATCH_NOT_FOUND",
"BATCH_RETIRED", "BATCH_EXPIRED", "BATCH_NOT_PRODUCED", "ATTESTATION_MISSING", "SEASON_CLOSED",
"IN_TRANSIT", "LIMIT_EXCEEDED", "DOCUMENT_MISSING"].map((n) => [ethers.keccak256(ethers.toUtf8Bytes(`AREX:${n}`)), n]),
);
const explain = (code) => (code === ethers.ZeroHash ? "OK" : REASONS[code] ?? code);
const [admin, issuer, farmer, buyer, coop, mill, warehouse, certifier, enforcer, custodian] = await ethers.getSigners();
2 · Despliegue
El token de referencia es actualizable mediante UUPS, por lo que se despliega detrás de un proxy ERC-1967 con el plugin de actualizaciones de OpenZeppelin, que también valida la disposición del almacenamiento. El registro de atestaciones es un contrato sencillo vinculado al token.
const ARexToken = await ethers.getContractFactory("ARexToken");
const token = await upgrades.deployProxy(ARexToken, [admin.address, "ipfs://<cid>/{id}.json"], { kind: "uups" });
await token.waitForDeployment();
const attestations = await (await ethers.getContractFactory("ARexAttestations"))
.deploy(admin.address, await token.getAddress());
Desde la línea de comandos, el mismo conjunto, más los hooks de elegibilidad y de caducidad, se despliega con:
npx hardhat node # terminal 1
npx hardhat run scripts/deploy-standard.js --network localhost # token, attestations, 2 hooks
npx hardhat run scripts/deploy-full-standard.js --network localhost # every hook in order + extensions
o con Hardhat Ignition:
npx hardhat ignition deploy ignition/modules/ARexToken.js --network localhost \
--parameters '{"ARexTokenModule":{"baseURI":"ipfs://<cid>/{id}.json","allowPreHarvestTransfers":true}}'
Para una red de pruebas o una L2, hardhat.config.js define sepolia, polygon y arbitrum; configúrense la URL del RPC y PRIVATE_KEY en .env (plantilla en .env.example). ARexToken se compila a 24,136 bytes con optimizer.runs = 1, por debajo del límite de 24,576 bytes de EIP-170.
3 · Roles
initialize(admin, baseURI) concede todos los roles a admin, lo cual es cómodo para un primer despliegue e incorrecto para producción. Concédase cada rol operativo al servicio o a la multifirma que lo ejerce:
await token.grantRole(await token.ISSUER_ROLE(), issuer.address);
await token.grantRole(await token.TRANSFORMER_ROLE(), mill.address);
await token.grantRole(await token.STATUS_ROLE(), warehouse.address);
await token.grantRole(await token.ENFORCER_ROLE(), enforcer.address);
| Rol | Permite | Titular recomendado |
|---|---|---|
DEFAULT_ADMIN_ROLE | conceder y revocar cualquier rol; setBaseURI | multifirma de gobernanza detrás de un timelock |
UPGRADER_ROLE | sustituir la implementación | el mismo timelock |
HOOK_ADMIN_ROLE | registerHook, removeHook | el mismo timelock (los cambios de hooks son visibles antes de aplicarse) |
ENFORCER_ROLE | setFrozenTokens, forcedTransfer | multifirma de ejecución forzosa (cumplimiento y asuntos jurídicos) |
PAUSER_ROLE | pause, unpause | clave de respuesta a incidentes |
ISSUER_ROLE | createBatch | servicio de originación |
STATUS_ROLE | setBatchStatus, setBatchMetadata | almacén, inspector o registro |
TRANSFORMER_ROLE | transformaciones de procesamiento (cambio de producto básico, de unidad o de cantidad) | procesadores |
ATTEST_ADMIN_ROLE (en ARexAttestations) | setTrustedIssuer, revocar cualquier atestación | autoridad de certificación o plataforma |
Cuando cada rol tiene su titular, el desplegador renuncia a lo que ya no necesita con renounceRole(role, deployer).
4 · Crear un lote
Un lote es un id de ERC-1155. Su registro en la cadena es pequeño y fijo; todo lo descriptivo reside en un documento de metadatos cuyo hash queda comprometido en la cadena (esquema: standard/schema/batch-metadata.schema.json).
const metadata = { schemaVersion: "1.0.0", commodity: { scheme: "HS", code: "100199" }, unit: "KGM", quantity: 250000 };
const canonical = JSON.stringify(metadata); // RFC 8785 canonicalization in production
const metadataHash = ethers.keccak256(ethers.toUtf8Bytes(canonical));
const uri = "ipfs://<cid>/batch-1.json"; // wherever you stored `canonical`
const init = {
commodity: commodity("HS", "100199"), // wheat
unit: unit("KGM"),
producedAt: Math.floor(Date.parse("2026-03-18T00:00:00Z") / 1000), // 0 = not yet harvested
expiresAt: 0, // 0 = no expiry
originRef: ref("DDS-2026-AR-000123"), // a reference, never coordinates
metadataHash,
uri,
};
const id = await token.connect(issuer).createBatch.staticCall(init, 250_000, farmer.address, "0x");
await (await token.connect(issuer).createBatch(init, 250_000, farmer.address, "0x")).wait();
createBatch.staticCall devuelve el id que asignará la transacción; los ids empiezan en 1 y aumentan de uno en uno. El token rechaza:
| Condición | Error |
|---|---|
quien llama carece de ISSUER_ROLE | AccessControlUnauthorizedAccount(account, role) |
to es la dirección cero | ARexInvalidRecipient() |
quantity == 0 | ARexInvalidBatch("ZERO_QUANTITY") |
commodity == 0 o unit == 0 | ARexInvalidBatch("COMMODITY_REQUIRED") / ("UNIT_REQUIRED") |
ambas fechas definidas y expiresAt <= producedAt | ARexInvalidBatch("EXPIRY_BEFORE_PRODUCTION") |
en pausa, receptor no elegible, o un hook rechaza OP_MINT | EnforcedPause(), ERC7943CannotReceive(to), ARexHookRejected(hook, code) |
Convenciones de los campos:
| Campo | Convención | Ejemplo |
|---|---|---|
commodity | keccak256("<scheme>:<code>"), con scheme entre HS, FAO, GTIN, UNSPSC o uno propio | keccak256("HS:100199") trigo |
unit | código de la Recomendación 20 de UN/CEFACT como bytes32 | "KGM", "TNE", "LTR", "BX" |
producedAt | segundos Unix; 0 para un lote aún no cosechado | venta a plazo, derecho de cobro sobre cosecha |
expiresAt | segundos Unix; 0 si no caduca | productos perecederos |
originRef | hash de una referencia a la evidencia de origen; nunca coordenadas | referencia de DDS del EUDR, id de registro |
metadataHash | keccak256 del documento de metadatos canónico (RFC 8785) | |
uri | dónde reside el documento; manténgase corta, porque se almacena en la cadena | ipfs://<cid>, GS1 Digital Link |
5 · Leer un lote
const batch = await token.batchOf(id); // reverts ARexBatchNotFound if unknown
await token.batchExists(id); // true / false, never reverts
await token["totalSupply(uint256)"](id); // units in circulation (overloaded name)
await token.parentsOf(id); // [] for an original lot
await token.uri(id); // batch URI, or the base URI if empty
await token.isExpired(id); // reference-implementation helper
totalSupply está sobrecargado en ERC-1155 Supply (totalSupply() y totalSupply(uint256)), por lo que en ethers v6 debe llamarse por su firma. Batch.status es 0 (ACTIVE), 255 (RETIRED) o un valor definido por el emisor entre ambos.
6 · Transferencia
Consúltese siempre primero. transferCheck reproduce la canalización de transferencia como una vista y devuelve el primer código de rechazo; 0x00…00 significa permitido. canTransfer es la versión booleana ERC-7943 de la misma respuesta.
const code = await token.transferCheck(farmer.address, buyer.address, id, 10_000);
if (code !== ethers.ZeroHash) throw new Error(`transfer blocked: ${explain(code)}`);
await token.connect(farmer).safeTransferFrom(farmer.address, buyer.address, id, 10_000, "0x");
El orden de la canalización es fijo, de modo que el código indica qué regla ha fallado:
| Orden | Comprobación | Código devuelto por transferCheck | Reversión en la transacción |
|---|---|---|---|
| 1 | token en pausa | AREX:PAUSED | EnforcedPause() |
| 2 | remitente elegible (todos los hooks ELIGIBILITY) | AREX:SENDER_INELIGIBLE | ERC7943CannotSend(from) |
| 3 | destinatario elegible | AREX:RECEIVER_INELIGIBLE | ERC7943CannotReceive(to) |
| 4 | el lote existe | AREX:BATCH_NOT_FOUND | ARexBatchNotFound(id) |
| 5 | el lote no está retirado | AREX:BATCH_RETIRED | ARexBatchRetired(id) |
| 6 | amount <= balance − frozen | AREX:FROZEN | ERC7943InsufficientUnfrozenBalance(from, id, amount, unfrozen) |
| 7 | cada hook CHECK, en orden de registro | el código del hook | ARexHookRejected(hook, code) |
La vista y la transacción evalúan en el mismo orden, de modo que el código siempre nombra la comprobación que la transacción incumple primero.
Dos detalles importan en la práctica. AREX:FROZEN también se devuelve cuando el remitente simplemente no posee unidades suficientes, porque la comprobación se hace sobre el saldo no congelado. Y transferCheck pasa data vacío a los hooks; un hook que lee los datos de la transferencia (como la liquidación de DeliveryHook) puede responder de forma distinta para la transacción real.
7 · Linaje: dividir, fusionar, procesar
transform es la única forma de crear un lote a partir de lotes. Quema las entradas de from, acuña las salidas a to, registra parentsOf(output) = inputIds y emite un BatchTransformed, que se corresponde con un TransformationEvent de EPCIS. transform.staticCall devuelve los nuevos ids.
Una reagrupación conserva el producto básico, la unidad y la cantidad total, y no necesita ningún rol, solo la titularidad o la aprobación de ERC-1155. Para dividir un lote en dos para un comprador:
const [lotA, lotB] = await token.connect(farmer).transform.staticCall(
farmer.address, buyer.address, [id], [400], [init, init], [250, 150], ref("SALE-42"));
await token.connect(farmer).transform(
farmer.address, buyer.address, [id], [400], [init, init], [250, 150], ref("SALE-42"));
Para fusionar dos lotes en un único lote agrupado para una cooperativa:
const [pooled] = await token.connect(buyer).transform.staticCall(
buyer.address, coop.address, [lotA, lotB], [250, 150], [init], [400], ref("POOL-7"));
await token.connect(buyer).transform(
buyer.address, coop.address, [lotA, lotB], [250, 150], [init], [400], ref("POOL-7"));
El procesamiento cambia el producto básico, la unidad o la cantidad, por lo que necesita TRANSFORMER_ROLE además de la aprobación. El molino convierte 1,000 kg del trigo del agricultor en 750 kg de harina:
const now = (await ethers.provider.getBlock("latest")).timestamp;
const flourInit = { ...init, commodity: commodity("HS", "110100"), producedAt: now };
await token.connect(farmer).setApprovalForAll(mill.address, true);
const [flour] = await token.connect(mill).transform.staticCall(
farmer.address, mill.address, [id], [1_000], [flourInit], [750], ref("MILL-RUN-7"));
await token.connect(mill).transform(
farmer.address, mill.address, [id], [1_000], [flourInit], [750], ref("MILL-RUN-7"));
| Regla | Se garantiza mediante |
|---|---|
quien llama es from o está aprobado por from | transform |
| cada entrada existe, no está retirada y tiene saldo no congelado suficiente | _update |
| reagrupación: mismo producto básico y misma unidad, Σ entradas = Σ salidas | no se necesita rol |
| cualquier otro caso | TRANSFORMER_ROLE; de lo contrario, AccessControlUnauthorizedAccount |
| los hooks ven cada tramo | OP_TRANSFORM para las quemas y las acuñaciones |
El linaje construido en esta guía:
flowchart LR
B1["#1 trigo 250,000 kg<br/>agricultor"] -->|"división · SALE-42"| B2["#2 trigo 250"]
B1 -->|"división · SALE-42"| B3["#3 trigo 150"]
B2 -->|"fusión · POOL-7"| B4["#4 trigo 400<br/>cooperativa"]
B3 -->|"fusión · POOL-7"| B4
B1 -->|"procesamiento · MILL-RUN-7"| B5["#5 harina 750<br/>molino"]8 · Estado y metadatos
Solo STATUS_ROLE cambia el estado; poseer unidades no otorga nada. Los valores 1..254 los define el propio emisor; publíquese su significado en el documento de metadatos (status.table). La actualización de los metadatos sustituye el hash y la URI y emite BatchMetadataUpdated, de modo que el historial de documentos sigue siendo verificable.
await token.connect(warehouse).setBatchStatus(flour, 1, ref("IN_STORAGE")); // 1..254 are yours
const labReport = { ...metadata, lab: { moisture: 14.2 } };
await token.connect(warehouse).setBatchMetadata(
flour, ethers.keccak256(ethers.toUtf8Bytes(JSON.stringify(labReport))), "ipfs://<cid>/flour-lab.json");
9 · Hooks
Los hooks son contratos independientes a los que el token llama en orden de registro. Aquí se ejecuta primero el hook de elegibilidad, luego el de caducidad y después una regla según la cual el café necesita una declaración de diligencia debida del EUDR atestada antes de poder venderse.
const eligibility = await (await ethers.getContractFactory("EligibilityHook")).deploy(admin.address);
const expiry = await (await ethers.getContractFactory("ExpiryHook")).deploy(true); // allow pre-harvest
const attestationHook = await (await ethers.getContractFactory("AttestationHook"))
.deploy(admin.address, await attestations.getAddress());
const COFFEE = commodity("HS", "090111");
const EUDR_DDS = ref("AREX:EUDR:DDS");
await attestationHook.setRequirement(COFFEE, EUDR_DDS); // coffee needs a due-diligence statement
await token.registerHook(await eligibility.getAddress()); // evaluated first
await token.registerHook(await expiry.getAddress());
await token.registerHook(await attestationHook.getAddress()); // evaluated last
await token.getHooks(); // the ordered list
| Hook | Indicadores | Constructor | Se configura con |
|---|---|---|---|
EligibilityHook | CHECK, ELIGIBILITY | (admin) | setAllowlistEnabled, setAllowlisted, setBlocklisted, setSanctioned |
ExpiryHook | CHECK | (allowPreHarvestTransfers) | — (lee el registro del lote) |
AttestationHook | CHECK | (owner, attestationRegistry) | setRequirement(commodity, topic) |
PerishabilityHook | CHECK | (admin) | setCommodityDecay, setBatchDecay |
SeasonalHook | CHECK | (admin) | addWindow(commodity, op, start, end), clearWindows |
DeliveryHook | CHECK, EXECUTED | (admin) | schedule, pickUp, markInTransit, recordReading, confirmDelivered, dispute, resolve |
TraceabilityHook | EXECUTED | (admin, persist) | recordEvent (RECORDER_ROLE) |
El registro rechaza las EOA, los contratos sin soporte de ERC-165 para IARexHook y los hooks cuyos indicadores son 0. Colóquense primero los hooks baratos y que rechazan con frecuencia: un rechazo detiene la canalización. Para escribir un hook propio: docs/HOOK-DEVELOPMENT.md.
10 · Atestaciones
Una atestación es (batch, topic) → (issuer, reference, issuedAt, expiresAt, revoked), válida solo mientras su emisor sea de confianza para el tema. Con el requisito del paso 9 establecido, el lote de café no puede moverse hasta que la declaración esté atestada:
const coffee = await token.connect(issuer).createBatch.staticCall({ ...init, commodity: COFFEE }, 60, farmer.address, "0x");
await token.connect(issuer).createBatch({ ...init, commodity: COFFEE }, 60, farmer.address, "0x");
explain(await token.transferCheck(farmer.address, buyer.address, coffee, 1)); // "ATTESTATION_MISSING"
await attestations.setTrustedIssuer(EUDR_DDS, certifier.address, true); // ATTEST_ADMIN_ROLE
await attestations.connect(certifier).attest(coffee, EUDR_DDS, ref("DDS-2026-AR-000123"), 0);
await attestations.isAttested(coffee, EUDR_DDS); // true
explain(await token.transferCheck(farmer.address, buyer.address, coffee, 1)); // "OK"
isAttested es falso cuando no hay atestación, cuando está revocada, cuando ha pasado expiresAt o cuando el emisor ya no es de confianza para el tema. El emisor o ATTEST_ADMIN_ROLE pueden revocar. Los temas son valores bytes32 libres; úsese keccak256("<NAMESPACE>:<NAME>"), por ejemplo AREX:EUDR:DDS o AREX:CERT:ORGANIC_EU.
11 · Sanciones y elegibilidad
EligibilityHook responde a canSend y canReceive en nombre del token. Una sanción registra su fuente y una caducidad opcional, de modo que una actualización de la lista puede expirar automáticamente.
await eligibility.setSanctioned(buyer.address, true, ethers.encodeBytes32String("OFAC-SDN"), 0);
explain(await token.transferCheck(farmer.address, buyer.address, id, 1)); // "RECEIVER_INELIGIBLE"
await eligibility.setSanctioned(buyer.address, false, ethers.encodeBytes32String("OFAC-SDN"), 0);
12 · Ejecución forzosa
La congelación y las transferencias forzosas son la superficie de ejecución forzosa de ERC-7943, para gravámenes, órdenes judiciales e incautaciones. Una congelación puede superar el saldo actual y entonces cubre también los depósitos futuros. Una transferencia forzosa omite la elegibilidad y la comprobación de congelación, funciona mientras el token está en pausa, reduce la cantidad congelada a lo que queda y sigue ejecutando los hooks con OP_FORCED_TRANSFER.
await token.connect(enforcer).setFrozenTokens(buyer.address, id, 6_000); // may exceed the balance
await token.getFrozenTokens(buyer.address, id); // 6_000
explain(await token.transferCheck(buyer.address, farmer.address, id, 5_000)); // "FROZEN"
await token.pause(); // PAUSER_ROLE
await token.connect(enforcer).forcedTransfer(buyer.address, custodian.address, id, 6_000); // works while paused
await token.unpause();
Documéntese el procedimiento con arreglo al cual actúa ENFORCER_ROLE (orden judicial, contrato de prenda, notificación de sanciones) y manténgase el rol detrás de una multifirma. Cada acción emite Frozen o ForcedTransfer.
13 · Retiro
RETIRED (255) es terminal: no admite transferencias, acuñaciones ni transformaciones, y no hay vuelta atrás. La quema sigue siendo posible para que los bienes entregados o destruidos salgan del suministro.
await token.connect(warehouse).setBatchStatus(flour, 255, ref("DELIVERED")); // RETIRED is terminal
explain(await token.transferCheck(mill.address, buyer.address, flour, 1)); // "BATCH_RETIRED"
await token.connect(mill).burn(mill.address, flour, 750); // burning still works
14 · Indexación
El grafo de linaje es la lista de eventos BatchTransformed; no hace falta reconstruir nada a partir de transferencias individuales.
const transforms = await token.queryFilter(token.filters.BatchTransformed());
const edges = transforms.flatMap((e) =>
e.args.inputIds.flatMap((input) => e.args.outputIds.map((output) => [input, output, e.args.processRef])));
// edges: [1 → 2], [1 → 3], [2 → 4], [3 → 4], [1 → 5]
| Evento | Emitido por | Uso |
|---|---|---|
BatchCreated(id, issuer, commodity, unit, quantity, to) | createBatch | lote nuevo; ObjectEvent ADD de EPCIS |
BatchTransformed(operator, inputIds, inputAmounts, outputIds, outputAmounts, processRef) | transform | arista de linaje; TransformationEvent de EPCIS |
BatchStatusChanged(id, previousStatus, newStatus, reason) | setBatchStatus | ciclo de vida |
BatchMetadataUpdated(id, metadataHash, uri) | setBatchMetadata | historial de documentos |
TransferSingle, TransferBatch | cada cambio de saldo | posiciones; ObjectEvent OBSERVE de EPCIS |
Frozen(account, tokenId, amount), ForcedTransfer(from, to, tokenId, amount) | ejecución forzosa | gravámenes, incautaciones |
HookRegistered(hook, flags, position), HookRemoved(hook) | administración de hooks | cambios de reglas |
Attested, AttestationRevoked, TrustedIssuerUpdated | ARexAttestations | evidencia |
15 · Actualización
El almacenamiento tiene espacio de nombres ERC-7201 (erc7201:arex.storage.token); añádanse campos a TokenStorage, sin reordenarlos ni eliminarlos nunca. Valídese antes de cada actualización; el plugin rechaza las disposiciones que corromperían el almacenamiento.
await upgrades.validateUpgrade(await token.getAddress(), ARexToken, { kind: "uups" });
await upgrades.upgradeProxy(await token.getAddress(), ARexToken); // UPGRADER_ROLE
En producción, UPGRADER_ROLE está detrás de una multifirma y un timelock, y la actualización se ensaya antes contra un fork de la cadena de destino.
Probar la integración
npx hardhat test test/standard/ImplementationGuide.test.js # this chapter, executed
npx hardhat test test/standard/ARexToken.test.js # token behaviour and conformance
npx hardhat run scripts/smoke-standard.js # narrated walkthrough of one lot
npx hardhat run scripts/gas-report-standard.js # gas with 0, 1 and 2 hooks, and per hook
test/standard/helpers.js exporta deployStack(), batchInit(), las constantes OP, HOOK, STATUS y CODES y supply(), para las pruebas propias.
Paso a producción: lista de verificación
- Compilación reproducible (
npm ci), CI en verde, ids de interfaz iguales a los de la especificación. - Roles redistribuidos como en el paso 3; roles del desplegador renunciados.
- Hooks auditados, orden documentado,
getHooks()publicado. - Versión del esquema de metadatos y tabla de estados publicadas;
originRefy los datos restringidos libres de datos personales. - Instrumentos jurídicos referenciados en los metadatos; clasificación de la oferta acordada con los asesores jurídicos.
- Auditoría de terceros del token, del registro de atestaciones y de cada hook registrado (paquete:
docs/AUDIT-READINESS.md). - Actualización ensayada en un fork con
validateUpgrade.