CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

adobe/aem-cloud-service

All AEM as a Cloud Service skills: component development, Workflow model design, development, triggering, launchers, debugging, and triaging, Dispatcher config authoring, advisory, incident response, performance tuning, and security hardening, Rapid Development Environment (RDE) deployment, log inspection, snapshots, and troubleshooting via `aio aem rde`, Jackrabbit Oak query/index tuning (property, Lucene index definitions), and extracting/inventorying JCR queries (XPath, SQL2, QueryBuilder) across a codebase.

Quality

85%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

unified-collection-reader.jsskills/migration/scripts/

/**
 * Unified Collection Reader
 * 
 * Reads unified code transformer collections (cloud-adoption-service format)
 * and returns data in the same format as the CAM BPA fetcher MCP tool.
 */

const fs = require('fs');
const path = require('path');

// Default batch size for paged access to findings. Keep this in sync with
// the documented batch size in migration/SKILL.md and cam-mcp.md.
const DEFAULT_BATCH_SIZE = 5;

// Pattern to subtype mapping (matching cam-bpa-fetcher.ts)
const PATTERN_TO_SUBTYPE = {
  scheduler: "sling.commons.scheduler",
  assetApi: "unsupported.asset.api",
  eventListener: "javax.jcr.observation.EventListener",
  resourceChangeListener: "org.apache.sling.api.resource.observation.ResourceChangeListener",
  eventHandler: "org.osgi.service.event.EventHandler",
  guavaCache: "custom.guava.cache",
  oakIndex: "oak.index.definition",
  inputStream: "java.io.inputstream"
};

// MongoDB-safe to pattern mapping
const MONGO_SAFE_TO_PATTERN = {
  "sling_commons_scheduler": "scheduler",
  "unsupported_asset_api": "assetApi",
  "javax_jcr_observation_EventListener": "eventListener",
  "org_apache_sling_api_resource_observation_ResourceChangeListener": "resourceChangeListener",
  "org_osgi_service_event_EventHandler": "eventHandler",
  "custom_guava_cache": "guavaCache",
  "oak_index_definition": "oakIndex",
  "java_io_inputstream": "inputStream"
};

// Pattern → subtype(s), 1:many. Covers the Java patterns above plus the
// content/legacy-UI patterns whose findings are keyed by JCR path. A subtype
// may belong to more than one pattern (e.g. legacy.static.template is both a
// LUI sub-type and a template-modernization candidate).
const PATTERN_TO_SUBTYPES = {
  scheduler: ["sling.commons.scheduler"],
  assetApi: ["unsupported.asset.api"],
  eventListener: ["javax.jcr.observation.EventListener"],
  resourceChangeListener: ["org.apache.sling.api.resource.observation.ResourceChangeListener"],
  eventHandler: ["org.osgi.service.event.EventHandler"],
  cdw: ["custom.classic.widget"],
  lui: ["legacy.dialog.classic", "legacy.dialog.coral2", "legacy.custom.component", "legacy.static.template"],
  templateModernization: ["legacy.static.template", "custom.static.template"],
  replication: ["forward.replication", "reverse.replication"],
  urc: ["unsupported.runmode"],
  guavaCache: ["custom.guava.cache"],
  oakIndex: ["oak.index.definition"],
  inputStream: ["java.io.inputstream"],
};

// Patterns whose findings are keyed by JCR path (raw keys, generic processor).
const CONTENT_PATTERNS = new Set(["cdw", "lui", "templateModernization", "replication", "urc"]);

/** MongoDB-safe subtype key: dots → underscores (matches the parser). */
function toMongoSafeSubtype(subtype) {
  return subtype ? subtype.replace(/\./g, '_') : null;
}

// Known scheduler identifier
const SCHEDULER_IDENTIFIER = "org.apache.sling.commons.scheduler";

/**
 * BPA Target type (matching cam-bpa-fetcher.ts)
 */
class BpaTarget {
  constructor(pattern, className, identifier, issue, severity = "high") {
    this.pattern = pattern;
    this.className = className;
    this.identifier = identifier;
    this.issue = issue;
    this.severity = severity;
  }
}

/**
 * BPA Result type (matching cam-bpa-fetcher.ts)
 */
