feat: add universal resource activation policy profile flow

This commit is contained in:
defiQUG
2026-04-24 22:06:26 -07:00
parent 0035a787fe
commit 566cecd8f9
71 changed files with 2705 additions and 153 deletions

View File

@@ -204,7 +204,7 @@ CT 2301 (besu-rpc-private-1) may fail to start with `lxc.hook.pre-start` due to
- **Daily/weekly checks:** `./scripts/maintenance/daily-weekly-checks.sh [daily|weekly|all]` — explorer sync (135), RPC health (136), config API (137). **Cron:** `./scripts/maintenance/schedule-daily-weekly-cron.sh [--install|--show]` (daily 08:00, weekly Sun 09:00). See [OPERATIONAL_RUNBOOKS.md](../docs/03-deployment/OPERATIONAL_RUNBOOKS.md) § Maintenance.
- **Start firefly-ali-1 (6201):** `./scripts/maintenance/start-firefly-6201.sh [--dry-run] [--host HOST]` — start CT 6201 on r630-02 when needed (optional ongoing).
- **Config validation (pre-deploy):** `./scripts/validation/validate-config-files.sh` — set `VALIDATE_REQUIRED_FILES` for required paths. **CI / all validation:** `./scripts/verify/run-all-validation.sh [--skip-genesis] [--json-out reports/status/run-all-validation-latest.json]` — dependencies, config files, **cW\* mesh matrix** (merge of `cross-chain-pmm-lps/config/deployment-status.json` and `reports/extraction/promod-uniswap-v2-live-pair-discovery-latest.json` when that file exists; no RPC), optional genesis (no LAN/SSH). **Matrix only:** `./scripts/verify/build-cw-mesh-deployment-matrix.sh` — stdout markdown; `--json-out reports/status/cw-mesh-deployment-matrix-latest.json` for machine-readable rows. **URA (universal resource activation) smoke:** `./scripts/verify/smoke-universal-resource-activation.sh` (JSON Schema only) or the same with `--http` and optional `PHOENIX_BASE_URL` to assert Phoenix `GET /api/v1/universal-resource-activation/manifest`; see `docs/04-configuration/universal-resource-activation/UNIVERSAL_RESOURCE_WIRING.md` §2.1.
- **Config validation (pre-deploy):** `./scripts/validation/validate-config-files.sh` — set `VALIDATE_REQUIRED_FILES` for required paths. **CI / all validation:** `./scripts/verify/run-all-validation.sh [--skip-genesis] [--json-out reports/status/run-all-validation-latest.json]` — dependencies, config files, **cW\* mesh matrix** (merge of `cross-chain-pmm-lps/config/deployment-status.json` and `reports/extraction/promod-uniswap-v2-live-pair-discovery-latest.json` when that file exists; no RPC), optional genesis (no LAN/SSH). **Matrix only:** `./scripts/verify/build-cw-mesh-deployment-matrix.sh` — stdout markdown; `--json-out reports/status/cw-mesh-deployment-matrix-latest.json` for machine-readable rows. **URA (universal resource activation):** **`pnpm ura:validate`**, **`pnpm ura:validate-profiles`**, **`pnpm ura:merge-manifest`**, **`pnpm ura:validate-ledger-mapping`**, **`pnpm ura:writer:ledger`**, **`pnpm ura:writer:settlement`**, **`pnpm ura:profile-hash`**, **`pnpm ura:validate-closure`** / **`pnpm ura:validate-closure:strict`**, **`pnpm ura:keccak`**, **`pnpm ura:smoke`**. Optional **`URA_STRICT_CLOSURE=1`**. Tracker: `docs/04-configuration/universal-resource-activation/URA_MANIFEST_AUTOMATION_IMPLEMENTATION_TRACKER.md`. See `UNIVERSAL_RESOURCE_WIRING.md` §2.1 and §5; multi-jurisdiction: `docs/04-configuration/compliance-matrices/README.md`.
- **Wrapper summaries:** `./scripts/run-completable-tasks-from-anywhere.sh --json-out reports/status/run-completable-tasks-latest.json`, `./scripts/run-e2e-flow-tasks-full-parallel.sh --json-out reports/status/run-e2e-flow-tasks-latest.json`, `./scripts/deployment/run-all-next-steps-chain138.sh --json-out reports/status/run-all-next-steps-chain138-latest.json`, and `./scripts/run-all-operator-tasks-from-lan.sh --json-out reports/status/run-all-operator-tasks-latest.json` produce machine-readable step summaries that match the terminal progress output.
### 13. Phase 2, 3 & 4 Deployment Scripts

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env node
/**
* Emit keccak256(utf8(resourceId)) for each row in the URA manifest.
* Use when anchoring resourceId in on-chain / GRU registries (operator step; not automatic mirroring).
*
* Usage: from repo root — node scripts/ura/keccak-resource-ids.mjs
* Requires: root devDependency `ethers` (v6).
*/
import { readFileSync, existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { keccak256, toUtf8Bytes } from 'ethers';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, '../..');
const manifestPath = path.join(projectRoot, 'config', 'universal-resource-activation', 'manifest.json');
if (!existsSync(manifestPath)) {
console.error(`[keccak-ura] Missing ${manifestPath}`);
process.exit(1);
}
const m = JSON.parse(readFileSync(manifestPath, 'utf8'));
const resources = m.resources || [];
console.log(
'# keccak256(utf8(resourceId)) for universal-resource-activation resources\n# Use for optional on-chain / GRU registry rows; keep manifest as canonical off-chain store.\n',
);
for (const r of resources) {
const id = r.resourceId;
if (typeof id !== 'string' || !id) continue;
const h = keccak256(toUtf8Bytes(id));
console.log(id);
console.log(` ${h}`);
console.log('');
}
console.log(`[keccak-ura] ${resources.length} resource(s)`);

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env node
/**
* Shared URA manifest validation (schemas + cross-checks).
* Used by validate-universal-resource-activation.mjs and merge-manifest-fragments.mjs.
*/
import { readFileSync, existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export function getProjectRoot() {
return path.resolve(path.join(__dirname, '..', '..', '..'));
}
export function getDefaultManifestPath(projectRoot = getProjectRoot()) {
return path.join(projectRoot, 'config', 'universal-resource-activation', 'manifest.json');
}
/**
* @returns {{ validateManifest: import('ajv').ValidateFunction, validateResource: import('ajv').ValidateFunction, validateEvidence: import('ajv').ValidateFunction, ajv: import('ajv').default }}
*/
export function loadUraManifestValidators(projectRoot = getProjectRoot()) {
const manifestSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.manifest.v1.schema.json');
const resourceSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.resource.v1.schema.json');
const evidenceSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.evidence-package.v1.schema.json');
const ajv = new Ajv({
allErrors: true,
strict: false,
validateSchema: false,
});
addFormats(ajv);
const manifestSchema = JSON.parse(readFileSync(manifestSchemaPath, 'utf8'));
const resourceSchema = JSON.parse(readFileSync(resourceSchemaPath, 'utf8'));
const evidenceSchema = JSON.parse(readFileSync(evidenceSchemaPath, 'utf8'));
return {
validateManifest: ajv.compile(manifestSchema),
validateResource: ajv.compile(resourceSchema),
validateEvidence: ajv.compile(evidenceSchema),
ajv,
};
}
/**
* @param {unknown} data
* @param {{ validateManifest: import('ajv').ValidateFunction, validateResource: import('ajv').ValidateFunction, validateEvidence: import('ajv').ValidateFunction }} validators
* @returns {string[]} empty if valid
*/
export function validateUraManifestData(data, validators) {
const errors = [];
const { validateManifest, validateResource, validateEvidence, ajv } = validators;
if (!validateManifest(data)) {
return [`Manifest failed manifest schema: ${ajv.errorsText(validateManifest.errors, { separator: '\n' })}`];
}
if (!Array.isArray(data.resources)) {
return ['resources must be an array'];
}
if (!Array.isArray(data.evidencePackages)) {
return ['evidencePackages must be an array'];
}
data.resources.forEach((r, i) => {
if (!validateResource(r)) {
errors.push(
`resources[${i}] (resourceId=${r?.resourceId}): ${ajv.errorsText(validateResource.errors, { separator: '\n' })}`
);
}
});
data.evidencePackages.forEach((p, i) => {
if (!validateEvidence(p)) {
errors.push(
`evidencePackages[${i}] (id=${p?.evidencePackageId}): ${ajv.errorsText(validateEvidence.errors, { separator: '\n' })}`
);
}
});
if (errors.length) return errors;
const ids = new Set(data.resources.map((r) => r.resourceId).filter(Boolean));
data.evidencePackages.forEach((p, pi) => {
(p.resourceIds || []).forEach((rid) => {
if (!ids.has(rid)) {
errors.push(`evidencePackages[${pi}] references unknown resourceId: ${rid}`);
}
});
});
return errors;
}
/**
* CLI-style: read file, validate, log, exit 0 or 1.
* @param {string} [manifestPath]
*/
export function validateUraManifestFileCli(manifestPath) {
const projectRoot = getProjectRoot();
const pathToUse = manifestPath || getDefaultManifestPath(projectRoot);
function fail(msg) {
console.error(`[validate-ura] ${msg}`);
process.exit(1);
}
if (!existsSync(pathToUse)) {
fail(`Missing ${pathToUse}`);
}
let data;
try {
data = JSON.parse(readFileSync(pathToUse, 'utf8'));
} catch (e) {
fail(`Invalid JSON: ${e.message}`);
}
const validators = loadUraManifestValidators(projectRoot);
const errs = validateUraManifestData(data, validators);
if (errs.length) {
errs.forEach((e) => console.error(`[validate-ura] ${e}`));
process.exit(1);
}
console.log(
`[validate-ura] OK: ${data.resources.length} resource(s), ${data.evidencePackages.length} evidence package(s)`
);
process.exit(0);
}

View File

@@ -0,0 +1,36 @@
# URA manifest writer (ledger + settlement fragments)
**Purpose:** Build **partial manifest fragments** from machine-readable inputs so ops or a batch job can merge them (`pnpm ura:merge-manifest` or manual paste) after validation.
## Ledger → `accountingRef`
1. Define mapping: [`omnl-ledger-mapping.v1.example.json`](../../../config/universal-resource-activation/integration/omnl-ledger-mapping.v1.example.json) → copy to `omnl-ledger-mapping.v1.json` (gitignored or secured).
2. Export ledger snapshot JSON (Fineract/OMNL/sidecar ETL).
3. Run:
```bash
pnpm ura:writer:ledger -- \
--mapping config/universal-resource-activation/integration/omnl-ledger-mapping.v1.example.json \
--ledger config/universal-resource-activation/integration/examples/ledger-snapshot.example.json
```
4. Pipe output into a file under `manifest-fragments/` or merge with `merge-manifest-fragments.mjs --out`.
Validate mapping: `pnpm ura:validate-ledger-mapping -- config/.../omnl-ledger-mapping.v1.example.json`
## Rail / chain → `settlementOrChainRef`
```bash
pnpm ura:writer:settlement -- \
--evidence-package-id ura:pilot:evidence-register-bootstrap \
--message-id 0xabc \
--tx-hash 0xdef \
--chain-id 138
```
Emits a fragment with a single `evidencePackages[]` row (shallow merge by id).
## Related
- [`URA_MANIFEST_WRITER_OPS.md`](../../../docs/03-deployment/URA_MANIFEST_WRITER_OPS.md)
- [`URA_MANIFEST_AUTOMATION_IMPLEMENTATION_TRACKER.md`](../../../docs/04-configuration/universal-resource-activation/URA_MANIFEST_AUTOMATION_IMPLEMENTATION_TRACKER.md)

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env node
/**
* Build a manifest fragment from a ledger snapshot JSON + omnl-ledger-mapping.v1.json
* See scripts/ura/manifest-writer/README.md
*/
import { readFileSync, existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { getByPath, asString } from './lib/get-by-path.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, '..', '..', '..');
function parseArgs() {
const a = process.argv.slice(2);
const o = {};
for (let i = 0; i < a.length; i++) {
if (a[i] === '--mapping') o.mapping = a[++i];
else if (a[i] === '--ledger') o.ledger = a[++i];
}
return o;
}
const args = parseArgs();
const mappingPath = path.resolve(projectRoot, args.mapping || 'config/universal-resource-activation/integration/omnl-ledger-mapping.v1.example.json');
const ledgerPath = path.resolve(
projectRoot,
args.ledger || 'config/universal-resource-activation/integration/examples/ledger-snapshot.example.json'
);
for (const p of [mappingPath, ledgerPath]) {
if (!existsSync(p)) {
console.error(`[writer-ledger] Missing file: ${p}`);
process.exit(1);
}
}
let mapping;
let ledger;
try {
mapping = JSON.parse(readFileSync(mappingPath, 'utf8'));
ledger = JSON.parse(readFileSync(ledgerPath, 'utf8'));
} catch (e) {
console.error(`[writer-ledger] JSON error: ${e.message}`);
process.exit(1);
}
const evidencePackages = [];
for (const row of mapping.evidencePackages || []) {
const pkg = { evidencePackageId: row.evidencePackageId };
if (row.accountingRefField) {
const v = getByPath(ledger, row.accountingRefField);
pkg.accountingRef = asString(v);
}
evidencePackages.push(pkg);
}
const resources = [];
for (const ru of mapping.resourceUpdates || []) {
const v = getByPath(ledger, ru.quantityField);
resources.push({
resourceId: ru.resourceId,
quantity: asString(v),
});
}
const fragment = {};
if (evidencePackages.length) fragment.evidencePackages = evidencePackages;
if (resources.length) fragment.resources = resources;
process.stdout.write(`${JSON.stringify(fragment, null, 2)}\n`);

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env node
/**
* Build a manifest fragment with settlementOrChainRef on an evidence package.
*/
function parseArgs() {
const a = process.argv.slice(2);
const o = {};
for (let i = 0; i < a.length; i++) {
if (a[i] === '--evidence-package-id') o.evidencePackageId = a[++i];
else if (a[i] === '--message-id') o.messageId = a[++i];
else if (a[i] === '--tx-hash') o.txHash = a[++i];
else if (a[i] === '--chain-id') o.chainId = a[++i];
}
return o;
}
const args = parseArgs();
if (!args.evidencePackageId) {
console.error('[writer-settlement] Required: --evidence-package-id');
process.exit(1);
}
const parts = [];
if (args.messageId) parts.push(`messageId=${args.messageId}`);
if (args.txHash) parts.push(`tx=${args.txHash}`);
if (args.chainId) parts.push(`chain=${args.chainId}`);
const settlementOrChainRef = parts.length ? parts.join(';') : '';
const fragment = {
evidencePackages: [
{
evidencePackageId: args.evidencePackageId,
settlementOrChainRef,
},
],
};
process.stdout.write(`${JSON.stringify(fragment, null, 2)}\n`);

View File

@@ -0,0 +1,25 @@
/**
* @param {Record<string, unknown>} obj
* @param {string} dotPath e.g. "a.b.c"
* @returns {unknown}
*/
export function getByPath(obj, dotPath) {
if (!dotPath || typeof dotPath !== 'string') return undefined;
const parts = dotPath.split('.').filter(Boolean);
let cur = obj;
for (const p of parts) {
if (cur == null || typeof cur !== 'object') return undefined;
cur = /** @type {Record<string, unknown>} */ (cur)[p];
}
return cur;
}
/**
* @param {unknown} v
* @returns {string}
*/
export function asString(v) {
if (v == null) return '';
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return String(v);
return JSON.stringify(v);
}

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env node
/**
* Merge optional JSON fragments into the URA manifest and validate (dry-run or --out).
*
* Fragments: partial objects with optional keys resources[], evidencePackages[], policyProfileRefs[].
* Later files in sort order override same resourceId / evidencePackageId.
*
* Usage (repo root):
* node scripts/ura/merge-manifest-fragments.mjs
* node scripts/ura/merge-manifest-fragments.mjs --out /tmp/merged-manifest.json
* node scripts/ura/merge-manifest-fragments.mjs --base config/universal-resource-activation/manifest.json --fragments-dir config/universal-resource-activation/manifest-fragments
*/
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
import path from 'path';
import {
getProjectRoot,
getDefaultManifestPath,
loadUraManifestValidators,
validateUraManifestData,
} from './lib/validate-ura-manifest.mjs';
function parseArgs() {
const argv = process.argv.slice(2);
const out = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--out') out.out = argv[++i];
else if (a === '--base') out.base = argv[++i];
else if (a === '--fragments-dir') out.fragmentsDir = argv[++i];
}
return out;
}
function deepClone(o) {
return JSON.parse(JSON.stringify(o));
}
function profileKey(ref) {
if (!ref || typeof ref.id !== 'string') return null;
const v = ref.version != null ? String(ref.version) : '';
return `${ref.id}@${v}`;
}
function mergeFragment(base, frag, fragmentLabel) {
const issues = [];
if (frag.policyProfileRefs && Array.isArray(frag.policyProfileRefs)) {
const map = new Map();
(base.policyProfileRefs || []).forEach((r) => {
const k = profileKey(r);
if (k) map.set(k, r);
});
frag.policyProfileRefs.forEach((r) => {
const k = profileKey(r);
if (k) map.set(k, r);
});
base.policyProfileRefs = [...map.values()].sort((a, b) => {
const ka = profileKey(a);
const kb = profileKey(b);
return ka.localeCompare(kb);
});
}
if (frag.resources && Array.isArray(frag.resources)) {
const byId = new Map((base.resources || []).map((r) => [r.resourceId, r]));
frag.resources.forEach((r) => {
if (!r || typeof r.resourceId !== 'string') {
issues.push(`${fragmentLabel}: skip resource without resourceId`);
return;
}
issues.push(
`${fragmentLabel}: ${byId.has(r.resourceId) ? 'replace' : 'add'} resource ${r.resourceId}`
);
byId.set(r.resourceId, { ...(byId.get(r.resourceId) || {}), ...r });
});
base.resources = [...byId.values()].sort((a, b) => String(a.resourceId).localeCompare(String(b.resourceId)));
}
if (frag.evidencePackages && Array.isArray(frag.evidencePackages)) {
const byId = new Map((base.evidencePackages || []).map((p) => [p.evidencePackageId, p]));
frag.evidencePackages.forEach((p) => {
if (!p || typeof p.evidencePackageId !== 'string') {
issues.push(`${fragmentLabel}: skip evidence package without evidencePackageId`);
return;
}
issues.push(
`${fragmentLabel}: ${byId.has(p.evidencePackageId) ? 'replace' : 'add'} evidencePackage ${p.evidencePackageId}`
);
const prev = byId.get(p.evidencePackageId) || {};
byId.set(p.evidencePackageId, { ...prev, ...p });
});
base.evidencePackages = [...byId.values()].sort((a, b) =>
String(a.evidencePackageId).localeCompare(String(b.evidencePackageId))
);
}
return issues;
}
function main() {
const args = parseArgs();
const projectRoot = getProjectRoot();
const basePath = path.resolve(projectRoot, args.base || getDefaultManifestPath(projectRoot));
const fragmentsDir = path.resolve(
projectRoot,
args.fragmentsDir || path.join('config', 'universal-resource-activation', 'manifest-fragments')
);
if (!existsSync(basePath)) {
console.error(`[merge-ura-manifest] Missing base: ${basePath}`);
process.exit(1);
}
let merged;
try {
merged = JSON.parse(readFileSync(basePath, 'utf8'));
} catch (e) {
console.error(`[merge-ura-manifest] Invalid base JSON: ${e.message}`);
process.exit(1);
}
const mergeNotes = [];
if (existsSync(fragmentsDir)) {
const files = readdirSync(fragmentsDir)
.filter((f) => f.endsWith('.json') && !f.startsWith('_'))
.sort();
for (const f of files) {
const fp = path.join(fragmentsDir, f);
let frag;
try {
frag = JSON.parse(readFileSync(fp, 'utf8'));
} catch (e) {
console.error(`[merge-ura-manifest] Invalid JSON in ${fp}: ${e.message}`);
process.exit(1);
}
mergeNotes.push(...mergeFragment(merged, frag, f));
}
}
const validators = loadUraManifestValidators(projectRoot);
const errs = validateUraManifestData(merged, validators);
if (errs.length) {
errs.forEach((e) => console.error(`[merge-ura-manifest] ${e}`));
process.exit(1);
}
mergeNotes.forEach((n) => console.log(`[merge-ura-manifest] ${n}`));
console.log(
`[merge-ura-manifest] OK: ${merged.resources.length} resource(s), ${merged.evidencePackages.length} evidence package(s)`
);
if (args.out) {
writeFileSync(path.resolve(args.out), `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
console.log(`[merge-ura-manifest] Wrote ${path.resolve(args.out)}`);
} else {
console.log('[merge-ura-manifest] Dry-run (pass --out <file> to write merged JSON)');
}
process.exit(0);
}
main();

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env node
/**
* Compute a content hash for one policy profile row (for PolicyProfileRegistry.publishProfile on-chain).
* Uses keccak256(utf8(canonicalJson)) where canonicalJson is stable key-sorted JSON of the profile object.
*
* Usage: node scripts/ura/policy-profiles-content-hash.mjs <policyProfileId>
*/
import { readFileSync, existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { keccak256, toUtf8Bytes } from 'ethers';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, '..', '..');
const registryPath = path.join(projectRoot, 'config/universal-resource-activation/policy-profiles.json');
const id = process.argv[2];
if (!id) {
console.error('Usage: node scripts/ura/policy-profiles-content-hash.mjs <policyProfileId>');
process.exit(1);
}
if (!existsSync(registryPath)) {
console.error(`Missing ${registryPath}`);
process.exit(1);
}
const data = JSON.parse(readFileSync(registryPath, 'utf8'));
const profile = (data.profiles || []).find((p) => p.policyProfileId === id);
if (!profile) {
console.error(`Unknown policyProfileId: ${id}`);
process.exit(1);
}
function sortKeys(obj) {
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return obj;
const out = {};
for (const k of Object.keys(obj).sort()) {
out[k] = sortKeys(obj[k]);
}
return out;
}
const canonical = JSON.stringify(sortKeys(profile));
const hash = keccak256(toUtf8Bytes(canonical));
console.log(JSON.stringify({ policyProfileId: id, contentHash: hash, canonicalBytes: canonical.length }, null, 2));

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env node
/**
* Optional production gate: fail if manifest still contains pilot placeholders or open reconciliation.
*
* Usage (repo root):
* node scripts/ura/validate-manifest-closure.mjs # warnings only, exit 0
* node scripts/ura/validate-manifest-closure.mjs --strict # exit 1 on violations
*
* CI: set URA_STRICT_CLOSURE=1 and run validate-config-files.sh (see script header there).
*/
import { readFileSync, existsSync } from 'fs';
import { getProjectRoot, getDefaultManifestPath } from './lib/validate-ura-manifest.mjs';
const PILOT_PARTICIPANT = /ura:participant:pilot-/i;
const PENDING_EVIDENCE = /^ura:evidence:pending-/i;
const TBD_RE = /\bTBD\b/i;
const strict = process.argv.includes('--strict');
const projectRoot = getProjectRoot();
const manifestPath = getDefaultManifestPath(projectRoot);
function main() {
const log = (m) => console.log(`[validate-ura-closure] ${m}`);
const warn = (m) => console.warn(`[validate-ura-closure] WARN: ${m}`);
const err = (m) => console.error(`[validate-ura-closure] ${strict ? 'FAIL' : 'WARN'}: ${m}`);
if (!existsSync(manifestPath)) {
console.error(`[validate-ura-closure] Missing ${manifestPath}`);
process.exit(1);
}
let data;
try {
data = JSON.parse(readFileSync(manifestPath, 'utf8'));
} catch (e) {
console.error(`[validate-ura-closure] Invalid JSON: ${e.message}`);
process.exit(1);
}
const violations = [];
(data.resources || []).forEach((r, i) => {
const pid = r.ownerParticipantId;
if (typeof pid === 'string' && PILOT_PARTICIPANT.test(pid)) {
violations.push(`resources[${i}] ownerParticipantId is pilot placeholder: ${pid}`);
}
(r.evidenceRefs || []).forEach((ref, j) => {
if (typeof ref === 'string' && PENDING_EVIDENCE.test(ref)) {
violations.push(`resources[${i}] evidenceRefs[${j}] pending: ${ref}`);
}
});
});
(data.evidencePackages || []).forEach((p, i) => {
if (p.reconciliationStatus === 'open') {
violations.push(`evidencePackages[${i}] (${p.evidencePackageId}) reconciliationStatus is open`);
}
const checkFields = ['custodyOrSourceEvidence', 'accountingRef', 'settlementOrChainRef', 'deploymentRef', 'explanation'];
for (const f of checkFields) {
const v = p[f];
if (typeof v === 'string' && TBD_RE.test(v)) {
violations.push(`evidencePackages[${i}] (${p.evidencePackageId}).${f} contains TBD`);
}
}
});
if (violations.length === 0) {
log('OK: no pilot/TBD/open-reconciliation patterns detected');
process.exit(0);
}
violations.forEach((v) => (strict ? err : warn)(v));
if (strict) {
err(`${violations.length} violation(s) — close pilots per URA_PILOT_CLOSURE_RUNBOOK.md`);
process.exit(1);
}
log(`${violations.length} notice(s) (use --strict to fail CI)`);
process.exit(0);
}
main();

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env node
/**
* Validate omnl-ledger-mapping.v1.json against omnl-ledger-mapping.v1.schema.json
* Usage: node scripts/validate/validate-omnl-ledger-mapping.mjs [path]
*/
import { readFileSync, existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, '../..');
const defaultPath = path.join(
projectRoot,
'config/universal-resource-activation/integration/omnl-ledger-mapping.v1.example.json'
);
const schemaPath = path.join(
projectRoot,
'config/universal-resource-activation/integration/omnl-ledger-mapping.v1.schema.json'
);
const file = path.resolve(projectRoot, process.argv[2] || defaultPath);
if (!existsSync(file)) {
console.error(`[validate-ledger-mapping] Missing ${file}`);
process.exit(1);
}
if (!existsSync(schemaPath)) {
console.error(`[validate-ledger-mapping] Missing schema ${schemaPath}`);
process.exit(1);
}
const ajv = new Ajv({ allErrors: true, strict: false, validateSchema: false });
addFormats(ajv);
const validate = ajv.compile(JSON.parse(readFileSync(schemaPath, 'utf8')));
let data;
try {
data = JSON.parse(readFileSync(file, 'utf8'));
} catch (e) {
console.error(`[validate-ledger-mapping] Invalid JSON: ${e.message}`);
process.exit(1);
}
if (!validate(data)) {
console.error(`[validate-ledger-mapping] FAIL: ${ajv.errorsText(validate.errors, { separator: '\n' })}`);
process.exit(1);
}
console.log(`[validate-ledger-mapping] OK: ${file}`);
process.exit(0);

View File

@@ -1,100 +1,10 @@
#!/usr/bin/env node
/**
* Validate config/universal-resource-activation/manifest.json against
* - universal-resource-activation.manifest.v1.schema.json
* - universal-resource-activation.resource.v1.schema.json (per item in resources[])
* - universal-resource-activation.evidence-package.v1.schema.json (per item in evidencePackages[])
* Validate config/universal-resource-activation/manifest.json against URA JSON Schemas.
*
* Usage: from repo root: node scripts/validate/validate-universal-resource-activation.mjs
* Exit 0 on success, 1 on error.
* Usage: from repo root: node scripts/validate/validate-universal-resource-activation.mjs [path/to/manifest.json]
*/
import { readFileSync, existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { validateUraManifestFileCli } from '../ura/lib/validate-ura-manifest.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, '../..');
const configDir = path.join(projectRoot, 'config', 'universal-resource-activation');
const manifestPath = path.join(configDir, 'manifest.json');
const manifestSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.manifest.v1.schema.json');
const resourceSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.resource.v1.schema.json');
const evidenceSchemaPath = path.join(projectRoot, 'config', 'universal-resource-activation.evidence-package.v1.schema.json');
function fail(msg) {
console.error(`[validate-ura] ${msg}`);
process.exit(1);
}
if (!existsSync(manifestPath)) {
fail(`Missing ${manifestPath}`);
}
const ajv = new Ajv({
allErrors: true,
strict: false,
validateSchema: false,
});
addFormats(ajv);
const manifestSchema = JSON.parse(readFileSync(manifestSchemaPath, 'utf8'));
const resourceSchema = JSON.parse(readFileSync(resourceSchemaPath, 'utf8'));
const evidenceSchema = JSON.parse(readFileSync(evidenceSchemaPath, 'utf8'));
const validateManifest = ajv.compile(manifestSchema);
const validateResource = ajv.compile(resourceSchema);
const validateEvidence = ajv.compile(evidenceSchema);
const raw = readFileSync(manifestPath, 'utf8');
let data;
try {
data = JSON.parse(raw);
} catch (e) {
fail(`Invalid JSON: ${e.message}`);
}
if (!validateManifest(data)) {
fail(`Manifest failed manifest schema: ${ajv.errorsText(validateManifest.errors, { separator: '\n' })}`);
}
if (!Array.isArray(data.resources)) {
fail('resources must be an array');
}
if (!Array.isArray(data.evidencePackages)) {
fail('evidencePackages must be an array');
}
data.resources.forEach((r, i) => {
if (!validateResource(r)) {
fail(
`resources[${i}] (resourceId=${r?.resourceId}): ${ajv.errorsText(validateResource.errors, { separator: '\n' })}`
);
}
});
data.evidencePackages.forEach((p, i) => {
if (!validateEvidence(p)) {
fail(
`evidencePackages[${i}] (id=${p?.evidencePackageId}): ${ajv.errorsText(validateEvidence.errors, { separator: '\n' })}`
);
}
});
// Cross-check: all resourceIds referenced in evidence exist
const ids = new Set(data.resources.map((r) => r.resourceId).filter(Boolean));
data.evidencePackages.forEach((p, pi) => {
(p.resourceIds || []).forEach((rid) => {
if (!ids.has(rid)) {
fail(
`evidencePackages[${pi}] references unknown resourceId: ${rid}`
);
}
});
});
console.log(
`[validate-ura] OK: ${data.resources.length} resource(s), ${data.evidencePackages.length} evidence package(s)`
);
process.exit(0);
const arg = process.argv[2];
validateUraManifestFileCli(arg || undefined);

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env node
/**
* Validate config/universal-resource-activation/policy-profiles.json against
* universal-resource-activation.policy-profile-registry.v1.schema.json
* and ensure manifest policyProfileRefs[] ids exist in the registry.
*
* Usage: from repo root — node scripts/validate/validate-ura-policy-profiles.mjs
*/
import { readFileSync, existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, '../..');
const registryPath = path.join(projectRoot, 'config', 'universal-resource-activation', 'policy-profiles.json');
const registrySchemaPath = path.join(
projectRoot,
'config',
'universal-resource-activation.policy-profile-registry.v1.schema.json',
);
const manifestPath = path.join(projectRoot, 'config', 'universal-resource-activation', 'manifest.json');
function fail(msg) {
console.error(`[validate-ura-profiles] ${msg}`);
process.exit(1);
}
if (!existsSync(registryPath)) fail(`Missing ${registryPath}`);
if (!existsSync(registrySchemaPath)) fail(`Missing ${registrySchemaPath}`);
const ajv = new Ajv({ allErrors: true, strict: false, validateSchema: false });
addFormats(ajv);
const registrySchema = JSON.parse(readFileSync(registrySchemaPath, 'utf8'));
const validateRegistry = ajv.compile(registrySchema);
const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
if (!validateRegistry(registry)) {
console.error('[validate-ura-profiles] policy-profiles.json failed schema:', validateRegistry.errors);
process.exit(1);
}
const ids = new Set(registry.profiles.map((p) => p.policyProfileId));
console.log(`[validate-ura-profiles] OK: ${ids.size} profile(s) in registry`);
if (existsSync(manifestPath)) {
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
const refs = manifest.policyProfileRefs || [];
for (const r of refs) {
const pid = r.id;
if (!pid || !ids.has(pid)) {
fail(`manifest policyProfileRefs contains unknown or missing id: "${pid}"`);
}
}
for (const res of manifest.resources || []) {
const pid = res.policyProfileId;
if (pid && !ids.has(pid)) {
fail(`resource ${res.resourceId} references unknown policyProfileId: "${pid}"`);
}
}
console.log('[validate-ura-profiles] OK: manifest refs match registry');
}

View File

@@ -188,6 +188,15 @@ else
log_warn "Optional config/universal-resource-activation/policy-profiles.json missing; skipping"
fi
# Optional production gate: URA_STRICT_CLOSURE=1 fails if pilots/TBD/open reconciliation remain
if [[ -f "$PROJECT_ROOT/config/universal-resource-activation/integration/omnl-ledger-mapping.v1.json" ]] && command -v node &>/dev/null && [[ -f "$PROJECT_ROOT/scripts/validate/validate-omnl-ledger-mapping.mjs" ]]; then
log_ok "Found: config/universal-resource-activation/integration/omnl-ledger-mapping.v1.json"
if node "$PROJECT_ROOT/scripts/validate/validate-omnl-ledger-mapping.mjs" "$PROJECT_ROOT/config/universal-resource-activation/integration/omnl-ledger-mapping.v1.json"; then
log_ok "omnl-ledger-mapping.v1.json: JSON Schema OK"
else
log_err "omnl-ledger-mapping.v1.json: validation failed"
ERRORS=$((ERRORS + 1))
fi
fi
if [[ "${URA_STRICT_CLOSURE:-}" == "1" ]] && [[ -f "$PROJECT_ROOT/scripts/ura/validate-manifest-closure.mjs" ]]; then
log_info "URA_STRICT_CLOSURE=1: running URA manifest closure gate…"
if node "$PROJECT_ROOT/scripts/ura/validate-manifest-closure.mjs" --strict; then

View File

@@ -1,6 +1,11 @@
#!/usr/bin/env bash
# Universal resource activation — local smoke: JSON Schema validation (always) +
# optional HTTP GET to Phoenix (when --http or PHOENIX_BASE_URL is set).
# optional HTTP GETs to Phoenix (when --http or PHOENIX_BASE_URL is set):
# 1) GET /api/v1/universal-resource-activation/manifest (expect 200 + .schemaVersion)
# 2) GET /api/v1/universal-resource-activation/policy-profiles (expect 200 + .profiles array)
# 3) GET /api/v1/universal-resource-activation/server-funds-sidecar-probe
# — expect 200 (sidecar ok or probe JSON) or 503 with .configured == false (URL unset)
# — 502 = fail (URL set but sidecar paths not healthy)
#
# Usage (repo root):
# bash scripts/verify/smoke-universal-resource-activation.sh
@@ -61,7 +66,9 @@ fi
log "GET $URL (expect 200, JSON with .schemaVersion)…"
body_file="$(mktemp)"
trap 'rm -f "$body_file"' EXIT
probe_file="$(mktemp)"
profiles_file="$(mktemp)"
trap 'rm -f "$body_file" "$probe_file" "$profiles_file"' EXIT
code=$(curl -sS -o "$body_file" -w '%{http_code}' --connect-timeout 5 --max-time 15 "$URL" || true)
if [[ "$code" != "200" ]]; then
@@ -78,4 +85,42 @@ if ! jq -e '.schemaVersion | type == "string"' "$body_file" &>/dev/null; then
exit 1
fi
log "HTTP OK (.schemaVersion present; HTTP $code)"
PROFILES_URL="${BASE}/api/v1/universal-resource-activation/policy-profiles"
log "GET $PROFILES_URL (expect 200, JSON with .profiles array)…"
prcode=$(curl -sS -o "$profiles_file" -w '%{http_code}' --connect-timeout 5 --max-time 15 "$PROFILES_URL" || true)
if [[ "$prcode" != "200" ]]; then
log_err "policy-profiles HTTP $prcode (expected 200). BASE=$BASE"
exit 1
fi
if ! jq -e '(.profiles | type == "array")' "$profiles_file" &>/dev/null; then
log_err "policy-profiles response missing .profiles array"
cat "$profiles_file" >&2
exit 1
fi
log "policy-profiles HTTP OK"
PROBE_URL="${BASE}/api/v1/universal-resource-activation/server-funds-sidecar-probe"
log "GET $PROBE_URL (expect 200 OK response or 503 with configured not true when URL unset)…"
pcode=$(curl -sS -o "$probe_file" -w '%{http_code}' --connect-timeout 5 --max-time 20 "$PROBE_URL" || true)
if [[ "$pcode" == "200" ]]; then
if ! jq -e 'type == "object"' "$probe_file" &>/dev/null; then
log_err "Probe response is not a JSON object"
exit 1
fi
log "sidecar-probe HTTP 200 (JSON object returned)"
elif [[ "$pcode" == "503" ]]; then
if ! jq -e '.configured == false' "$probe_file" &>/dev/null; then
log_err "Probe expected 503 with .configured==false when SERVER_FUNDS_SIDECAR_URL unset; got: $(head -c 400 "$probe_file")"
exit 1
fi
log "sidecar-probe HTTP 503 (SERVER_FUNDS_SIDECAR_URL not set — expected in dev)"
elif [[ "$pcode" == "502" ]]; then
log_err "sidecar-probe HTTP 502 — SERVER_FUNDS_SIDECAR_URL is set but sidecar health paths failed. Fix env or sidecar. Body: $(head -c 400 "$probe_file")"
exit 1
else
log_err "sidecar-probe HTTP $pcode (expected 200, 503, or 502). Body: $(head -c 400 "$probe_file")"
exit 1
fi
exit 0