class BpaResult {
  constructor(success = false) {
    this.success = success;
    this.environment = 'local';
    this.projectId = 'unified-collection';
    this.targets = [];
    this.summary = {};
    this.message = null;
    this.error = null;
    this.errorDetails = null;
    this.troubleshooting = [];
    this.suggestion = [];
  }
}

/**
 * Convert MongoDB-safe field name back to original format
 */
function fromMongoSafeFieldName(mongoSafeFieldName) {
  return mongoSafeFieldName ? mongoSafeFieldName.replace(/_/g, '.') : null;
}

/**
 * Slice a flat array of findings and return `{ targets, paging }`.
 *
 * Caller must produce `items` in a stable, deterministic order so that
 * incrementing `offset` across calls yields contiguous, non-overlapping batches.
 * `limit: null` disables batching (returns everything).
 */
function paginate(items, { offset = 0, limit = DEFAULT_BATCH_SIZE } = {}) {
  const list = Array.isArray(items) ? items : [];
  const total = list.length;
  const start = Math.max(0, Math.floor(Number(offset)) || 0);
  const unbounded = limit === null;
  const size = unbounded ? total : Math.max(1, Math.floor(Number(limit)) || DEFAULT_BATCH_SIZE);
  const targets = list.slice(start, unbounded ? total : start + size);
  const reached = start + targets.length;
  const nextOffset = reached < total ? reached : null;
  return {
    targets,
    paging: {
      total,
      returned: targets.length,
      offset: start,
      limit: unbounded ? null : size,
      nextOffset,
      hasMore: nextOffset !== null
    }
  };
}

/**
 * Check if unified collection exists
 */
function hasUnifiedCollection(collectionsDir = './unified-collections') {
  const unifiedPath = path.join(collectionsDir, 'unified-collection.json');
  try {
    return fs.existsSync(unifiedPath) && fs.statSync(unifiedPath).isFile();
  } catch (error) {
    return false;
  }
}

/**
 * Get available patterns in unified collection
 */
function getAvailablePatterns(collectionsDir = './unified-collections') {
  if (!hasUnifiedCollection(collectionsDir)) {
    return [];
  }
  
  try {
    const unifiedCollection = readUnifiedCollection(collectionsDir);
    if (!unifiedCollection || !unifiedCollection.subtypes) {
      return [];
    }
    
    const present = new Set(Object.keys(unifiedCollection.subtypes));
    // A pattern is available if ANY of its subtypes is present in the collection.
    const patterns = Object.keys(PATTERN_TO_SUBTYPES).filter(pattern =>
      PATTERN_TO_SUBTYPES[pattern].some(st => present.has(toMongoSafeSubtype(st)))
    );

    return patterns;
  } catch (error) {
    console.error('Error reading unified collection:', error.message);
    return [];
  }
}

/**
 * Read unified collection file
 */
function readUnifiedCollection(collectionsDir = './unified-collections') {
  const unifiedPath = path.join(collectionsDir, 'unified-collection.json');
  
  if (!fs.existsSync(unifiedPath)) {
    return null;
  }
  
  try {
    const content = fs.readFileSync(unifiedPath, 'utf8');
    return JSON.parse(content);
  } catch (error) {
    console.error('Error reading unified collection:', error.message);
    return null;
  }
}

/**
 * Process scheduler data from unified collection
 */
function processSchedulerFromUnified(subtypeData, targets) {
  let count = 0;
  
  // Sort identifiers alphabetically so the iteration order is deterministic
  // across runs, independent of how the unified-collection JSON was written.
  const identifierKeys = Object.keys(subtypeData || {}).sort();
  for (const mongoSafeIdentifier of identifierKeys) {
    const classNames = subtypeData[mongoSafeIdentifier] || [];
    const identifier = fromMongoSafeFieldName(mongoSafeIdentifier);
    
    for (const className of classNames) {
      count++;
      targets.push(new BpaTarget(
        "scheduler",
        className,
        identifier,
        "Uses imperative Scheduler API instead of declarative @SlingScheduled annotation",
        "high"
      ));
    }
  }
  
  return count;
}

/**
 * Process asset API data from unified collection
 */
function processAssetApiFromUnified(subtypeData, targets) {
  let count = 0;
  
  // Sort identifiers alphabetically so the iteration order is deterministic
  // across runs, independent of how the unified-collection JSON was written.
  const identifierKeys = Object.keys(subtypeData || {}).sort();
  for (const mongoSafeIdentifier of identifierKeys) {
    const classNames = subtypeData[mongoSafeIdentifier] || [];
    const identifier = fromMongoSafeFieldName(mongoSafeIdentifier);
    
    for (const className of classNames) {
      count++;
      targets.push(new BpaTarget(
        "assetApi",
        className,
        identifier,
        `Uses unsupported Asset API: ${identifier}`,
        "critical"
      ));
    }
  }
  
  return count;
}

/**
 * Process event listener data from unified collection
 */
function processEventListenerFromUnified(subtypeData, targets) {
  let count = 0;
  
  // Sort identifiers alphabetically so the iteration order is deterministic
  // across runs, independent of how the unified-collection JSON was written.
  const identifierKeys = Object.keys(subtypeData || {}).sort();
  for (const mongoSafeIdentifier of identifierKeys) {
    const classNames = subtypeData[mongoSafeIdentifier] || [];
    const identifier = fromMongoSafeFieldName(mongoSafeIdentifier);
    
    for (const className of classNames) {
      count++;
      targets.push(new BpaTarget(
        "eventListener",
        className,
        identifier,
        `Uses JCR Event Listener: ${identifier}`,
        "high"
      ));
    }
  }
  
  return count;
}

/**
 * Process resource change listener data from unified collection
 */
function processResourceChangeListenerFromUnified(subtypeData, targets) {
  let count = 0;
  
  // Sort identifiers alphabetically so the iteration order is deterministic
  // across runs, independent of how the unified-collection JSON was written.
  const identifierKeys = Object.keys(subtypeData || {}).sort();
  for (const mongoSafeIdentifier of identifierKeys) {
    const classNames = subtypeData[mongoSafeIdentifier] || [];
    const identifier = fromMongoSafeFieldName(mongoSafeIdentifier);
    
    for (const className of classNames) {
      count++;
      targets.push(new BpaTarget(
        "resourceChangeListener",
        className,
        identifier,
        `Uses Resource Change Listener: ${identifier}`,
        "high"
      ));
    }
  }
  
  return count;
}

/**
 * Process event handler data from unified collection
 */
function processEventHandlerFromUnified(subtypeData, targets) {
  let count = 0;
  
  // Sort identifiers alphabetically so the iteration order is deterministic
  // across runs, independent of how the unified-collection JSON was written.
  const identifierKeys = Object.keys(subtypeData || {}).sort();
  for (const mongoSafeIdentifier of identifierKeys) {
    const classNames = subtypeData[mongoSafeIdentifier] || [];
    const identifier = fromMongoSafeFieldName(mongoSafeIdentifier);
    
    for (const className of classNames) {
      count++;
      targets.push(new BpaTarget(
        "eventHandler",
        className,
        identifier,
        `Uses OSGi Event Handler: ${identifier}`,
        "high"
      ));
    }
  }
  
  return count;
}

/**
 * Process Guava cache data from unified collection. One target per
 * **bundle** — the bundle name (the actionable unit) is deduped upstream in
 * `processGuavaCacheFindings` (bpa-local-parser.js), so a bundle that produced
 * hundreds of raw CSV rows (one per Guava-internal class BPA found on its
 * classpath, e.g. `com.google.common.cache.AbstractCache` — never a customer
 * class) still collapses to exactly one migration unit here.
 *
 * Field shape is inverted from every other pattern in this file: `className`
 * carries the actionable bundle name, and `identifier` carries the fixed
 * subtype string `custom.guava.cache` (not a real class) — because BPA's own
 * `identifier` column on this subtype is the Guava-internal class, which is
 * never actionable, so it's discarded rather than round-tripped.
 */
function processGuavaCacheFromUnified(subtypeData, targets) {
  let count = 0;

  const identifierKeys = Object.keys(subtypeData || {}).sort();
  for (const mongoSafeIdentifier of identifierKeys) {
    const bundleNames = subtypeData[mongoSafeIdentifier] || [];
    const identifier = fromMongoSafeFieldName(mongoSafeIdentifier);

    for (const bundleName of bundleNames) {
      count++;
      targets.push(new BpaTarget(
        "guavaCache",
        bundleName,
        identifier,
        `Bundle uses Guava cache: ${bundleName}`,
        "info"
      ));
    }
  }

  return count;
}

/**
 * Process Oak index data from unified collection. Unlike the content/LUI
 * patterns, the unified data here is keyed by the BPA **sub-type** string
 * (`index.rule.violation` / `standard.index.modification`, MongoDB-safe
 * round-tripped), each mapping to an array of raw JCR index paths — so a
 * generic `processContentFromUnified` pass (keyed by JCR path) doesn't fit.
 *
 * Field shape mirrors `processGuavaCacheFromUnified`: `className` carries the
 * actionable JCR index path, and `identifier` carries the BPA sub-type string
 * (which distinguishes a full rule violation from an OOTB index modified in
 * place — both need the Index Converter tool, but the message differs).
 */
function processOakIndexFromUnified(subtypeData, targets) {
  let count = 0;

  const identifierKeys = Object.keys(subtypeData || {}).sort();
  for (const mongoSafeIdentifier of identifierKeys) {
    const indexPaths = subtypeData[mongoSafeIdentifier] || [];
    const identifier = fromMongoSafeFieldName(mongoSafeIdentifier);

    for (const indexPath of indexPaths) {
      count++;
      targets.push(new BpaTarget(
        "oakIndex",
        indexPath,
        identifier,
        `Oak index definition (${identifier}): ${indexPath}`,
        "high"
      ));
    }
  }

  return count;
}

/**
 * Process java.io.InputStream data from unified collection. Keyed by the
 * referenced API (`java.io.FileInputStream`, …) → flagged class names, so each
 * target carries the class to locate (`className`) and the API to classify by
 * (`identifier`) — see migration/references/input-stream.md. When the parser
 * recorded bundles (`meta.inputStreamBundles`), there is one target per
 * (API, class, bundle), each with its `bundle`.
 */
function processInputStreamFromUnified(subtypeData, targets, meta = {}) {
  let count = 0;
  const bundlesByApi = meta.inputStreamBundles || {};

  const identifierKeys = Object.keys(subtypeData || {}).sort();
  for (const mongoSafeIdentifier of identifierKeys) {
    const classNames = subtypeData[mongoSafeIdentifier] || [];
    const identifier = fromMongoSafeFieldName(mongoSafeIdentifier);
    const bundlesByClass = bundlesByApi[identifier] || {};

    for (const className of classNames) {
      for (const bundle of bundlesByClass[className] || [null]) {
        count++;
        const target = new BpaTarget(
          "inputStream",
          className,
          identifier,
          `Uses ${identifier} — triage per references/input-stream.md`,
          "medium"
        );
        if (bundle) target.bundle = bundle;
        targets.push(target);
      }
    }
  }

  return count;
}

/**
 * Process a content/legacy-UI subtype whose unified data is keyed by RAW JCR
 * path (no MongoDB round-trip). Emits one target per finding, with the JCR path
 * as `className` (so downstream `location`/`file` resolve to the path) and the
 * BPA sub-type as `identifier`.
 */
function processContentFromUnified(subtypeData, targets, pattern, subtype) {
  let count = 0;
  const pathKeys = Object.keys(subtypeData || {}).sort();
  for (const jcrPath of pathKeys) {
    const values = subtypeData[jcrPath] || [];
    for (let i = 0; i < values.length; i++) {
      count++;
      targets.push(new BpaTarget(
        pattern,
        jcrPath,                       // className → runbook location/file
        subtype,                       // identifier → runbook detail (the sub-type)
        `${pattern} finding (${subtype}): ${jcrPath}`,
        "high"
      ));
    }
  }
  return count;
}

/**
 * Fetch findings from unified collection (mimics cam-bpa-fetcher behavior).
 *
 * The full ordered list for the requested `pattern` is assembled, then
 * sliced into a batch via {@link paginate}. The returned object contains
 * `targets` (the batch) and `paging` (batch metadata for resuming).
 *
 * Ordering contract: for a given unified-collection.json file and a given
 * `pattern`, successive calls with incrementing `offset` return contiguous,
 * non-overlapping batches covering the full list.
 *
 * @param {string} pattern
 * @param {string} collectionsDir
 * @param {{offset?: number, limit?: number|null|'all'}} [paging]
 */
function fetchUnifiedBpaFindings(pattern = "all", collectionsDir = './unified-collections', paging = {}) {
  const result = new BpaResult();
  
  // Check if unified collection exists
  if (!hasUnifiedCollection(collectionsDir)) {
    result.error = `Unified collection not found: ${collectionsDir}/unified-collection.json`;
    result.troubleshooting = [
      "Run bpa-local-parser.js with --unified flag to create unified collection",
      "Ensure the collections directory path is correct",
      "Check that BPA file has been processed successfully"
    ];
    result.suggestion = [
      "Use: node bpa-local-parser.js <bpa-file-path> [output-directory] --unified",
      "Verify BPA file contains valid findings data"
    ];
    return result;
  }
  
  // Read unified collection
  const unifiedCollection = readUnifiedCollection(collectionsDir);
  if (!unifiedCollection || !unifiedCollection.subtypes) {
    result.error = "Invalid unified collection format";
    result.troubleshooting = [
      "Check that unified-collection.json contains valid JSON",
      "Verify the file has 'subtypes' property",
      "Re-run bpa-local-parser.js if file is corrupted"
    ];
    return result;
  }
  
  // Determine patterns to fetch
  const availablePatterns = getAvailablePatterns(collectionsDir);
  
  if (availablePatterns.length === 0) {
    result.error = "No valid pattern collections found in unified collection";
    result.troubleshooting = [
      "Check that unified collection contains supported subtypes",
      "Verify pattern files contain valid data",
      "Re-run bpa-local-parser.js if data is missing"
    ];
    return result;
  }
  
  // Sort patterns alphabetically so "all" returns findings in a deterministic
  // order independent of how availablePatterns was produced.
  const patternsToFetch = pattern === "all"
    ? [...availablePatterns].sort()
    : availablePatterns.filter(p => p === pattern);
  
  if (patternsToFetch.length === 0) {
    result.error = `Pattern '${pattern}' not found in unified collection. Available: ${availablePatterns.join(', ')}`;
    result.suggestion = [
      `Use one of: ${availablePatterns.join(', ')}, all`,
      "Check that the requested pattern was included in the BPA report"
    ];
    return result;
  }
  
  console.log(`[Unified Collection Reader] Reading patterns: ${patternsToFetch.join(', ')}`);
  
  // Processors for the Java patterns (keyed by class name). Content/legacy-UI
  // patterns use the generic JCR-path processor.
  const JAVA_PROCESSORS = {
    scheduler: processSchedulerFromUnified,
    assetApi: processAssetApiFromUnified,
    eventListener: processEventListenerFromUnified,
    resourceChangeListener: processResourceChangeListenerFromUnified,
    eventHandler: processEventHandlerFromUnified,
    guavaCache: processGuavaCacheFromUnified,
    oakIndex: processOakIndexFromUnified,
    inputStream: processInputStreamFromUnified,
  };

  // Process each pattern — a pattern may map to more than one subtype.
  for (const pat of patternsToFetch) {
    const subtypes = PATTERN_TO_SUBTYPES[pat];
    if (!subtypes) {
      console.warn(`[Unified Collection Reader] Unknown pattern: ${pat}, skipping`);
      continue;
    }

    let count = 0;
    for (const subtype of subtypes) {
      const subtypeData = unifiedCollection.subtypes[toMongoSafeSubtype(subtype)];
      if (!subtypeData) continue;
      count += CONTENT_PATTERNS.has(pat)
        ? processContentFromUnified(subtypeData, result.targets, pat, subtype)
        : JAVA_PROCESSORS[pat](subtypeData, result.targets, unifiedCollection.meta || {});
    }
    result.summary[`${pat}Count`] = count;

    console.log(`[Unified Collection Reader] Processed ${count} findings for pattern: ${pat}`);
  }
  
  if (result.targets.length === 0) {
    result.error = "No findings found in unified collection";
    result.suggestion = [
      "Verify that BPA file contained relevant findings",
      "Check that bpa-local-parser.js processed the file correctly",
      "Inspect unified collection file for expected data structure"
    ];
    return result;
  }

  const { targets, paging: p } = paginate(result.targets, paging);
  result.targets = targets;
  result.paging = p;
  result.success = true;
  console.log(
    `[Unified Collection Reader] Batch ${p.returned}/${p.total} ` +
    `(offset ${p.offset}, hasMore=${p.hasMore})`
  );
  return result;
}

/**
 * Get summary of unified collection (derived from unified-collection.json).
 * Returns { timestamp, subtypes, totalFindings } for display purposes.
 */
function getUnifiedCollectionSummary(collectionsDir = './unified-collections') {
  const unifiedCollection = readUnifiedCollection(collectionsDir);
  if (!unifiedCollection || !unifiedCollection.subtypes) {
    return null;
  }

  const subtypes = Object.keys(unifiedCollection.subtypes);
  let totalFindings = 0;
  for (const subtypeKey of subtypes) {
    const subtypeData = unifiedCollection.subtypes[subtypeKey] || {};
    for (const classNames of Object.values(subtypeData)) {
      totalFindings += (classNames || []).length;
    }
  }

  return {
    timestamp: unifiedCollection.meta?.timestamp || null,
    subtypes,
    totalFindings: unifiedCollection.meta?.totalFindings ?? totalFindings
  };
}

/**
 * CLI interface for testing
 *
 * Usage: node unified-collection-reader.js <pattern> [collectionsDir] [offset] [limit]
 *   offset defaults to 0
 *   limit  defaults to 5 (use 'all' to disable batching)
 */
function main() {
  const args = process.argv.slice(2);
  const pattern = args[0] || 'all';
  const collectionsDir = args[1] || './unified-collections';
  const offset = args[2] !== undefined ? Number(args[2]) : 0;
  const limit = args[3] !== undefined
    ? (args[3] === 'all' ? null : Number(args[3]))
    : DEFAULT_BATCH_SIZE;

  console.log('Unified Collection Reader');
  console.log('========================');
  console.log(`Pattern: ${pattern}`);
  console.log(`Collections Directory: ${collectionsDir}`);
  console.log(`Offset: ${offset}`);
  console.log(`Limit:  ${limit === null ? 'all' : limit}`);
  console.log('');
  
  // Check if unified collection exists
  if (!hasUnifiedCollection(collectionsDir)) {
    console.error(`❌ Unified collection not found: ${collectionsDir}/unified-collection.json`);
    console.error('');
    console.error('To create unified collection:');
    console.error('  node bpa-local-parser.js <bpa-file-path> [output-directory] --unified');
    process.exit(1);
  }
  
  // Show available patterns
  const availablePatterns = getAvailablePatterns(collectionsDir);
  console.log(`Available patterns: ${availablePatterns.join(', ')}`);
  
  // Show summary
  const summary = getUnifiedCollectionSummary(collectionsDir);
  if (summary) {
    console.log(`Total subtypes: ${summary.subtypes?.length || 0}`);
    console.log(`Total findings: ${summary.totalFindings || 0}`);
    console.log(`Created: ${summary.timestamp || '(unknown)'}`);
  }
  
  console.log('');
  
  // Fetch findings
  const result = fetchUnifiedBpaFindings(pattern, collectionsDir, { offset, limit });

  if (result.success) {
    const p = result.paging;
    console.log(`✅ Batch ${p.returned}/${p.total} (offset ${p.offset}, next ${p.nextOffset ?? 'none'}, hasMore=${p.hasMore})`);
    console.log('');
    
    // Group by pattern
    const byPattern = result.targets.reduce((acc, target) => {
      if (!acc[target.pattern]) acc[target.pattern] = [];
      acc[target.pattern].push(target);
      return acc;
    }, {});
    
    Object.entries(byPattern).forEach(([pat, targets]) => {
      console.log(`${pat.toUpperCase()}:`);
      targets.forEach(target => {
        console.log(`  - ${target.className} (${target.severity}): ${target.issue}`);
      });
      console.log('');
    });
  } else {
    console.error('❌ Error:', result.error);
    if (result.troubleshooting?.length > 0) {
      console.error('');
      console.error('Troubleshooting:');
      result.troubleshooting.forEach(tip => console.error(`  - ${tip}`));
    }
    if (result.suggestion?.length > 0) {
      console.error('');
      console.error('Suggestions:');
      result.suggestion.forEach(tip => console.error(`  - ${tip}`));
    }
    process.exit(1);
  }
}

// Run CLI if called directly
if (require.main === module) {
  main();
}

module.exports = {
  hasUnifiedCollection,
  getAvailablePatterns,
  fetchUnifiedBpaFindings,
  getUnifiedCollectionSummary,
  readUnifiedCollection,
  paginate,
  DEFAULT_BATCH_SIZE,
  PATTERN_TO_SUBTYPES,
  BpaTarget,
  BpaResult
};

skills

migration

README.md

SKILL.md

README.md

tile.json